Java之volatile的实现

volatile

  • volatile
  • 指令重排
  • as-if-serial

上一篇说了Java的内存模型,并留下“volatile的可见性就一定是立即可见的吗?synchronize了有为何还需要volatile?”的疑问,这篇文章试着讲讲它们的恩恩怨怨。

volatile

volatile有两层语义,第一层保证不同线程间的可见性;第二层则是禁止指令重排。

针对第一层,我们在Java内存模型一文中已分析,它通过load和store指令,从主存加载变量值或者将修改后的变量值存储进主存,让其他线程可见。从这里我们可以看到在加载和保存的过程中,是会有时间差的,如果此时另外一个线程B来了,B完全可以在最新的变量未保存进主存的时候,去读取到了旧的值。如下面的例子中,跑出来的值很明显是有漏加的情况

public static void main(String[] args) throws InterruptedException {
    VolatileDemo demo = new VolatileDemo();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            demo.run();
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
           demo.run();
        }
    });
    t1.start();
    t2.start();
    Thread.sleep(10000);
}

这也就是为啥volatile既然有保证线程可见性,为啥还需要加锁了,有些人可能觉得这跟普通成员变量有啥子区别,因为就算是普通的成员变量它也可以被其他线程可见,它在不加锁的情况下,并发访问时也存在的线程安全的问题。

确实如此,但volatile可以让这个数据及时同步至主存并每次从主存load进变量值,而普通成员变量则不会了,虽然它也会同步至主存,但对于其他线程来说,每次访问该变量时,是不会load最新值的。

老千层饼之第二层,禁止指令重排,指令重排是CPU的优化机制, 举一个烂大街的例子,拿这句代码来说,`VolatileDemo demo = new VolatileDemo()`它主要包含三层操作:

  1. 在主存分配一块区域
  2. 初始化VolatileDemo
  3. 返回该区域的引用给demo

在不违背as-if-serial语义的情况下,CPU可以将1-2-3的顺序调整为1-3-2。这里会产生一个问题((举一个烂大街的例子):在重排序后,由于VolatileDemo 已经初始化了(半初始),所以在经过图1的操作时,会判断,结果不为null(半初始话状态),然后就返回了,但此时对象还没完全初始化完毕,结果在用demo执行相关的方法时则会NPE。(图中没有将构造方法私有化)

Java之volatile的实现_第1张图片

在阐述重排序带来的问题后,那volatile如何禁止重排序呢?答案是加内存屏障指令。对于写操作而言,前后需要如下指令,表明前面的指令必须先store完,才能执行volatile的写,只有volatile的写store后,下面的指令才能load。

  1. storestoreBarrier
  2. storeloadBarrier

对于volatile的读操作,需要加如下指令,让前面load完指令,才执行load的读操作(这里感觉有点儿鸡肋),load的读操作在store后才能执行后边的指令。
1. loadloadBarrier
3. loadstoreBarrier

上面将的屏障指令是JVM上实现的,在CPU的实现上,它会通过“lock.."全屏障的方式,具体是进行锁总线(CPU和内存交互的通道),这种方式简单粗暴且大多数CPU支持这种方式,通过这种方式,实现了重排序的实现。


指令重排

为何需要指令重排呢,因为CPU在执行多个指令的时候,有个别指令涉及访问内存的时候相当于CPU内部操作是比较费时的,所以对于一些比较费时的指令,可能不会立刻执行,会对后边的指令先指令,充分压榨CPU的效率。

Java之volatile的实现_第2张图片

as-if-serial

中文为看上去就像串行的?尽管CPU出于高效利用的目的,会进行指令重排,但也遵守一个规约,你再怎么重排,也不能改变单线程情况下程序的执行结果。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。如:

int i = 20;
int j = 30;
int result = i * j

上面的例子中,result的结果依赖于i和j所以该行代码不可能重排到一二行代码的前面。

你可能感兴趣的:(JVM深入学习,指令重排,volatile,JVM,as-if-serial)