Volatile 关键字浅析解析

1. volatile的定义
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致性地更新,线程应该确保通过排他锁单独获取这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile关键字,Java线程内存模型确保所有线程看到这个变量值的一致性。
2.volatile的实现原则
1)Lock前缀指令会引起处理器缓存写回内存。Lock前缀指令导致在执行指令期间,声言处理器的Lock#信号。在多核处理器环境中,Lock#信号确保在声言该信号期间,处理器可以独占任何共享内存。
2)一个处理器的缓存写会内存会导致其他处理器的缓存失效。(根据MESI协议
3.volatile的自身特性(自身角度分析特性)
理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。如下两个代码示例:
volatile关键字代码:

public class VolatileFeaturesExample {

    volatile long v1 = 0L;

    public void set (long v2) {
        this.v1 = v2;
    }

    public long get () {
        return v1;
    }

    public void getAndIncrement() {
        v1++;
    }

    public static void main(String[] args) {
        VolatileFeaturesExample v = new VolatileFeaturesExample();
        /*new Thread(new ThreadSet(v)).start();
        new Thread(new ThreadGet(v)).start();*/
        final CountDownLatch countDownLatch = new CountDownLatch(5000);
        for (int i = 0;i < 5000;i ++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    v.getAndIncrement();
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
            System.out.println(v.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class ThreadGet implements Runnable {
        private VolatileFeaturesExample v;

        public ThreadGet (VolatileFeaturesExample v) {
            this.v = v;
        }

        public void run() {
            long local_v1 = 0L;
            while (local_v1 <10) {
                if (local_v1 != v.get()) {
                    System.out.println("ThreadGet--------------"+v.get());
                    local_v1 = v.get();
                }
            }

        }

    }

    static class ThreadSet implements Runnable {

        private VolatileFeaturesExample v;

        public ThreadSet (VolatileFeaturesExample v) {
            this.v = v;
        }

        public void run() {
            long local_v1 = 0L;
            while (local_v1 < 10) {
                System.out.println("ThreadSet----------"+(++local_v1));
                v.set(local_v1);
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

把volatile改为锁synchronized。(这里只贴了方法代码,其它和上面一样)

long v1 = 0L;

    public synchronized void set (long v2) {
        this.v1 = v2;
    }

    public synchronized long get () {
        return v1;
    }

代码分析:
get和set方法执行结果
(1)加了volatile关键字的执行结果

ThreadSet----------1
ThreadGet--------------1
ThreadSet----------2
ThreadGet--------------2
ThreadSet----------3
ThreadGet--------------3
ThreadSet----------4
ThreadGet--------------4
ThreadSet----------5
ThreadGet--------------5
ThreadSet----------6
ThreadGet--------------6
ThreadSet----------7
ThreadGet--------------7
ThreadSet----------8
ThreadGet--------------8
ThreadSet----------9
ThreadGet--------------9
ThreadSet----------10
ThreadGet--------------10

(2)去掉volatile执行结果

ThreadSet----------1
ThreadGet--------------1
ThreadSet----------2
ThreadSet----------3
ThreadSet----------4
ThreadSet----------5
ThreadSet----------6
ThreadSet----------7
ThreadSet----------8
ThreadSet----------9
ThreadSet----------10

(3)去掉volatile关键字,换成锁synchronized的执行结果

ThreadSet----------1
ThreadGet--------------1
ThreadSet----------2
ThreadGet--------------2
ThreadSet----------3
ThreadGet--------------3
ThreadSet----------4
ThreadGet--------------4
ThreadSet----------5
ThreadGet--------------5
ThreadSet----------6
ThreadGet--------------6
ThreadSet----------7
ThreadGet--------------7
ThreadSet----------8
ThreadGet--------------8
ThreadSet----------9
ThreadGet--------------9
ThreadSet----------10
ThreadGet--------------10

getAndIncrement执行结果
(4)使用带有volatile关键字的v1,调用getAndIncrement累加到5000

第一次:5000
第二次:500
第三次:4999
第四次:5000
第五次:4999

如上面示例代码所示,一个volatile变量的读/写操作,与一个普通变量的读/写使用锁同步,它们之间的执行结果不同。不使用volatile关键字,发现一个线程改线,另一个线程可能都不可见。
锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对于一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
锁的语义决定了临界区代码执行具有原子性。这意味,即使64位的long型和double变量,只要它是volatile变量,对该该变量的读/写就具有原子性。根据代码getAndIncrement方法结果得知,对于volatile++这种复合操作不就有原子性,这些操作本身不具有原子性。
特性
(1)可见性。对于一个volatile变量的读,总是能看到(任意线程)对这个volatile变量的最后写入。
(2)原子性。对任意单个volatile变量的读/写具有原子性,但类似volatile++这种操作不具有原子性。
4.volatile特性的影响性(从不是volatile变量的角度分析,volatile给它们带来的内存可见性影响)
1)volatile 写/读建立的happens-before关系
其实volatile保证了可见性,其实就是完成了线程之间的通信。
我们来分析下如下代码的happens-before关系

int num = 0;
    volatile boolean flag = false;

    public void write (int i) {
        num = i;  // 1
        flag = true;// 2
    }

    public  int read () {
        if (flag) { // 3
            int i = num; // 4
            return i;
        }
        return num;
    }

(1)根据程序次序规则,1happens-before2;3happens-before4;
(2)根据volatile规则,2happens-before3;
(3)根据happens-before的传递规则,1happens-before4;
我们发现一个问题volatile影响了普通的字段,可以理解为write的普通num写对read的普通读num可见了,根据1happens-before来判断。
代码测试,测试volatile对普通变量的影响:
影响特性
(1)任何变量的写在volatile变量写之前,那么这个变量在volatile变量读之后是可见的.(具体解释在实现原理中)
5.volatile内存语义实现
1)volatile重排序规则表
(1)当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile之后。
(2)当第一个是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile之前。
(3)当第一个操作是volatile写,第二个操作volatile时,不能进行重排序。
2)限制重排序的规则(内存屏障)
(1)在每个volatile写操作的前面插入一个StoreStore屏障。
(2)在每个volatile写操作的后面插入一个StoreLoad屏障。
(3)在每个volatile读操作的后面插入一个LoadLoad屏障。
(4)在每个volatile读操作的后面插入一个LoadStore屏障。
上面的内存屏障都非常保守,但它可以保证任意处理器平台,任意程序中都能得到正确的volatile语义。
3)代码示例分析
(1)volatile写插入的内存屏障
内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
实现写语义的内存屏障:StoreStore和StoreLoad。如下图执行指令执行顺序
Volatile 关键字浅析解析
StoreStore屏障可以保证在volatile写之前,前面所有的普通写操作已经对任何处理器可见了。这是因为StoreStore屏障将保证上面所有的普通写在volatile写之前刷新到主内存。
这里比较有意思的是,volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。因为编译器常无法准确判断一个volatile写的后面是否需要插入一个StoreLoad屏障。为了保证能正确实现volatile的内存语义,JMM实现了保守策略:在每个volatile写的后面或者每个volatile读前面插入一个StoreLoad屏障。从整体的执行效率角度考虑,JMM最终选择了在在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见模式是:一个线程写volatile变量,读个线程读取volatile读取同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。
(2)volatile读插入的内存屏障
Volatile 关键字浅析解析

LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

猜你喜欢

转载自blog.51cto.com/14220760/2376454