安卓

Android高级进阶-Java多线程编程之volatile关键

2021-07-15  本文已影响0人  肖义熙

锁在多线程编程或者说并发编程中极为重要,善用锁有助于避免程序出现意想不到的错误。volatile也可以说是锁机制中的一部分吧,之后会陆续学习分享锁机制的内容。

volatile关键字

volatile关键字用于 保持内存可见性防止指令重排序,什么意思呢?

public class JMMTest {

    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {

        new Thread(){
            @Override
            public void run() {
                System.out.println("11111");
                while (!flag){
                }
                System.out.println("22222");
            }
        }.start();

        Thread.sleep(100);

        new Thread(){
            @Override
            public void run() {
                System.out.println("33333");
                flag = true;
                System.out.println("44444");
            }
        }.start();
    }

}

以上代码,开启两个线程,中间睡眠100ms。其最终结果是:


image.png

为什么呢?明明第二个线程修改了flag变量的值为true,那第一个线程中while(!flag)应该不会进入循环才对,应该最终会打印22222才对。其实这里就是因为两个线程内存不可见性导致,两个线程中的flag都是变量flag变量的一个副本,第二个线程修改flag=true并不影响第一个线程中的flag。其实在IDEA中已经有所提醒了:


image.png
那我们在申明flag变量的地方加上volatile关键字对flag变量进行修饰后再执行结果:
public static volatile boolean flag = false;
image.png

⚠️ 注意: ⚠️ 这个时候,我将上面例子换一个顺序再执行,结果又不尽相同:

public class JMMTest {

    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {

        new Thread(){
            @Override
            public void run() {
                System.out.println("33333");
                flag = true;
                System.out.println("44444");
            }
        }.start();

        Thread.sleep(100);

        new Thread(){
            @Override
            public void run() {
                System.out.println("11111");
                while (!flag){
                }
                System.out.println("22222");
            }
        }.start();

    }

}
这个时候到执行结果如下: image.png

这里就需要抛出一个疑问,不是说两个线程中的变量都是副本么?这里第一个线程改了flag值为true应该和第二个线程没有关系的啊????
解释一下:大家都知道代码执行顺序,当执行到flag=true时,某个时段会将flag刷新回主存中,意味着第二个线程开始执行之前,flag值已经被第一个线程修改并且将值刷新回到了主存中,主存中的flag值变为true,第二个线程执行时拷贝的变量副本就已经是true了。如何验证呢?我们再来修改一下代码:

public static void main(String[] args) throws InterruptedException {

        new Thread(){
            @Override
            public void run() {
                System.out.println("33333");
                flag = true;
                System.out.println("44444");
                while (flag){
                    //此时flag是true,虽然第二个线程1000毫秒后将值重新改回false并刷新回主存,
                    //但是这里的flag在刷新前已经将主存中flag拷贝到了线程工作内存中了,后面的代码将不再执行
                }
                System.out.println("55555");

            }
        }.start();
        Thread.sleep(100);
        new Thread(){
            @Override
            public void run() {
                System.out.println("11111");
                while (!flag){
                }
                System.out.println("22222");

                try {
                    sleep(1000);
                    flag = false;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }.start();

    }

运行后的结果可以预知:不会打印55555:

image.png
volatile 的内存可见性就说这么多了,这里再放一张图,线程工作内存和主存之间的关系:(偷来的图,反正我画的贼难看...) image.png
public class ObjTest {

    public static void main(String[] args) {
        Obj obj = new Obj();
    }
}

class Obj{
    int i = 10;
}

转换字节码后的指令四个过程,过程如:


image.png

在CPU执行指令过程中,第三步和第四步的执行指令顺序可能不一样,在单线程下,第四步指令先执行,后执行第三步指令的情形下,对结果并没有影响,但是在多线程下就可能出现问题。

光说不做不是一枚老程序员的做法,我们验证一下指令重排的效果:


public class VolatileTest {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; ; i++) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread threadOne = new Thread() {
                @Override
                public void run() {
                    a = 1;
                    x = b;
                }
            };
            Thread threadTwo = new Thread() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            };

            threadOne.start();
            threadTwo.start();
            threadOne.join();
            threadTwo.join();

//            String result = "第" + i + "次( x=" + x + ",  y="+  + y + ")";
//            System.out.println(result);

            if (x == 0 && y == 0) {
                String result = "第" + i + "次( x=" + x + ", y=" + y + ")";
                System.out.println(result);
                break;
            }
        }
    }
}

如果不发生指令重排的话,正常的执行结果是:x=0,y=1。但是我们实际运行过程中会碰到这种情况,如下图:


image.png

可以发现,在循环第196558次的时候,既然出现了x=0,y=0的情况,发生这种情况的唯一可能就只有threadOne线程中的指令x=b跑到a=1前面,threadTwo中的指令y=a跑到b=1前面才可能发生,因为上面说了,单线程情况下,x=b和a=1谁在前面是不是都没影响,只有在多线程情况下,他们才可能由于指令重排造成意想不到的结果。

上面代码验证了指令重排可能造成的结果,接下来说一个我们最为常用的

如:双重校验锁单例下:

public class TestSingle {

    private static TestSingle instance;

    int i = 0;

    private TestSingle(){
        i = 13;
    }

    public static TestSingle getInstance(){
        if(instance == null){
            synchronized (TestSingle.class){
                if(instance == null){
                    instance = new TestSingle();
                }
            }
        }
        return instance;
    }

}

上面代码,可能发生的情景:
线程执行到new TestSingle时,由于指令重排机制,可能执行的顺序是1-2-3-4或者1-2-4-3。1-2是堆栈的内存分配,不会有指令重排的问题,总的来说就可以分为三个步骤:

所以,为了防止CPU在多线程下指令重排造成的影响,使用关键字volatile来解决。

好了,Java多线程编程之volatile关键字到此结束,有不同见解的请直接评论区指出,唯有不足才有继续成长的空间!
这里借这片文章再说一下,由于这段时间真的挺忙的,所以很少学习,也很少更新博客公众号,尽量多挤出来时间来学习和记录分享吧。

上一篇 下一篇

猜你喜欢

热点阅读