美文网首页
Java并发编程系列之volatile关键字

Java并发编程系列之volatile关键字

作者: 野生Java程序员 | 来源:发表于2020-05-12 14:59 被阅读0次

    在讲解Java内存模型之前给大家看一个栗子。

    package com.company;
    
    public class VolatileVisibilityTest {
    
        private static boolean initFlag=false;
    
        public static void main(String[] args) throws InterruptedException {
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("waiting data...");
                    while (!initFlag) {
                    }
    
                    System.out.println("Success");
                }
            }).start();
    
            Thread.sleep(2000, 0);
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    prepareData();
                }
            }).start();
    
        }
    
        public static void prepareData(){
            System.out.println("preparing data...");
            initFlag=true;
            System.out.println("prepare end...");
    
        }
    }
    

    大家可能会认为出现下面的结果:

    waiting data...
    preparing data...
    prepare end...
    Success
    

    结果真的是这样的吗?下面我就启动main方法,演示结果如下

    waiting data...
    preparing data...
    prepare end...
    

    并没有打印出Success,大家可能会奇怪下面的线程已经把initData变量改成了true,上面的线程应该会跳出死循环,打印出Success。

    我们搜索csdn查看什么是Java内存模型的原子操作,给出的解释如下,大家一脸懵逼,这说的啥意思:

    1. read(读取):从主内存读取数据

    2. load(载入):将主内存读取到的数据写入工作内存

    3. use(使用):从工作内存读取数据来计算

    4. assign(赋值):将计算好的值重新赋值到工作内存中

    5. store(存储):将工作内存数据写入主内存

    6. write(写入):将store过去的变量值赋值给主内存中的变量

    7. lock(锁定):将主内存变量加锁,标示为线程独占状态

    8. unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量

    下面我以图形的方式给大家讲一讲Java数据的8种原子操作,大家就会一目了然。

    image.png

    线程二已经将主内存中的initFlag的值改为true,但是线程1还在去自己工作内存中的initFlag=false,所以没有打印出“Success”。

    由于缓存不一致导致出现了不该出现的结果,那么如何解决Jmm缓存不一致问题呢?科学家共同给出了下面两种解决方案

    • 总线加锁(性能太低)

    cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其他cpu没法去读或者写这个数据,知道这个cpu使用完数据释放锁之后其他cpu才能读取该数据。

    总线加锁示意图如下


    image.png

    缺点:将并行的程序变成了串行化,所以现代的计算机不采用这种这种方式

    • MEIS缓存一致性协议

    多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效


    image.png

    那么这个程序该如何修改呢?相信很多同学都知道在initFlag变量之前在volatile关键词修饰,那么volatile关键字有什么作用呢?大家都能答出volatile能够保证变量之间的可见性,那么你们知道volatile关键字底层的工作原理吗?

    Volatile缓存可见性实现原理

    底层实现主要是通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)回会写到主内存

    IA-32架构软件开发者手册对lock指令的解释:

    1. 会将当前处理器的缓存行的数据立即写回到系统内存。

    2. 这个写回内存的操作会引起其他cpu里缓存了该内存地址的数据无效(MESI)

    好了让我们一起看看volatile的源码,按住ctrl+鼠标左键,可是我们并不能看到源码(volatile的很多底层源码是有c语言实现的),别着急下面我教大家如何看volatile的底层源码

    1. 将hsdis-amd64.dll文件放到你的jre的bin目录下:

    D:\java\jdk1.8.0_181\jre\bin

    image.png

    2.在idea上的Edit Configurations...添加参数配置,并将jre配置成D:\java\jdk1.8.0_181\jre

    -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*VolatileVisibilityTest.prepareData
    
    image.png

    3.执行main方法,代码太多,


    image.png

    volatile底层也是加lock锁来实现缓存一致性协议,那么他与总线加锁有什么区别呢?

    volatile只对内存的缓存行进行加锁,然后进行赋值操作。

    并发编程的三大特性:可见性,原子性,有序性

    volatile保证可见性与有序性,但是不保证原子性,保证原子性需要借助synchronized关键字

    如何理解上面这句话的意思呢?下面我再给大家举一个栗子

    package com.company;
    
    public class VolatileAtomicTest {
    
        public static volatile int num=0;
    
        public static void increase(){
            num++;
        }
    
        public static void main(String[] args) throws InterruptedException {
    
            Thread[] threads=new Thread[10];
    
            for (int i = 0; i < threads.length ; i++) {
                threads[i] = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j = 0; j <1000 ; j++) {
                            increase();
                        }
                    }
                });
                threads[i].start();
            }
    
            for(Thread t:threads){
                t.join();
            }
    
            System.out.println(num);
        }
    }
    

    大家猜一猜,上面的程序运行后的结构是多少呢?

    先抢到锁的线程会把没有抢到锁的线程的值失效掉

    说完了可见性、原子性,最后我给大家讲一讲有序性

    package com.company;
    
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    
    public class VolatileSerialTest {
    
        static int x=0,y=0;
    
        public static void main(String[] args) throws InterruptedException {
    
            Set<String> resultSet=new HashSet<>();
    
            Map<String,Integer> resultMap=new HashMap<>();
    
            for (int i = 0; i <10000 ; i++) {
                x=0;y=0;
                resultMap.clear();
                Thread one =new Thread(new Runnable() {
                    @Override
                    public void run() {
                        int a=y;
                        x=1;
                        resultMap.put("a",a);
                    }
                });
    
                Thread other =new Thread(new Runnable() {
                    @Override
                    public void run() {
                        int b=x;
                        y=1;
                        resultMap.put("b",b);
                    }
                });
    
                one.start();
                other.start();
                one.join();
                other.join();
    
                resultSet.add("a="+resultMap.get("a")+","+"b="+resultMap.get("b"));
                System.out.println(resultSet);
            }
        }
    }
    

    大家想一想,上面输出的结果是多少呢?


    image.png

    运行程序,最后会出现四种情况。
    大家思考一下,为什么?

    相关文章

      网友评论

          本文标题:Java并发编程系列之volatile关键字

          本文链接:https://www.haomeiwen.com/subject/avrzectx.html