前面说了如何管理对共享、易变状态的数据的状态的访问,《JAVA并发编程》第三章主要介绍共享和发布可以被多线程安全访问的对象,使用了java.util.concurrent包下的类为基础,创建线程安全的类和构建安全的并发应用程序。
synchronized不仅仅与操作原子性和关键区域定界相关,它还有个重要却不明显的作用——内存可见性。同步不仅仅是防止线程修改正在被别的线程使用的对象状态,还可以保证当一个线程修改了对象状态后,其他线程可以看到状态的更新。通过显示的同步或利用内置的同步的类库,可以保证发布的对象是线程安全的。
可见性
当一个线程对对象状态进行读操作,另一个线程对该对象状态进行写操作时,如果没有线程同步,则无法保证读对象的线程能即时的读取到写对象更新后的值。
package com.zyp.test.concurrent;
public class NoVisibility {
private static boolean ready;//默认值为false
private static int number;
private static class ReaderThread extends Thread {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
System.out.println("end");
}
}
这段代码中有两个线程,一个主线程和一个读线程都要反问number和ready。主线程启动读线程后,将ready置为true。读线程一直循环,直到发现ready为true后,打印number的值。看起来读线程打印结果:
1:可能是明显的42,
2:也可能打印出0,
3:也可能永远不停止。
因为没有适当的同步,不能保证主线程写了number和ready一定对读线程是可见的。上面读线程可能永远不停止,因为ready可能对读线程永远不可见。上面可能打印出0,跟重排序有关。
重排序(reordering):在一个线程中的方法的执行顺序并不保证与代码中顺序一致。在没有同步的情况下,java内存模型(JMM)允许编译器重新排序操作,以获取多处理器带来的更多好处。
重排序后,编译器后的代码可能先执行ready的赋值再执行number的赋值。