11.2 基本线程同步方式

对于多线程程序中存在的数据竞争,需要利用Java平台提供的同步机制来确保对共享变量的访问存在合适的“在之前发生”的顺序。Java平台提供的基本同步方式包括synchronized关键词和Object类中提供的wait、notify和notifyAll方法。

11.2.1 synchronized关键词

synchronized关键词应该是最为开发人员所熟悉的多线程开发时可以使用的结构,是基本的线程同步方式之一。synchronized关键词可以添加在方法或代码块之上。声明为synchronized的方法或代码块在同一时刻只能有一个线程允许访问。如果当前已经有线程正在访问synchronized方法或代码块,那么其他试图访问该方法或代码块的线程会处于等待状态。这种互斥性使该方法或代码块中的代码逻辑实际上成为一个原子操作。从“在之前发生”顺序的角度更容易理解synchronized关键词的含义。所有的Java对象都有一个与之关联的监视器对象(monitor),允许线程在该监视器对象上进行加锁和解锁操作。每个synchronized关键词在使用时都与一个监视器对象相对应。对于声明为synchronized的方法,静态方法对应的监视器对象是所在Java类对应的Class类的对象所关联的监视器对象,而实例方法使用的是当前对象实例所关联的监视器对象。对于synchronized代码块,对应的监视器对象是synchronized代码块声明中的对象所关联的监视器对象。

在一个线程允许执行方法或代码块之前,需要先获取对应的监视器对象上的锁。在执行完成之后,该线程所持有的锁会被自动释放。根据“在之前发生”顺序,上一次的解锁操作肯定在下一次的成功加锁操作之前发生。由于解锁操作是上一次线程在synchronized方法或代码块中执行的最后一个动作,而加锁操作是下一次线程执行synchronized方法或代码块时的第一个动作,所以上一次线程运行时对共享变量所做的修改对下一次线程中的动作是肯定可见的。这是开发人员使用synchronized关键词时可以得到的保证。Java虚拟机和编译器会负责完成实际的工作。当锁被释放时,对共享变量的修改会从CPU缓存中直接写回到主存中;当锁被获取时,CPU的缓存中的内容被置为无效的状态,从主存中重新读取共享变量的值。当有线程在执行synchronized方法或代码块时,其他线程由于无法获取锁而处于等待状态,不会影响当前线程的运行。编译器在处理synchronized方法或代码块时,不会把其中包含的代码移动到synchronized方法或代码块之外,从而避免了由于代码重排而造成的问题。

synchronized关键词的使用比较简单。代码清单11-6给出了对代码清单11-1中IdGenerator类的修改方式。类SynchronizedIdGenerator中的getNext方法使用了synchronized来声明,而getNextV2方法则包含了一个synchronized代码块。两个方法的作用是相同的。由于getNext方法是一个对象的实例方法,因此在同步时使用的监视器对象是当前对象实例所关联的监视器对象。而getNextV2方法中的synchronized代码块声明中也同样使用了当前对象实例this。

代码清单11-6 使用synchronized生成唯一标识符的Java类


public class SynchronizedIdGenerator{

private int value=0;

public synchronized int getNext(){

return value++;

}

public int getNextV2(){

synchronized(this){

return value++;

}

}

}


在使用synchronized时有一个错误倾向,那就是被synchronized所保护的代码过多,比如一个方法中只有少数几行代码访问共享变量,却把整个方法声明为synchronized。这么做虽然不会对程序的正确性造成影响,但是会影响程序的性能。正确的做法是把方法中需要同步的代码用synchronized代码块包围即可。