美文网首页Android进阶之路
一篇让你搞懂“volatile的可见性、有序性”

一篇让你搞懂“volatile的可见性、有序性”

作者: 码农的地中海 | 来源:发表于2022-05-18 20:18 被阅读0次

前言:

首先说我们如果要使用 volatile 了,那肯定是在多线程并发的环境下。我们常说的并发场景下有三个重要特性:原子性、可见性、有序性。只有在满足了这三个特性,才能保证并发程序正确执行,否则就会出现各种各样的问题。

并发的三个特性

原子性,上篇文章说到的 CAS 和 Atomic* 类,可以保证简单操作的原子性,对于一些负责的操作,可以使用synchronized 或各种锁来实现。

可见性,指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性,程序执行的顺序按照代码的先后顺序执行,禁止进行指令重排序。看似理所当然的事情,其实并不是这样,指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。但是在多线程环境下,有些代码的顺序改变,有可能引发逻辑上的不正确。

而 volatile 做实现了两个特性,可见性和有序性。所以说在多线程环境中,需要保证这两个特性的功能,可以使用 volatile 关键字。

可见性


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线程使用的值和主线程使用值不一致,导致程序混乱,这就是线程之间的不可见性,这么说你应该能明白了。线程间的不可见性是该程序死循环的根本原因。

[图片上传失败...(image-a87070-1652876272363)]

2 .volatile可见性

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

[图片上传失败...(image-4d7fdf-1652876272363)]

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有相同的可见性语义。

[图片上传失败...(image-8f6e60-1652876272363)]

2.原子性


1. 原子性

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

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++时,就出现了线程安全问题。

[图片上传失败...(image-b4ba71-1652876272363)]

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,控制台输出结果如下图所示。

[图片上传失败...(image-36ae32-1652876272363)]

3.有序性

1. 有序性

什么是有序性?我们写的Java程序代码不总是按顺序执行的,都有可能出现程序重排序(指令重排)的情况,这么做的好处就是为了让执行块的程序代码先执行,执行慢的程序放到后面去,提高整体运行效率。画个简单图后举个实际运用案例代码,大家就学到了。

[图片上传失败...(image-9b3b22-1652876272363)]

如上图所示,任务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编译器优化代码重排序后的结果。

[图片上传失败...(image-82433e-1652876272363)]

2 .volatile有序性

被volatile修饰的共享变量相当于屏障,屏障的作用是不允许指令随意重排的,有序性主要表现在下面三个方面。

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;两行代码也做了重排序。证明了屏障上面的指令是可以重排序的。

[图片上传失败...(image-732c71-1652876272363)]

2.2 屏障下面的指令可以重排序。

[图片上传失败...(image-a88f8e-1652876272363)]

如上图所示将c,d屏障放到普通变量上面,再次执行代码,依然会有x,y同时等于0的情况,证明了屏障下面的指令是可以重排的。

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,该程序会一直陷入死循环,结束不了,证明了屏障上下的代码不可以重排。

2.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的情况。

总结:

原子性等问题,因为这些特性是理解多线程的基础,在我看来基础又特别重要 。

学无止境,对volatile的学习还只是一个基础学习,还有更多的知识等待我们去探索学习;https://shimo.im/docs/ojjlAQDuNi09o55j/ 《Android核心技术进阶手册、实战笔记、面试题纲资料》

相关文章

网友评论

    本文标题:一篇让你搞懂“volatile的可见性、有序性”

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