目录
一、volatile的可见性、有序性、原子性
volatile保证可见性
- 某个共享变量如果被一个线程给修改了,其他线程能够立即知道这个修改操作
- 线程的处理器会一直在总线上嗅探,一旦发现其他处理器对主内中的共享变量进行了修改
- 会将自己的工作空间中的缓存置为失效,强制去内存中重新读取这个共享变量
volatile如何实现可见性?
CPU2对共享变量的值进行了修改以后,CPU1会立即对其工作空间中的变量值进行更新(原有工作空间中的值失效,重新从内存中读取)
volatile保证有序性
- 通过禁止指令重排保证了有序性
- 如果一个变量被声明volatile的话,那么这个变量就不会被进行重排序,也就是说,虚拟机会保证这个变量之前的代码一定会比它先执行,而之后的代码一定会比它晚执行
- 【关键】通过内存屏障(Memory Barrier)来实现禁止指令重排
- 是一种CPU指令,也称为内存栅栏或栅栏指令,是一种屏障指令。通过插入内存屏障,屏障之前发布的操作一定先执行,屏障之后的操作一定后执行,禁止了前后的指令的重排序优化
volatile不能保证原子性
- 即使通过volatile对变量进行声明,但如果对变量执行的是非原子操作,也无法保证线程安全
- 通常使用volatile修饰一个只有原子性读/写操作的变量,类似于++这种复合操作不具有原子性,不适用volatile。因为++的复合性质:先读取;再++;再赋值
二、volatile变量的应用场景
场景一:状态标志
- volatile修饰一个指示性的变量,通过可见性保证实时性(短时延,因为能立马给感知到)
-
指示发生了一个重要的一次性事件,例如完成初始化或请求停机
场景二: 线程安全的双重检查单例模式
volatile通过保证有序性,避免了"无序写入"所有可能带来的问题
class DoubleCheck{
private static volatile DoubleCheck instance;
private DoubleCheck(){
}
public static DoubleCheck getInstance(){
if(instance == null){
/*
线程A,线程B同时进入下面的代码块,线程B进行等待
*/
synchronized (DoubleCheck.class){
if(instance == null){
instance = new DoubleCheck();
}
}
}
return instance;
}
}
如果不用volatile变量修饰instance变量,会带来如下的问题
我们以为的new操作
- 分配一块内存M
- 在内存M上初始化Singleton对象
- 然后将M的地址赋值给instance变量
new 操作通过指令重排序后发生
- 分配一块内存M
- 将M的地址赋值给instance变量,这时instance变量不为空,但是还没有初始化ThreadSafeDoubleCheck对象,从而导致其他线程拿到一个半成品,无法正确使用。
- 最后在内存M上初始化ThreadSafeDoubleCheck对象
- 通过给instance变量加以volatile变量的修饰,从而确保线程的安全。
应用场景三-volatile bean
- 对象是易变数据的持有者,这些对象必须是线程安全的
- 在 volatile bean模式中,JavaBean的所有数据成员都是 volatile类型的
- 比如ConcurrentHashMap之中,当发生hash冲突时,需要使用拉链法来解决hash冲突,为了保证线程安全,需用到volatile变量的修饰。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u; Map.Entry<?,?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
/**
* Virtualized support for map.get(); overridden in subclasses.
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
应用场景四-读-写锁策略(分段锁的具体实现方式)
- 如果读操作远远超过写操作,可以结合锁和 volatile来减少开销
- 使用synchronized加锁机制保证更新动作是原子的,并使用volatile保证读取结果是可见的
- 如果更新不频繁的话,该方法可实现更好的性能,因为volatile 读操作的开销通常要优于—个无竞争的锁获取的开销
class SafeCounter2{
private volatile int count;
public int get(){
return count;
}
//同一时间只有一个线程可以进入add方法
public synchronized void add(){
//这是一个复合操作
this.count ++;
}
}
将其修饰为volatile,在读的时候不进行同步操作,get方法并没有加锁,能保证读取的最新性,在写入方法中add()需要进行同步。
总结
- volatile保证了可见性、有序性,不保证原子性
-
开销一般情况下小于锁的获取,使用无锁的机制完成线程安全任务