@lemonguge
2015-07-07T01:49:20.000000Z
字数 7416
阅读 383
Concurrency
Java是最先支持多线程的开发的语言之一
在过去单CPU时代,单任务在一个时间点只能执行单一程序。之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程。虽然并不是真正意义上的“同一时间点”,而是多个任务或进程共享一个CPU,并交由操作系统来完成多任务间对CPU的运行切换,以使得每个任务都有机会获得一定的时间片运行。
现代的计算机伴随着多核CPU的出现,多线程技术使得在一个程序内部能拥有多个线程并行执行。一个线程的执行可以被认为是一个CPU在执行该程序。当一个程序运行在多线程下,就好像有多个CPU在同时执行该程序。
进程就是正在进行中的程序,线程就是进程中一个负责程序执行的控制单元(执行路径)。
// 进程public class Proc {public static void main(String[] args) throws Exception {Runtime rt = Runtime.getRuntime();Process p = rt.exec("notepad.exe"); // 打开记事本Thread.sleep(1000); // 等待1秒p.destroy(); // 关闭记事本}}
开启多个线程是为了同时运行多部分代码。每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
JVM启动时就启动了多个线程,至少有两个线程可以分析的出来:
1. 执行main函数的线程,该线程的任务代码都定义在main函数中。
2. 负责垃圾回收的线程。
class Demo {@Overrideprotected void finalize() throws Throwable {System.out.println(Thread.currentThread().getName()+"@thread run.."); // 静态方法获取当前线程名称super.finalize();}}public class Thd {public static void main(String[] args) throws InterruptedException {System.out.println(Thread.currentThread().getName()+"@thread run..");new Demo();System.gc();Thread.sleep(100);}} /* Output:main@thread run..Finalizer@thread run..*///:~
想想这么一个问题,如果一个线程在读一个内存时,另一个线程正向该内存进行写操作,那进行读操作的那个线程将获得什么结果呢?是写操作之前旧的值?还是写操作成功之后的新值?或是一半新一半旧的值?或者,如果是两个线程同时写同一个内存,在操作完成后将会是什么结果呢?是第一个线程写入的值?还是第二个线程写入的值?还是两个线程写入的一个混合值?
因此如没有合适的预防措施,任何结果都是可能的。而且这种行为的发生甚至不能预测,所以结果也是不确定性的。我们在开发中会常常遇见上面的问题,尽管面临着很多的挑战,多线程有一些优点使得它一直被使用。
从一个单线程的应用到一个多线程的应用并不仅仅带来好处,它也会有一些代价。不要仅仅为了使用多线程而使用多线程。而应该明确在使用多线程时能多来的好处比所付出的代价大的时候,才使用多线程。
创建一个自定义线程时,我们首先都需要明确此线程运行的任务。
编写线程运行时执行的代码(任务)有两种方式:一种是创建java.lang.Thread子类的一个实例并重写run方法,第二种是创建类的时候实现Runnable接口。
Thread类,任务就通过覆盖Thread类中的run方法来体现,run方法就是封装自定义线程运行任务的函数。而Thread类自身就实现了Runnable接口。Runnable,该接口只有一个run方法需要实现。将线程的任务代码封装到run方法中,并将实现了Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。对于创建线程任务的这两种方式哪种好,并没有一个确定的答案,它们都能满足要求。笔者更倾向于实现Runnable接口这种方法。根据面向对象的思想,我们应该明确任务是否属于Thread继承体系(is a),还仅只是额外的功能,而往往在开发中需要的是一个多线程的功能。此外线程池可以有效的管理实现了Runnable接口的线程,如果线程池满了,新的线程就会排队等候执行,直到线程池空闲出来为止。而如果线程是通过实现Thread子类实现的,这将会复杂一些。
调用线程对象的
start方法开启线程。
run方法会在调用start方法之后被执行,一旦线程启动后start方法就会立即返回,而不会等待到run方法执行完毕才返回,就好像run方法是在另外一个CPU上执行一样。
// Java线程的演示class RunExt extends Thread {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "@run..");}}class RunImp implements Runnable {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "@run..");}}public class Run {public static void main(String[] args) {System.out.println(Thread.currentThread().getName() + "@run..");new RunExt().start(); // 继承自Thread的线程new Thread(new RunImp()).start(); // 实现Runnable接口,作为构造函数的参数进行传递}} /* Output: // 输出顺序并不确定,以下是一种输出顺序main@run..Thread-1@run..Thread-0@run..*///:~
需要注意的是,尽管启动线程的顺序是有序的,但是执行的顺序并非是有序的。JVM和操作系统一起决定了线程的执行顺序,执行顺序和线程的启动顺序并非一定是一致的。
创建并运行一个线程所犯的常见错误是调用线程的run方法而非start方法,起初你并不会感觉到有什么不妥,因为run方法的确如你所愿的被调用了。但是事实上run方法并非是由刚创建的新线程所执行的,而是被创建新线程的当前线程所执行了。想要让创建的新线程执行run方法,必须调用新线程的start方法。
public class Run {public static void main(String[] args) {run();}public static void run() {Thread thread = new Thread("新的线程") { // 自定义线程名@Overridepublic void run() {System.out.println(Thread.currentThread().getName());}};thread.run(); // 并没有开启新的线程}} /* Output:main // 当前线程仍然是main线程,而没有输出:新的线程*///:~
在讲解多线程的状态之前,笔者有必要让大家了解一下两个概念:
对于上面的这两个概念,举个例子:你在食堂排队(处理队列)打饭,还没有轮到你(此时你仅具有执行资格),过了一段时间后,终于轮到食堂阿姨(CPU)给你打饭(你还在排队中,同时具有执行资格和执行权),当你打完饭你就离开了(释放执行权和执行资格),吃完要是你还是觉得饿,你又会去排队打饭。
在大家明白这两个概念以后,笔者认为多线程的状态有五种:
start方法开启线程。run方法(线程任务)的结束或是调用了stop方法(该方法已过时)。
public class Run {public static void main(String[] args) throws InterruptedException {stop();}public static void stop() throws InterruptedException {Thread thread = new Thread("新的线程") {@Overridepublic void run() {for (int i = 0; i < 100; i++)System.out.println(Thread.currentThread().getName() + ":" + i);}};thread.start(); // 开启线程thread.sleep(5);thread.stop(); // 终止线程}} ///~ // 新的线程并没有输出到100就被终止,处于消亡状态
对于运行状态、临时阻塞状态和冻结状态,有一个相互的转换关系。
sleep(time)方法:在指定的毫秒数内让当前正在执行的线程休眠,当调用该方法时,会使处于运行状态的线程变为冻结状态,当时间一到,线程自己唤醒,回到临时阻塞状态或者是运行状态。wait()方法:导致当前线程等待,与sleep方法类似,但是线程不能自己唤醒,需要被该线程的监视器notify()或notifyAll()唤醒。notify()唤醒该监视器所监视的处于冻结状态的所有线程的任意一个线程,notifyAll()唤醒该监视器所监视的所有冻结线程。(监视器后面会进行介绍)
当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件(race conditions)。导致竞态条件发生的代码区称作临界区。
举个例子:有储户a和b向同一个银行存钱,都存三次且每次都存100。
// 银行class Bank {private int sum; // 银行库存public void add(int num) { // 每次存钱,库存增加sum += num;System.out.println(Thread.currentThread().getName() + " sum=" + sum);}}// 储户class Customer implements Runnable {private Bank bank;public Customer(Bank bank) { this.bank = bank; }@Overridepublic void run() {for (int i = 0; i < 3; i++) // 存三次,每次存100bank.add(100);}}public class BankDemo {public static void main(String[] args) {Bank bank = new Bank();// 指定a和b储户存钱的银行为bankCustomer a = new Customer(bank);Customer b = new Customer(bank);// 开启两个线程可以使a和b同时存钱new Thread(a, "a").start(); // 指定线程名为anew Thread(b, "b").start(); // 指定线程名为b}} /* Output: // 输出顺序并不确定,以下是一种输出顺序b sum=200a sum=200a sum=400a sum=500b sum=300 // 运行程序的电脑为双核(以后会进行解释)b sum=600*///:~
通过以上的输出结果可以发现,程序明显有问题!我们可以预料的正确输出应该是从100到600逐步递增。为什么会出现这种情况呢?a和b两个线程同时对银行库存sum进行了写操作,我们无法知道操作系统何时会在两个线程之间切换,具体分析该输出结果的前两条如下:
Bank的add方法时,执行sum += num;语句后,sum库存为100后,还未在主控台打印语句,CPU进行了运行切换,a线程便获得了执行权;Bank的add方法时,执行sum +=num;语句,sum库存此时为200,还未在主控台打印语句,CPU进行了运行切换,b线程又获得了执行权;sum库存为200,所以打印了200,接着CPU进行了运行切换,a线程获得了执行权;如果a和b储户不是向同一个银行存钱就不会导致竞态条件。
Bank bank1 = new Bank();Bank bank2 = new Bank();Customer a = new Customer(bank1);Customer b = new Customer(bank2);
线程控制逃逸规则:如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
资源可以是对象,数组,文件,数据库连接,套接字等等。Java中你无需主动销毁对象,所以“销毁”指不再有引用指向对象。
多个线程同时读同一个资源不会产生竞态条件,线程安全的代码不包含竞态条件。我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全。
public class ImmutableValue { // 线程安全private int value = 0;public ImmutableValue(int value) {this.value = value;}public int getValue() { return this.value; }}
上面这段代码意味着一旦ImmutableValue实例被创建,value(通过构造函数赋值的)成员变量就不能再被修改,这就是不可变性。但你可以通过getValue()方法读取这个变量的值。
public class ImmutableValue { // 也是线程安全private int value = 0;public ImmutableValue(int value) {this.value = value;}public int getValue() { return this.value; }public ImmutableValue add(int valueToAdd) {return new ImmutableValue(this.value + valueToAdd);}}
请注意add()方法以加法操作的结果作为一个新的ImmutableValue类实例返回,而不是直接对它自己的value变量进行操作,所以也是线程安全的。
即使一个对象是线程安全的不可变对象,指向这个对象的引用也可能不是线程安全的。
public class Calculator {private ImmutableValue currentValue = null;public ImmutableValue getValue() {return currentValue;}public void setValue(ImmutableValue newValue) {this.currentValue = newValue;}public void add(int newValue) {this.currentValue = this.currentValue.add(newValue);}}
Calculator类持有一个成员变量currentValue指向ImmutableValue实例。注意,通过setValue()方法和add()方法可能会改变这个引用。Calculator类本身还是可变的,因此Calculator类不是线程安全的。
要使Calculator类实现线程安全,将getValue()、setValue()和add()方法都声明为同步方法即可。
不变(Immutable)和只读(Read Only)是不同的,当一个变量是“只读”时,变量的值不能直接改变,但是可以在其它变量发生改变的时候发生改变。比如,一个人的出生年月日是“不变”属性,而一个人的年龄便是“只读”属性,但是不是“不变”属性。随着时间的变化,一个人的年龄会随之发生变化,而一个人的出生年月日则不会变化。这就是“不变”和“只读”的区别。