第66条:同步访问共享的可变数据
关键字synchronized可以保证同一时刻,只有一个线程可以执行某个方法,或者摸一个代码块。许多人把同步的概念理解为一种互斥的方式,即,当一个对象被一个线程修改的时候,可以阻止另一个线程观察到对象内部不一致的状态。这种观点对象创建的时候处于一致的状态,当有方法访问它的时候,它就被锁定了。
这种观点是正确的,但是它并不是同步的全部意义。同步不仅可以阻止一个线程看到对象处于不一致的状态中,它还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效果。
“为了提高性能,在读写原子数据的时候,应该避免使用同步。”这个建议是非常危险而且错误的。 因为,虽然读写原子数据都是原子操作,但是不保证一个线程的写入的值对于另一个线程是完全可见的。为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。
如果对共享的可变数据的访问不能同步,其后果将非常可怕,即使这个变量是原子可读写的。要阻止一个线程妨碍另一个线程,建议做法是让第一个线程轮询(poll)一个boolean域,这个域一开始为false,但可以通过第二个线程设置为true,以表示第一个线程将终止自己。因为boolean域是原子的,所以保证程序在访问这个域时不会使用同步。
public class Thread {
private static boolean stopRequest;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int i = 0 ;
/* while (!stopRequest){
i++;
//System.out.println(i); 如果注掉 循环不会关闭 线程一直运行。 如果打开注释则 会关闭。考虑system源码是否有同步功能 源码中确实有同
步功能 使得线程可以正常关闭。源码中得到验证system确实有同步锁。
}*/
while (!stopRequest){
i++;
}
}
});
thread.start();
TimeUnit.SECONDS.sleep(1);
stopRequest = true;
System.out.println("Thread over.");
}
//需要把requestStop 和stopRequest同步化。如果读和写操作没有被同步,同步就不会起作用。
private static synchronized void requestStop() {
stopRequest = true;
}
private static synchronized boolean stopRequest() {
return stopRequest;
}
//由于++运算符不是原子的,因此在多线程的时候会出错。++运算符执行两项操作:1、读取值;2、写回新值(相当于原值+1)。
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
}
让一个线程在短时间内修改一个数据对象,然后与其他线程共享,这是可以接受的,只同步共享对象引起的动作。然后其他线程没有进一步的同步也可以读取对象。只要它没有再被修改。这种对象被称作事实上不可变的(effectively immutable)。将这种对象引用从一个线程传递到其他的线程被称作安全发布(safe publication)。 安全发布对象引用有许多种方法:可以将它保存在静态域中,作为类初始化的一部分;可以将它保存在volatile域、final域或者通过正常锁定访问的域中,或者可以将它放到并发的集合中。
总之,当多个线程共享可变数据的时候,每个读或写数据的线程都必须执行同步。如果没有同步,就无法保证一个线程所做的修改可以被另一个线程获知。未能同步共享可变数据会造成程序的活性失败(liveness failure)和安全性失败(safety failure)。 这样的失败时最难调试的。他们可能是间歇性的,且与时间相关,程序的行为在不同的VM可能根本不同,如果只需要线程之间的交互通信,而不需要互斥,volatile修饰符就是一种可以接受的同步形式,但要正确的使用它可能需要一些技巧。