1 同步
如何同步多个线程对共享资源的访问是多线程编程中最基本的问题之一。当多个线程并发访问共享数据时会出现数据处于计算中间状态或者不一致的问题,从而影响到程序的正确运行。我们通常把这种情况叫做竞争条件(race condition),把并发访问共享数据的代码叫做关键区域(critical section)。同步就是使得多个线程顺序进入关键区域从而避免竞争条件的发生。
1.1 Synchronized关键字
Synchronized是Java多线程编程中最常用的关键字。所有的Java 对象都有自己唯一的隐式同步锁。该锁只能同时被一个线程获得,其他试图获得该锁的线程都会被阻塞在对象的等待队列中直到获得该锁的线程释放锁才能继续工作。Synchronized关键字通常有两种用法。当Synchronized关键字用于类方法定义中时,表示所有调用该方法的线程都必须获得当前对象的锁。这种方式比较简单,但是同步的粒度比较大,当一个线程要执行某个对象的同步方法的时候,必须同时没有任何其他线程在执行该对象的任一同步方法。此外,同步方法中的所有代码均在同步块中,获得锁的线程必须在执行完所有的代码离开该方法后才会释放锁,这些代码中可能只有一部分涉及到对共享资源(例如成员变量)的访问需要同步,其余则不需要,那么这样粗粒度的同步显然增加了其他线程的等待时间。Synchronized的另一种 用法允许作用在某个对象上,并且只同步一段代码而不是整个方法。
synchronized (object) {
// 需要同步的代码
}
这里synchronized所作用的对象可以是类的某个成员变量,也可以是这个类对象(用this表示)。这种用法使得程序员可以根据需要同步不同的成员变量,而不总是当前类对象,提高了灵活性。
值得一提的是,并不是只有对象才有锁,类本身也有自己的锁,这使得static方法同样可以用synchronized来修饰。访问同步static方法的线程需要获得类的同步锁才能继续执行。
1.2 Volatile关键字
在Java内存模型中每个线程拥有自己的本地存储(例如寄存器),并且允许线程拥有变量值的拷贝。这使得本来不需要同步的一些原子操作,例如boolean成员变量存储和读取也变得不安全。设想我们有个叫做done的boolean成员变量和一个当done为true时才会停止的循环,该循环由后台线程执行,另一个UI线程等待用户输入,用户按下某个按钮以后会把done设成true从而终止循环。由于UI线程自己本地拥有done的拷贝,用户在按下按钮时只是把自己本地的done设成了true而没有及时更新主内存中的done,所以后台线程由于看不到done的改变而不会终止。即使主内存中的done变化了,后台线程也会因为自己本地的变量值没有及时更新而没有察觉到done的变化。解决这一问题的方法之一是为done提供synchronized的setter和getter方法,这是因为获得同步锁会迫使所有变量的值从临时存储(寄存器)写会主内存。除此之外,Java提供了一个解决这个问题更为优雅的方法:Volatile关键字。每次使用volatile变量,JVM都会保证从主内存中读取它的值;同样每次修改volatile变量,JVM都会把值写回到主内存中。
Volatile适用的场景比较严格,必须很清楚地看到volatile只是告诉JVM对于该变量的读写必须每次都在主内存中进行而禁止使用临时的拷贝来优化,它只是出于JVM特殊的内存模型的需要,并没有同步的功能。因此只有对volatile变量进行的原子操作(读取和赋值)才是线程安全的,像自增++自减--这样包含多个命令的操作仍然需要其它的同步措施。
另一个需要注意的的地方是当用volatile修饰数组的时候,它只是说数组的引用是volatile的,而数组中的元素还是和普通变量一样,可能被JVM优化,我们无法为数组中的元素加上volatile修饰。解决上述问题的方法是使用Atomic变量。作为使用volatile修饰数组的一个例子,可以参考java.util.concurrent.CopyOnWriteArrayList。它的add操作是通过复制原来的数组并把新元素添加到新数组末尾然后再把内部数组引用变量指向新数组来实现的,因此数组变量经常会被修改,需要使用volatile。
1.3 显式锁Lock
尽管synchronized关键字可以解决大多数同步问题,J2SE5.0还是引入了Lock接口。相比使用synchronized关键字获取对象隐式的同步锁,我们称Lock为显式锁。使用显式锁的一个显而易见的好处是它不再属于某个对象,从而可以在多个对象之间共享它。Lock接口有lock()和unlock()两个方法,使用它们和使用synchronized关键字类似,在进入需要同步的代码之前调用lock,在离开同步代码块时调用unlock。通常unlock会被放在finally中以保证即使同步代码块中有异常发生,锁仍然可以被释放。