关于volatile:volatilesynchronized可见性有序性原子性代码证明基础硬核

0.简介


前一篇文章《Synchronized用法原理和锁优化降级过程》从面试角度详细分析了synchronized关键字原理,本篇文章次要围绕volatile关键字用代码剖析下可见性,原子性,有序性,synchronized也辅助证实一下,来加深对锁的了解。

**

1.可见性


1.1 不可见性

A线程操作共享变量后,该共享变量对线程B是不可见的。咱们来看上面的代码。

package com.duyang.thread.basic.volatiletest;
/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 10:10
 * @description:不可见性测试
 * @modified By:
 * 公众号:叫练
 */
public class VolatileTest {

    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            while (flag){
                //留神在这里不能有输入
            };
            System.out.println("threadA over");
        });
        threadA.start();
        //休眠100毫秒,让线程A先执行
        Thread.sleep(100);
        //主线程设置共享变量flag等于false
        flag = false;
    }
}

上述代码中,在主线程中启动了线程A,主线程休眠100毫秒,目标是让线程A先执行,主线程最初设置共享变量flag等于false,控制台没有输入后果,程序死循环没有完结不了。如下图所示主线程执行完后flag = false后Java内存模型(JMM),主线程把本人工作内存的flag值设置成false后同步到主内存,此时主内存flag=false,线程A并没有读取到主内存最新的flag值(false),主线程执行结束,线程A工作内存始终占着cpu工夫片不会从主内存更新最新的flag值,线程A看不到主内存最新值,A线程应用的值和主线程应用值不统一,导致程序凌乱,这就是线程之间的不可见性,这么说你应该能明确了。线程间的不可见性是该程序死循环的根本原因。

1.2 volatile可见性

上述案例中,咱们用代码证实了线程间的共享变量是不可见的,其实你能够从上图得出结论:只有线程A的工作内存可能感知主内存中共享变量flag的值发生变化就好了,这样就能把最新的值更新到A线程的工作内存了,你只有能想到这里,问题就曾经完结了,没错,volatile关键字就实现了这个性能,线程A能感知到主内存共享变量flag产生了变动,于是强制从主内存读取到flag最新值设置到本人工作内存,所以想要VolatileTest代码程序失常完结,用volatile关键字润饰共享变量flag,private volatile static boolean flag = true;就功败垂成。volatile底层实现的硬件根底是基于硬件架构和缓存一致性协定。如果想深刻下,能够翻看上一篇文章可见性是什么?(通俗易懂)》。肯定要试试才会有播种哦!

1.3 synchronized可见性

synchronized是能保障共享变量可见的。每次获取锁都从新从主内存读取最新的共享变量。

package com.duyang.thread.basic.volatiletest;
/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 10:10
 * @description:不可见性测试
 * @modified By:
 * 公众号:叫练
 */
public class VolatileTest {

    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            while (flag){
                synchronized (VolatileTest.class){
                    
                }
            };
            System.out.println("threadA over");
        });
        threadA.start();
        //休眠100毫秒,让线程A先执行
        Thread.sleep(100);
        //主线程设置共享变量flag等于false
        flag = false;
    }
}

上述代码中,我在线程A的while循环中加了一个同步代码块,synchronized (VolatileTest.class)锁的是VolatileTest类的class。最终程序输入”threadA over”,程序完结。能够得出结论:线程A每次加锁前会去读取主内存共享变量flag=false这条最新的数据。由此证明synchronized关键字和volatile有雷同的可见性语义。

2.原子性


2.1 原子性

原子性是指一个操作要么胜利,要么失败,是一个不可分割的整体。

2.2 volatile 非原子性

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 11:22
 * @description:Volatile关键字原子性测试
 * @modified By:
 * 公众号:叫练
 */
public class VolatileAtomicTest {

    private volatile static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread threadA = new Thread(task);
        Thread threadB = new Thread(task);
        threadA.start();
        threadB.start();
        //主线程期待AB执行结束!
        threadA.join();
        threadB.join();
        System.out.println("累加count="+count);
    }

    private static class Task implements Runnable {
        @Override
        public void run() {
            for(int i=0; i<10000; i++) {
                count++;
            }
        }
    }

}

上述代码中,在主线程中启动了线程A,B,每个线程将共享变量count值加10000次,线程AB运行实现之后输入count累加值;下图是控制台输入后果,答案不等于20000,证实了volatile润饰的共享变量并不保障原子性。呈现这个问题的根本原因的count++,这个操作不是原子操作,在JVM中将count++分成3步操作执行。

  • 读取count值。
  • 将count加1。
  • 写入count值到主内存。

当多线程操作count++时,就呈现了线程平安问题。

2.3 synchronized 原子性

咱们用synchronized关键字来革新下面的代码。

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 11:22
 * @description:Volatile关键字原子性测试
 * @modified By:
 * 公众号:叫练
 */
public class VolatileAtomicTest {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread threadA = new Thread(task);
        Thread threadB = new Thread(task);
        threadA.start();
        threadB.start();
        //主线程期待AB执行结束!
        threadA.join();
        threadB.join();
        System.out.println("累加count="+count);
    }

    private static class Task implements Runnable {
        @Override
        public void run() {
            //this锁住的是Task对象实例,也就是task
            synchronized (this) {
                for(int i=0; i<10000; i++) {
                    count++;
                }
            }
        }
    }
}

上述代码中,在线程自增的办法中加了synchronized(this)同步代码块,this锁住的是Task对象实例,也就是task对象;线程A,B执行程序是同步的,所以最终AB线程运行的后果是20000,控制台输入后果如下图所示。

3.有序性


3.1 有序性

什么是有序性?咱们写的Java程序代码不总是按程序执行的,都有可能呈现程序重排序(指令重排)的状况,这么做的益处就是为了让执行块的程序代码先执行,执行慢的程序放到前面去,进步整体运行效率。画个简略图后举个理论使用案例代码,大家就学到了。

如上图所示,工作1耗时长,工作2耗时短,JIT编译程序后,工作2先执行,再执行工作1,对程序最终运行后果没有影响,然而进步了效率啊(工作2先运行完对后果没有影响,但进步了响应速度)!

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 15:09
 * @description:指令重排测试
 * @modified By:
 * 公众号:叫练
 */
public class CodeOrderTest {
    private static int x,y,a,b=0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //初始化4个变量
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 3;
                    x = b;
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 3;
                    y = a;
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("执行次数:"+count);
                break;
            } else {
                System.out.println("执行次数:"+count+","+"x:"+x +" y:"+y);
            }
        }

    }
}

上述代码中,循环启动线程A,B,如果说x,y都等于0时,程序退出。count是程序次数计数器。下图是控制台程序打印局部后果。从图上能够剖析出x,y都等于0时,线程A的a = 3; x = b;两行代码做了重排序,线程B中 b = 3;y = a;两行代码也做了重排序。这就是JIT编译器优化代码重排序后的后果。

3.2 volatile有序性

被volatile润饰的共享变量相当于屏障,屏障的作用是不容许指令随便重排的,有序性次要体现在上面三个方面。

3.2.1 屏障下面的指令能够重排序。

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 15:09
 * @description:指令重排测试
 * @modified By:
 * 公众号:叫练
 */
public class VolatileCodeOrderTest {
    private static int x,y,a,b=0;
    private static volatile int c = 0;
    private static volatile int d = 0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //初始化4个变量
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 3;
                    x = b;
                    c = 4;
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 3;
                    y = a;
                    d = 4;
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("执行次数:"+count);
                break;
            } else {
                System.out.println("执行次数:"+count+","+"x:"+x +" y:"+y);
            }
        }

    }
}

上述代码中,循环启动线程A,B,如果说x,y都等于0时,程序退出。共享变量c,d是volatile润饰,相当于内存屏障,count是程序次数计数器。下图是控制台程序打印局部后果。从图上能够剖析出x,y都等于0时,线程A的a = 3; x = b;两行代码做了重排序,线程B中 b = 3;y = a;两行代码也做了重排序。证实了屏障下面的指令是能够重排序的。

3.2.2 屏障上面的指令能够重排序。


如上图所示将c,d屏障放到一般变量下面,再次执行代码,仍然会有x,y同时等于0的状况,证实了屏障上面的指令是能够重排的。

3.2.3 屏障高低的指令不能够重排序。

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 15:09
 * @description:指令重排测试
 * @modified By:
 * 公众号:叫练
 */
public class VolatileCodeOrderTest {
    private static int x,y,a,b=0;
    private static volatile int c = 0;
    private static volatile int d = 0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //初始化4个变量
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 3;
                    //禁止高低重排
                    c = 4;
                    x = b;
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 3;
                    //禁止高低重排
                    d = 4;
                    y = a;
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("执行次数:"+count);
                break;
            } else {
                System.out.println("执行次数:"+count+","+"x:"+x +" y:"+y);
            }
        }

    }
}

如上述代码,将屏障放在两头,会禁止高低指令重排,x,y变量不可能同时为0,该程序会始终陷入死循环,完结不了,证实了屏障高低的代码不能够重排。

3.3 synchronized有序性

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 15:09
 * @description:指令重排测试
 * @modified By:
 * 公众号:叫练
 */
public class VolatileCodeOrderTest {
    private static int x,y,a,b=0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //初始化4个变量
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (VolatileCodeOrderTest.class) {
                        a = 3;
                        x = b;
                    }
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (VolatileCodeOrderTest.class) {
                        b = 3;
                        y = a;
                    }
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("执行次数:"+count);
                break;
            } else {
                System.out.println("执行次数:"+count+","+"x:"+x +" y:"+y);
            }
        }
    }
}

上述代码中,x,y也不可能同时等于0,synchronized锁的VolatileCodeOrderTest的class对象,线程A,B是同一把锁,代码是同步执行的,是有先后顺序的,所以synchronized也能保障有序性。值得注意的一点是上述代码synchronized不能用synchronized(this),this示意以后线程也就是threadA或threadB,就不是同一把锁了,如果用this测试会呈现x,y同时等于0的状况。

4.程序员学习办法心得


大家能够看到我最近几篇文章剖析多线程花了不少精力都在议论可见性,原子性等问题,因为这些个性是了解多线程的根底,在我看来根底又特地重要,所以怎么重复写我认为都不过分,在这之前有很多老手或者有2到3年工作教训的童鞋常常会问我对于Java的学习办法,我给他们的倡议就是要扎实根底,别上来就学高级的知识点或者框架,比方ReentrantLock源码,springboot框架,就像你玩游戏,一开始你就玩难度级别比拟高的,一旦坡度比拟高你就会比拟好受吃力更别说对着书本了,这就是真正的从入门到放弃的过程。同时在学习的时候别光思考,感觉这个知识点本人会了就过了,这是不够的须要多写代码,多实际,你在这个过程中再去加深本人对常识的了解与记忆,其实有很多常识你看起来是了解了,然而你没有入手去实际,也没有真正了解,这样只看不做的办法我是不举荐的,自己本科毕业后工作7年,始终从事Java一线的研发工作,两头也带过团队,因为本人已经也走过很多弯路踏着坑走过去的,对学习程序还是有肯定的心得体会,我会在今后的日子里继续整顿把一些教训和常识方面的经验分享给大家,心愿大家喜爱关注我。我是叫练,叫个口号就开始练!
总结下来就是两句话:多入手,扎实根底

5.总结


明天给和大家聊了多线程的3个重要的个性,用代码实现的形式具体论述了这些名词的含意,如果认真执行了一遍代码应该能看明确,喜爱的请点赞加关注哦。我是叫练【公众号】,边叫边练。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理