参考: Java并发编程:volatile关键字解析 正确使用 Volatile 变量
volatile修饰被不同线程访问和修改的变量,共享变量包括所有的实例变量,静态变量和数组元素。volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。
被volatile修饰的共享变量,具有以下两点特性:
1 . 保证了不同线程对该变量操作的内存可见性
2 . 禁止指令重排序
volatile底层实现机制
加入volatile关键字生成的汇编代码,会多出一个lock前缀指令。lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;
即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
1. 保证可见性:
可见性必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
使用volatile时,当线程a修改volatile修饰的变量(这里包括2个操作,修改线程a工作内存中的值,然后将修改后的值写入内存),会使得其他线程的工作内存中的缓存值无效,然后其他线程读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,再去对应的主存读取最新的值。
2. 保证一定的有序性:
指令重排序:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
volatile修饰的变量不允许重排序,volatile的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
3. 不能保证原子性:
(1) 对volatile变量的进行非原子性操作
public class Test { public volatile int inc = 0; // main函数每次运行结果都不一致,都是一个小于10000的数字,问题在于自增操作不是原子性操作,它包括读取变量的原始值、进行加1操作、写入工作内存。 // 可能导致两个线程同时读,假设两个线程读取变量时值为10,分别对变量加1,最终主存中的值为11 public void increase() { inc++; } /* 1. 使用synchronized保证原子性
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized
但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。
public int inc = 0; public synchronized void increase() { inc++; } */ /* 2. 使用Lock保证原子性 public int inc = 0; Lock lock = new ReentrantLock(); public void increase() { lock.lock(); try { inc++; } finally{ lock.unlock(); } } */ /* 3. JDK1.5提供了一些原子操作类,即对基本数据类型的自增、自减、加法,减法等操作进行了封装,保证这些操作是原子性操作。 public AtomicInteger inc = new AtomicInteger(); public void increase() { inc.getAndIncrement(); } */ public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保证前面的线程都执行完 Thread.yield(); System.out.println(test.inc); } }
(2) 非线程安全的数值范围类: 如果初始状态是 (0, 5)
,同一时间内,线程 A 调用 setLower(4)
并且线程 B 调用 setUpper(3),最后得到无效的范围值(4,3)。至于针对范围的其他操作,我们需要使
setLower()
和setUpper()
操作原子化, 而将字段定义为 volatile 类型是无法实现这一目的的。
public class NumberRange { private int lower, upper; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } }
要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中, 如start<end
适用场景
(1)场景:将 volatile 变量作为状态标志使用,状态标志并不依赖于程序内任何其他状态,所以此处非常适合使用 volatile。
volatile boolean closed; public void shutdown() { closed = true; } public void doWork() { while (!closed) { // do stuff } }
(2)场景:将 volatile变量用于一次性安全发布,可以解决双重检查锁定(double-checked-locking)问题
该模式的一个必要条件是:被发布的对象必须是线程安全的,或者是有效的不可变对象(有效不可变意味着对象的状态在发布之后永远不会被修改)。volatile 类型的引用可以确保对象的发布形式的可见性,但是如果对象的状态在发布后将发生更改,那么就需要额外的同步。
public class A { public volatile Apple apple; public void init() { apple = new Apple(); // this is the only write to apple } } public class B { public void doWork() { while (true) { // 使用volatile保证apple不会在构造到一半时,doWork判断apple不为空直接使用,读到一个不完全构造的对象 if (a.apple != null) // use the Apple only if it is ready doSomething(a.apple); } } }
(3)场景:定期发布观察结果
使用volatile以便程序的其他地方可以随时读取最新值。与一次性事件的发布不同,这是一系列独立事件,且发布的值可能随时发生变化。
public class UserManager { public volatile String lastUser; public boolean authenticate(String user, String password) { boolean valid = passwordIsValid(user, password); if (valid) { User u = new User(); activeUsers.add(u); lastUser = user; } return valid; } }
(4)场景:“volatile bean”模式
在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。
@ThreadSafe public class Person { private volatile String firstName; private volatile String lastName;public String getFirstName() { return firstName; } public String getLastName() { return lastName; }public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } }
(5)场景:高级模式 -结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”,获得更高的共享度
与锁相比,Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。
@ThreadSafe
public class Counter {
@GuardedBy("this")
private volatile int value;
public int getValue() { return value; }
public synchronized int increment() { // synchronized
确保增量操作是原子的
return value++;
}
}
常见试题:
1)Java 中能创建 volatile 数组吗?
能,不过是一个指向数组的引用,如果改变引用指向的数组,将会受到 volatile的保护,但是如果多个线程同时改变数组的元素,volatile标示符就不能起到保护作用了。
2)volatile 能使得一个非原子操作变成原子操作吗?
能,一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位的,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量,当你读/写之前Java 内存模型会插入一个读/写屏障,保证变量读写的原子性。
3)volatile 类型变量提供什么保证?
volatile 变量提供有序性和可见性保证,JVM 或者 JIT为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的,但 volatile 类型的 double 和 long 就是原子的。