线程间通信主要通过对字段和引用对象共享存取实现的,而这很容易导致线程冲突和内存一致性错误。那什么是线程冲突呢?线程冲突是指多个线程对某个字段进行访问或者操作,而这些操作有可能由多个步骤组成,即使操作只是简单的语句,比如a++。由于存在多个步骤就导致了多个线程可能对数据交叉操作,这样就容易引起操作结果与期望的不一致,举个例子如下:
class Counter { private int c = 0; public void increment() { c++; } public void decrement() { c--; } public int value() { return c; } }
假设现在有两个线程A和B,A调用increment方法而差不多同时B调用decrement方法,那么执行交叉执行的情况可能是这样的:
根据上面的执行情况,c的最终结果为-1,线程A对c的操作结果被线程B对c的操作覆盖了。而这只是可能的一种情况,或者线程A会覆盖线程B的操作,也可能根本不会出现错误。正是由于这种不可预见性,线程冲突带来的bug是很难被发现和修复的。
当不同线程对同一数据应该得到相同的结果但得到的却是不同的结果时,内存一致性的错误便发生了。避免内存一致性错误的关键是理解happens-before 关系。该关系保证被一个特定语句写入内存的数据对另一个特定语句是可见的。考虑上面的例子,假设线程A执行了increment()方法,线程B执行输出c值的语句Sytem.out.println(c),那么存在B得到的结果为0而不是1的可能性,但如果输出c的语句在A线程中,则输出结果为1。这是因为没有保证线程A对c做的修改对线程B是可见的,除非程序员在两个语句中建立了happens-before关系。
解决线程冲突和内存一致性错误的一种工具就是线程同步。Java提供了两种线程同步的语法,synchronized方法和synchronized语句。synchronized方法只需要在方法声明中添加synchronized关键字,如:publicsynchronized voidincrement()即可。方法同步有两个效果:
需要注意的是构造方法是不可以被同步,在构造方法上使用synchronized关键字是语法错误。如果一个对象被多个线程共享,所有对该对象中变量的读写都要通过synchronized方法,唯一的例外是final字段,因为final字段在对象构造完成后是不可以再被修改的,因此可以安全的使用非synchronized方法读取。
在学习synchronized语句前先要学习一下内部锁或者监控锁,线程同步就是围绕该内部实体建立的,每个对象都有与之关联的内部锁。内部锁在线程同步中起了两方面的作用:强制排它访问一个对象的状态和建立happens-before 关系。依照惯例,一个需要独占的、一致性的访问对象字段的线程必须在访问它们之前获得对象的内部锁,并在执行完毕后释放内部锁。一个线程在已经获得锁和释放锁之间的时间内拥有内部锁,只要一个线程拥有内部锁其它线程就不能获得相同的锁,其它线程将在视图获得该锁时阻塞。当一个线程释放了内部锁后,happens-before 关系在那个操作和后续对该锁的请求之间建立了。
当一个线程调用synchronized方法时,自动获得了该方法对象的内部锁并在方法返回时释放锁,锁释放在方法因为未捕获的异常返回时也会发生。静态synchronized方法关联的是类而不是对象,因此一个线程获得的是与该类对应的Class对象的内部锁。
synchronized语句必须显示地指明提供内部锁的对象,如:
public void addName(String name) { synchronized(this) { lastName = name; nameCount++; } nameList.add(name); }如果没有synchronized语句则需要一个单独的,不是synchronized的方法来执行nameList.add(name)调用。synchronized语句在细粒度同步方面也有很好的用处。假设FineGrained有两个字段c1和c2,但它们从不一起使用,对这两个字段的更新都需要同步,但没有理由阻止c1的更新与c2的更新交叉进行(因为它们从不一起使用,所以交叉更新不会出现不一致的现象),使用synchronized方法因为增加了不必要的阻塞而降低了并发性,此时synchronized语句就有了用武之地了,可以提供两个对象分别为这两个字段提供内部错,这样既不会相互影响也不会降低并发性:
public class FineGrained{ private long c1 = 0; private long c2 = 0; private Object lock1 = new Object(); private Object lock2 = new Object(); public void inc1() { synchronized(lock1) { c1++; } } public void inc2() { synchronized(lock2) { c2++; } } }
虽然一个线程不能获取被其它线程拥有的锁,但可以获取已经被它拥有的锁。允许一个线程多次获取同一个锁被称为可重入同步。假如某个同步代码中直接或间接地调用了其它包含同步代码的方法,而两个同步代码使用的是相同的锁,如果没有可重入同步,则会出现一个线程被它自己阻塞的情况。
线程同步提供了解决线程冲突和内存一致性错误的方法,但在使用过程还需格外小心,严格区分哪些字段交叉访问没有问题,哪些字段相互影响。