可见性
当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。但如果没有同步,可见性就无法实现。
下面的代码说明了当多个线程在没有同步的情况下共享数据时出现的错误。在代码中,主线程和读线程都会访问共享变量ready和number。很显然代码看起来会输出42,但事实上很可能输出0(重排序导致ready=true在number=42之前执行),甚至读线程无法结束(无法保证主线程写入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; }
}
加锁和可见性
当线程B执行由锁保护起来的代码时,可以看到线程A之前在同一个同步代码块中所有的操作结果。如果没有同步,那么就无法实现上述保证。
现在可以理解为什么访问某个共享的且可变的变量时要求所有线程在同一个锁上同步,就是为了保证某个线程对变量的修改对其他线程来讲都是可见的。否则一个线程在未持有正确的锁的情况下读取某个变量,那么读到的可能是一个失效值。也就是加锁的含义不仅仅是保持互斥(原子性),同时也可以实现内存可见性。
Volatile变量
volatile只能实现内存可见性,不能保证原子性。
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中的原子类)来保证原子性:
- 运算结果并不依赖变量的当前值,或者能保证只有单一线程修改变量的值;
- 变量不需要与其他状态变量共同参与不变约束。