下面的代码说明了当多个线程在没有同步的情况下共享数据时出现的错误。在代码中,主线程和读线程都会访问共享变量ready和number。很显然代码看起来会输出42,但事实上肯可能输出0(重排序导致),甚至根本无法终止。这是因为代码中没有足够的同步机制,因此无法保证主线程写入ready和number对读线程是可见的。
public class NoVisibility{
private static bolean ready;
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 ReadThread().start();
number = 42;
ready = true;
}
}
当读线程查看ready变量时,可能会得到一个失效的值。除非每次访问变量时都使用同步,否则很可能获得该变量的一个失效值。
下面的代码是很容易出现失效值问题的:如果一个线程调用了set,那么另一个正在调用get的线程可能会看到更新后的值,也可能看不到。
public class MutableInteger{
private int value;
public int get(){ return value; }
public void set(int alue) { this.value = value; }
}
将上面的代码改为线程安全的类,仅对set方法进行同步是不够的,调用get的线程仍然会看到失效值。
public class MutableInteger{
@GuardedBy("this") private int value;
public synchronized int get(){ return value; }
public synchronized void set(int alue) { this.value = value; }
}
最低安全性:当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是某个线程设置的值,而不是一个随机值。这种安全性被称为最低安全性。
最低安全性适用于绝大多数变量,但是存在一个例外:非volatile类型的64位数值变量(long和double)。Java内存模型要求变量的读取和写入必须是原子操作,但对于非volatile类型的64位数值变量,JVM允许将64位的操作分解为 两个32位的操作。那么如果在多线程中对该变量的读取和写入在不同线程中进行,很可能读取到某个值的高32位和另一个值的低32位。即使不考虑实习失效数据问题,多线程中共享64位类型数据也是不安全的,除非用volatile声明或者加锁。
当线程B执行由锁保护起来的代码时,可以看到线程A之前在同一个同步代码块中所有的操作结果。如果没有同步,那么就无法实现上述保证。
现在可以理解为什么访问某个共享的且可变的变量时要求所有线程在同一个锁上同步,就是为了保证某个线程对变量的修改对其他线程来讲都是可见的。否则一个线程在未持有正确的锁的情况下读取某个变量,那么读到的可能是一个失效值。
Volatile变量是一种稍弱的同步机制,用来确认变量的更新操作通知到其他线程。当把变量声明为volatile时,这个变量就是共享的,编译器不会将该变量上的操作与其他内存操作一起重排序。Volatile变量也不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它们。Volatile变量的正确使用方式包括:
下面的代码是volatile最经典的用法:检查某个状态标记以判断是否执行某个操作(单例模式):
public class Singleton{
private volatile static Singleton uniqueInstance;
private Singleton(){}//构造器定义为私有
public static Singleton getInstance(){
if(uniqueInstance==null){ //检查实例,不存在则进入同步块
synchronized (Singleton.class){
if(uniqueInstance==null){ //进入同步块后再次检查
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
加锁机制既可以保证可见性又可以保证原子性,而volatile变量只能保证可见性。
许多人认为:“volatile变量对所有线程是立即可见的,对volatile变量的修改能立即反映到其他线程中,所以基于volatile变量的运算是线程安全的”。这句话论据部分没错,但其论据并不能得出它的结论。Volatile变量在各个线程中不存在不一致问题(各个线程的工作内存中volatile可以不一致,但由于每次使用都会先刷新,执行引擎看不到不一致的情况),但Java中的运算是非原子性的,导致volatile变量在并发情况下一样是不安全的。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景下,仍然需要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性: