1、结论
volatile具有可见性
和防止指令重排
的能力,但是在某些场景下不能保证线程安全(无法替代synchronized关键字)
2、原因简析
1、线程安全问题中有三个概念:原子性(Atomicity)、可见性(Visibility)、有序性(Ordering)。
2、synchronized关键字可以保证原子性、可见性和有序性;volatile只能保证可见性和有序性(有序性体现在 防止指令重排 上)。
3、使用volatile修饰的(多线程共享的)变量进行的是原子的修改操作时,这时volatile可以保证线程安全;除此之外,单一地使用volatile不保证线程安全。
4、volatile会对总线(主存)加上LOCK前缀指令(观察汇编源码得知),LOCK不是内存屏障,但是完成的事情是类似内存屏障(也叫内存栅栏)的功能。LOCK可以理解成是CPU一级的锁,加上LOCK后,其他CPU对该内存地址的原子的读写请求
都会被阻塞,直到锁释放。(《码出高效Java开发手册》P232中描述使用了volatile后“...任何对此变量的操作都会在内存中进行,不会产生副本”,笔者认为描述有问题)
5、单一地使用synchronized(来保证线程安全)会有一定的效能损耗,可以用volatile搭配使用synchronized减少(因为要保证线程安全带来的)效能损耗,也可以搭配CAS(比如自旋锁运用了CAS -- Compare-And-Swap)。
3、背景
计算机在对内存进行操作时,会存在主内存(有些地方叫物理内存)和高速缓存的概念。主存中的变量值对所有线程可见,高速缓存是线程私有的--对其他线程不可见的。CPU对内存进行操作的时候,单个线程会从主存(总线)中读取目标内存地址中的数据,copy到高速缓存(作为副本),后续的一系列操作都是基于这个“副本”,操作完后,将副本的值同步回主存。
内存栅栏实现了 可见性 和 防止指令重排 的效果
内存栅栏/内存屏障
4、代码验证
4.1、这是一段线程不安全的代码
public class Test {
public static volatile int inc = 0;
public static void increase() throws InterruptedException {
inc++;
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 5; i++){
try {
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----A过程---" + Test.inc);
}
}
});
Thread thread2 = new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 5; i++){
try {
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----B过程---" + Test.inc);
}
}
});
thread1.start();
thread2.start();
Thread.sleep(2000);
System.out.println("--终态--" + inc);
}
}
4.2、对#4.1代码的优化
·#4.1的代码不能复现出问题,猜测可能是机器的CPU性能较好。所以优化了下代码,如下
public class Test {
public static volatile int inc = 0;
public static void increase() throws InterruptedException {
Thread.sleep(1);
inc ++;
}
public static void main(String[] args) throws InterruptedException {
for (int k=0;k<10;k++){
new Thread(new Runnable(){
@Override
public void run() {
for (int i=0 ; i < 500; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2);
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----A过程---" + inc);
}
}).start();
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
for (int i=0 ; i < 500; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2);
increase();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----B过程---" + inc);
}
}).start();
}
}
}).start();
}
Thread.sleep(5000);
System.out.println("--终态--" + inc);
}
}
4.3、#4.2的运行结果
预期结果是10,000,实际运行结果<10,0004.4、原因
因为#4.1和#4.2的模型一样,#4.1的逻辑更简单,故以#4.1为例讲
4.4.1、首先,问题出在这一行
inc++
4.4.2、其次,inc++非原子操作
inc++ 即 inc = inc + 1
4.4.3、出现异常(结果不合预期)的情况
step-1 step-2 step-3 step-4 step-5
4.5、反思
从结果看来,在这个场景中,volatile没有发挥任何作用嘛?
我们去掉#4.2代码中的volatile关键字,发现结果也是少于10,000
没有volatile修饰inc变量的情况
我认为,volatile还是发挥作用的(只是没有它没有让结果达到预期),举个例子
去掉volatile后,step-4的线程B不是无效掉前两步的操作,而是将自己的副本(inc=2)更新到主存中,这时主存中的inc值又被更新了一次(2 -> 2);
假设在线程竞争中,线程B获得的CPU时间片轮远少于线程A时,当线程A对inc更新过好几轮了后(假设此时主存中的inc=4),线程B仍然对主存更新为2。这时主存中的inc值经历了几个阶段
主存中inc的几个阶段
4.6、比较明确地体现volatile的可见性作用的例子
1、状态标记量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
2、双重检测锁
class Singleton{
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
5、volatile适用的场景
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
针对这两点约束,个人还不是很理解,具体参考# volatile的适用场景
6、参考来源
1、 Java并发编程:volatile关键字解析
2、 volatile 和 内存屏障
3、《码出高效Java开发手册》
网友评论