15

volatile原理技术知识整理 - 简书

 4 years ago
source link: https://www.jianshu.com/p/198373edc146?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
0.6922020.01.10 06:12:04字数 1,736阅读 15,579
webp

volatile是一种轻量且在有限的条件下线程安全技术,它保证修饰的变量的可见性和有序性,但非原子性。相对于synchronize高效,而常常跟synchronize配合使用。

webp
volatile原理.png

一. Java内存模型

这里主要描述的线程,工作内存,主存的变量的读写关系:

  1. 主存存放线程需要操作的变量,但线程并不直接操作主存。
  2. 每个线程读取主存变量都是先拷贝一份到工作内存中,不同线程工作内存互不干扰。
  3. 线程修改了工作内存后,再写回主存中。
  4. 每次从主存读写的过程都需要经过8原子性操作。
webp
java内存模型.png

二. volatile可见性

1. volatile特殊性

(1) 操作use之前必须先执行read和load操作。
(2) 操作assign之后必须执行store和write操作。

由特性性保证了read、load和use的操作连续性,assign、store和write的操作连续性,从而达到工作内存读取前必须刷新主存最新值;工作内存写入后必须同步到主存中。读取的连续性和写入的连续性,看上去像线程直接操作了主存

扩展:
lock和unlock操作并不直接开放给用户使用,而是提供给像Synchronize关键字指定monitorenter和monitorexit隐式使用。关于Synchronize的监听器锁monitor,javac编译后会在作用的方法前后增加monitorenter和monitorexit指令,详细的可以查看Synchronize原理。

2. 代码验证可见性
public class VolatileVisibility {

public static class TestData {
    volatile int num = 0;
    public void updateNum(){
        num = 1;
    }
}

public static void main(String[] args) {
    final TestData testData = new TestData();
    new Thread(new Runnable() {
        
        @Override
        public void run() {
            System.out.println("ChildThread num-->"+testData.num);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            testData.updateNum();
            System.out.println("ChildThread update num-->"+testData.num);
        }
    }).start();
    
    while (testData.num == 0){
     }
    
    System.out.println("MainThread num-->"+testData.num);
  }
}

(1) TestData中的num不添加volatile关键字,System.out.println("MainThread num-->"+testData.num);这一句一直不会执行。表示while中的条件testData.num == 0一直为0,子线程修改了num对主线程不起作用。

(2) TestData中的num添加volatile关键字,System.out.println("MainThread num-->"+testData.num);会执行,结果如下。

ChildThread num-->0
ChildThread update num-->1
MainThread num-->1

三. volatile非原子性

1. use和assign这两个操作整体上不是一个连续性的原子操作。

volatile本身并不对数据运算处理维持原子性,强调的是读写及时影响主存。

2. 非原子性操作

volatile修饰num,num++;num = num+1;这种就是非原子性操作。

(1) 主存读取num的值;
(2) 进行num++运算;
(3) 将num值写到主存。

像种操作在多线程环境中,use和assign是多次出现,如果有两个线程中读取到主存的num都是2,且同时执行num++,两个线程的结果都是3,这样就产生了脏数据,再写入主存中都是3。核心num++运算并没保证先后顺序执行。为了保证执行运算的线程顺序,可以选择Synchronize。

3. 代码验证非原子性
public class ValatileAtomic {
public static class TestData {
    volatile int num = 0;
      //synchronized
    public void updateNum(){
        num++;
    }
}

public static void main(String[] args) {
    final TestData testData = new TestData();
    for(int i = 1; i <= 10; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 1; j <= 1000; j++) {
                    testData.updateNum();
                }
            }
        }).start();
    }
    
    while (Thread.activeCount() > 2) {
        Thread.yield();
    }
    System.out.println("最终结果:" + testData.num);
  }
}

按我们的意愿10个线程,每个线程累加线程累加1000,一共是10 * 1000=10000。但是volatile int num = 0; 使用volatile与否都是体现非原子性,运行的结果都比10000小:

最终结果:9701

为了实现同步操作,在方法updateNum()前添加关键字synchronize即可:

最终结果:10000

四. volatile有序性

1. volatile禁止指令重排

(1) 指令重排:为了提高性能,编译器和和处理器通常会对指令进行指令重排序。

指令重排.png

图中的三个重排位置可以调换的,根据系统优化需要进行重排。遵循的原则是单线程重排后的执行结果要与顺序执行结果相同。

(2) 内存屏障指令:volatile在指令之间插入内存屏障,保证按照特定顺序执行和某些变量的可见性。

volatile就是通过内存屏障通知cpu和编译器不做指令重排优化来维持有序性。

2. synchronize串行控制

(1) synchronize无禁止指令重排。
(2) 一个变量在同一时刻只允许一条线程对其进行lock操作,获取对象锁,互斥排他性达到两个同步块串行执行。

五. volatile线程安全适用范围

由于volatile的非原子性原因,所以它的线程安全是有条件的:

(1) 运算结果不依赖但前置,或者能保证自由一个单一线程修改变量值。
(2) 变量不需要与其他的状态变量共同参与不变的约束。
这两条件描述出自于《深入理解java虚拟机》。

六. volatile与synchronize配合使用

1. DCL单例代码
public class Singleton {

private volatile static Singleton instance = null;
private Singleton(){
    
}

public static Singleton getInstance(){
    if(instance == null){ // 第①处
        synchronized (Singleton.class) {
            if(instance == null){  // 第②处
                instance = new Singleton();
            }
        }
    }
    return instance;
  }
}
2.为什么还要使用volatile来修饰

按照上边的写法已经对new Singleton();这个操作进行了synchronize操作,已经保证了多线程只能串行执行这个实例化代码。事实上,synchronize保证了线程执行实例化这段代码是串行的,但是Synchronize并不具备禁止指令重排的特性。

instance = new Singleton(); 主要做了3件事情:
(1) java虚拟机为对象分配一块内存x。
(2) 在内存x上为对象进行初始化 。
(3) 将内存x的地址赋值给instance 变量。

如果编译器进行重排为:
(1) java虚拟机为对象分配一块内存x。
(2) 将内存x的地址赋值给instance 变量。
(3) 在内存x上为对象进行初始化 。

第一种情况,无volatile修饰:此时,有两个线程执行getInstance()方法,加入线程A进入代码的注释中的第②处,并执行到了重排指令的(2),与其同时线程B刚好代码注释中的第①处的if判断。此时,instance有线程A把内存地址x地址赋值给了instance,那么instance已经不为空只是没有初始化完成,线程B就返回了一个没有完成初始化的instance,最终使用时候会处现空指针的错误。

第二种情况,有volatile修饰:instance因为被volatile的禁止指令重排的特性,那只会安装先初始化对象再赋值给instance这样顺序执行,这样就能保证返回正常的实例化的对象。

  1. volatile具有可见性和有序性,不能保证原子性。
  2. volatile在特定情况下线程安全,比如自身不做非原子性运算。
  3. synchronize通过获取对象锁,保证代码块串行执行,无禁止指令重排能力。
  4. DCL单例操作需要volatile和synchronize保证线程安全。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK