valatile用来修饰共享变量,是一种弱的同步机制,用来确保将变量的更新操作通知到其他线程。
volatile实现原理
首先看一段代码:
public class SynchronizedExample {
//一个控制变量,作为print方法的结束控制
private boolean asleep = false;
public void print() {
while (!asleep) {
System.out.println("asleep == true");
Thread.yield();
}
}
public void set(){
asleep = true;
}
public static void main(String[] args) {
SynchronizedExample obj = new SynchronizedExample();
//线程调用了实例对象的print方法
new Thread(new Runnable(){ public void run(){obj.print();}}).start();
//线程将实例对象的控制开关关闭
new Thread(new Runnable(){ public void run(){obj.set();}}).start();
}
}
当第二个线程调用set方法后,线程一中的方法按理应该马上结束,但是在并发编程实战上强调这不是一个线程安全的对象,线程二调用set后线程一并不能立即看到修改后的asleep值。
现在问题来了:线程之间是共享成员变量的,为何线程二更改后线程一不能立刻看到修改?我在自己电脑上进行测试,多次运行并没有产生线程不安全的问题。于是我阅读了现代操作系统多处理机的章节,并且在网上找到了一篇很好的博文:聊聊并发(一)——深入分析Volatile的实现原理。
问题在于在多处理机系统中,为了减少总线的压力提高每个CPU的性能,每个CPU都有一个高速缓存(即catche)。当引用一个字时,会将其从内存抽取到catche中。在单处理器系统中,存在的问题是catche和内存的不一致性,但是在多处理器中,多个线程在不同的CPU运行时,成员变量会存在于多个catche中。不一致性就存在于内存和多个catche之间。在硬件设计中已经给出了不一致性的一些解决方案,当某个CPU试图将修改后的字写回到内存当中时,总线硬件将会检测到写并且在总线上发送信号通知其他catche进行修改从而保证一致性。但是问题在于CPU修改catche之后何时将其写回到内存中。如果修改后立刻写回到内存当中,那么不会产生一致性问题。但是硬件设计当中通常是当这个字需要调出catche时才写回到内存中。
所以volatile的作用就是强制要求当catche中的成员变量发生更改时,立即写回到内存中。
和synchronized的区别
- volatile不需要加锁,因此也不会发生阻塞,因此效率要高一点。
- volatile只能保证可见性不能保证原子性。
比如 idNum = count++;(count是volatile修饰的)这个操作依然是线程不安全的。因为当用加锁机制时,读取和自增的操作是一个原子操作,在自增完成前,其他线程不允许读取count的值,但是使用volatile时,一个线程刚刚完成自增的操作同时有多个其他的线程正在读取阶段,虽然这些线程能看到最后更改后的值,但是这些线程读取到的count值都是相同的。即volatile允许同时读但是加锁不允许同时读。
总结
在并发编程实战中给出了volatile的适用范围:
- 对变量的写入操作不依赖于变量的当前值(id = count++违反了这个原则),除非可以确保只有单个线程更新变量的值(例如用作某个操作完成,发生中断,或者状态的标志)。
- 该变量不会和其他状态变量一起纳入不变性条件(原子性)
- 在访问变量时不需要加锁(允许同时读)