当前位置 博文首页 > Ocean曈的博客:volatile标识字段会造成:当前任务线程的内存副

    Ocean曈的博客:volatile标识字段会造成:当前任务线程的内存副

    作者:[db:作者] 时间:2021-06-21 12:40

    首先看代码:

    //实验一
    public class Test1 {
        public volatile   int i = 0;
        public  int flag = 1;
    
        public static void main(String[] args) throws InterruptedException {
            Test1 test1 = new Test1();
            Thread a = new Thread(()->{
                try {
                    Thread.sleep(1000);
                    test1.flag = 0;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            });
            Thread b = new Thread(()->{
                while (test1.flag!=0){
                    test1.i++;
                }
            });
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.println("end ,i="+test1.i);
        }
    }
    
    

    实验一打印输出:

    end ,i=165243757
    
    Process finished with exit code 0
    

    然后去除掉int i 前面的volatile 字段

    //实验二
    public class Test1 {
        public int i = 0;
        public  int flag = 1;
    
        public static void main(String[] args) throws InterruptedException {
            Test1 test1 = new Test1();
            Thread a = new Thread(()->{
                try {
                    Thread.sleep(1000);
                    test1.flag = 0;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            });
            Thread b = new Thread(()->{
                while (test1.flag!=0){
                    test1.i++;
                }
            });
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.println("end ,i="+test1.i);
        }
    }
    

    线程b 直接死循环,没有输出了。

    我们都知道用volatile 标记的字段会有线程间可见性。如果没有标记这个字段的值,在两个线程之间是不可见的。 如果 volatile 标记在 flag前面。保证了线程a跟线程b之间flag 的可见性。实验一,可以正常输出我们能理解。但是,为什么标记在变量i前面,也能带来flag 的线程之间的内存可见呢?

    个人猜测是不是JAVA 的内存模型JMM,在模仿cpu 的缓存行的机制带来的,让同一个缓存行一起刷新。因为volatile int i 跟 flag 在同一个缓存行,所以在刷新i 到主存 跟 b线程工作内存的时候,也把flag 刷进去了呢?
    带着这个疑问我做了一下实验?

    //实验三
    public class Test1 {
        public volatile  int i = 0;
        long a1,a2,a3,a4,a5,a6,a7,a8,a9=0;
        public  int flag = 1;
    
        public static void main(String[] args) throws InterruptedException {
            Test1 test1 = new Test1();
            Thread a = new Thread(()->{
                try {
                    Thread.sleep(1000);
                    test1.flag = 0;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
            });
            Thread b = new Thread(()->{
                while (test1.flag!=0){
                    test1.i++;
                }
            });
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.println("end ,i="+test1.i);
        }
    }
    

    实验三添加了9个long 类型的数据,超过了缓存的行的大小,按猜测锁想实验三应该会有b线程死循环,造成没有输出。然而事实并非如此。
    实验室三输出:

    end ,i=163084240
    
    Process finished with exit code 0
    

    继续猜测,难道是JMM模型的缓存行超过了64个字节了?那我们继续加大

    //实验四
    public class Test1 {
        public volatile  int i = 0;
        long[] a1 = new long[10000000];
        public  int flag = 1;
    
        public static void main(String[] args) throws InterruptedException {
            Test1 test1 = new Test1();
            Thread a = new Thread(()->{
                try {
                    Thread.sleep(100);
                    test1.flag = 0;
                    for (int i = 0; i < 10000000; i++) {
                        test1.a1[i] = i;
                    }
    
                } catch (Exception e) {
                    e.printStackTrace();
                }
    
            });
            Thread b = new Thread(()->{
                while (test1.flag!=0){
                    test1.i++;
                }
            });
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.println("end ,i="+test1.i);
        }
    }
    

    然而,实验结果依然是能获得输出

    end ,i=15935534
    
    Process finished with exit code 0
    

    通过上述的实验我们得出,volatile字段加上之后 JMM 模型中各个线程模型并没有像cpu的内存模型那样有一个缓存行的概念。但是我们的疑问仍然存在,为什么volatile i 字段会影响相同 同一个类里面的没有被volatile标记的flag呢??

    继续猜想,是不是只要JMM线程里面加载了一个volatile字段,会把整块工作线程的所有内存都保证了可见性?由此设计了实验如下

    //实验五:
    public class Test1 {
        public  int i = 0;
        public  int flag = 1;
    public static void main(String[] args) throws InterruptedException {
            Test1 test1 = new Test1();
            Test3 test3 = new Test3();
    
            Thread a = new Thread(()->{
                try {
                    Thread.sleep(100);
                    test1.flag = 0;
                } catch (Exception e) {
                    e.printStackTrace();
                }
    
            });
            Thread b = new Thread(()->{
                while (test1.flag!=0){
                    test3.i++;
                }
            });
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.println("end ,i="+test3.i);
        }
    }
    
    public class Test3 {
        public volatile  int i = 0;
    }
    
    
    end ,i=16666048
    
    Process finished with exit code 0
    

    实验竟然也是有结果的 ,我们用了class Test1 跟 Class Test2 ,被volatile标记的只有test2 中的 i,但是神奇的是 a线程中 test1.flag 的变量改变也通知到了b线程中。

    由此我们可以得出一个简单的结论,当线程中有volatile变量加载的时候,JAVA任务线程工作内存副本,会从主存中刷新当前任务线程中曾经加载过的所有内存数据,来更新工作线程的副本。如果线程没有加载过任何volatile相关变量,那么当前线程的工作内存副本中就不会去跟主存通信,及时的从主内存中刷新数据。

    当然这些实验也有一些问题,我从网上也没有搜到相关的一些资料。如果有问题,欢迎留言,讨论,共同进步。

    ps:后来同事老王,看到了我的这篇文章,继续深入研究。以及查阅到了一些相关的资料。总结了如下刷新时机。

    1.当线程执行到有加锁解锁变量时会去加载主存中的变量

    2.当线程从运行状态,切换到阻塞或者等待状态,再切回运行状态。此时也会把主存中的新变量加入到线程内存中。

    3.volititle 的 带来的内存可见性,并不是只是volititle标记的那个变量,而是会导致线程副本中的所有变量都会更新。

    而此实验的种添加volitile的情况是属于第一种情况。volitile中的在字节码层是通过一个ACC_VOLITILE字段来标记,在jvm里面的c++代码里面是通过调用了汇编指令lock 指令来完成的。这个操作就是平常我所说一般情况下对某个对象上锁最基本原理。

    这个总结性的结果,也相对的印证了使用了volitile 指令会让线程的工作副本内存中的其他内存也会变得存在可见性。即,每次调用过volitile指令后,会刷新一次主内存到工作线程内存中去。

    验证以上所说,我们接下来继续试验:

    试验六:
    public class Test1 {
        public  int i = 0;
        public  int flag = 1;
    
        public static void main(String[] args) throws InterruptedException {
            Test1 test1 = new Test1();
            Test3 test3 = new Test3();
    
            Thread a = new Thread(()->{
                try {
                    Thread.sleep(1000);
                    test1.flag = 0;
                } catch (Exception e) {
                    e.printStackTrace();
                }
    
            });
            Thread b = new Thread(()->{
                while (test1.flag!=0){
                    test3.i++;
                    if (test3.i%1000==0){
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.println("end ,i="+test3.i);
        }
    }
    

    试验六结果,是有输出的,输出如下:

    end ,i=99000
    
    Process finished with exit code 0
    

    我们看到试验6,对b线程执行了睡眠操作,让线程挂起。这就造成了线程不停的睡眠-》运行,运行-》睡眠两种状态的切换。达到条件一的结果,可以有最终结果。

    试验七:
    public class Test1 {
        public  int i = 0;
        public  int flag = 1;
    
        public static void main(String[] args) throws InterruptedException {
            Test1 test1 = new Test1();
    
            Thread a = new Thread(()->{
                try {
                    Thread.sleep(1000);
                    test1.flag = 0;
                } catch (Exception e) {
                    e.printStackTrace();
                }
    
            });
            Thread b = new Thread(()->{
                while (test1.flag!=0){
                    synchronized (test1){
                        test1.i++;
                    }
                }
            });
            a.start();
            b.start();
            a.join();
            b.join();
            System.out.println("end ,i="+test1.i);
        }
    }
    

    试验七的结果如下,

    end ,i=61465138
    
    Process finished with exit code 0
    

    试验7表明,线程每次碰到上锁逻辑都会从主存中的拉取最新的数据进入执行线程的副本内存中。可以通过关键字 synchronized 、volitile 或者UnSafe中的cas操作,都能来达到线程间可见的效果。

    最终总结: java中工作线程副本刷新的时机有两种:

    1.工作线程副本内存及时从主内存中刷新数据,可以通过执行带有lock指令的命令( synchronized 、volitile、cas)。

    2.当线程发生了状态的切换。

    3.volititle 的 带来的内存可见性,并不是只是volititle标记的那个变量,而是会导致线程副本中的所有变量都会更新。

    下一篇:没有了