目录

1.引言

2.volatil关键字

2.1内存可见性

2.2指令重排序


1.引言

  大家好,我是老cu,今天我们来继续聊聊线程安全问题 线程安全是我们在编程开发中遇到的非常常见,棘手 的问题.同时也是多线程部分很复杂的问题.为了线程安全我们要做很多努力.也要对线程安全部分的代码进行慎重的写,本篇文章,我们将继续围绕线程安全问题来展开.

2.volatil关键字

2.1内存可见性

我们先看下面的代码:

import java.util.Scanner;

public class Test {
    public static int a = 0;
    public static void main(String[] args) {

        Thread t1 = new Thread(()->{
            while (a==0){
                
            }
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            int b = scanner.nextInt();
            a=b;
        });
        t1.start();
        t2.start();
    }
}

很明显,上述代码是定义了一个全局静态变量a,并初始化为0,在t1线程里,通过while循环,如果a==0,则一直循环下去,在t2线程里改变a的值,理论上来说,如果我们输入一个非0的数,循环会停止.但是实际情况呢?

redis5 多线程_重排序

在我们运行以后发现,即使我们输入了一个数字循环还是没有结束.这是为什么呢?

  原来这是因为编译器为了执行效率,会进行优化,它检测到这个数字一直没有变化.本来的while循环步骤应该是直接在内存中读取a的值,然后在判断是否进行打印.但是为了效率,它优化的时候,发现这个数字并没有什么变化.就省去了去内存中读取这个步骤.直接从寄存器(工作内存)中读取数值a,此时我们在内存中修改这个数字,自然就影响不了结果了.

那么我们应该怎么做,可以让编译器每次在读取的时候,从内存中读取,而不是优化到寄存器中呢?

很简单,在这个变量前加入volatile关键字即可.

redis5 多线程_redis5 多线程_02

可以看到,加了这个volatile关键字以后,程序就能正常读取我们写入的数值了.

这就是volatile关键字的其中一个作用:解决内存可见性问题.

2.2指令重排序

为了举这个例子,我们先介绍一种设计模式-单例模式中的懒汉模式.单例模式是一种设计模式,它通过技巧,让一个类只能有一个对象,如果在实例化这个类,企图创建一个新的的对象就会报错.

redis5 多线程_线程安全_03

当线程执行到new SingletonLazy();的时候

一般来说.new一个实例分为以下三步:
1.申请一块内存

2.实例化对象

3.将内存引用赋值

但是编译器有时候会进行指令重排序,上述的过程可能会变成 1 3 2 

在一个线程的时候,不会有什么问题.但是如果是两个线程的话

redis5 多线程_线程安全_04

就会出现上述 的问题,为了解决它.我们可以将这个引用加入volatile关键字来禁止指令重排序:

class SingletonLazy{
    // 这个引用指向唯一实例. 这个引用先初始化为 null, 而不是立即创建实例
    private volatile static SingletonLazy singletonLazy = null;
    private static Object object = new Object();

    public static SingletonLazy getSingletonLazy() {
        if(singletonLazy == null){
            synchronized (object){
                if(singletonLazy == null){
                    singletonLazy = new SingletonLazy();
                }
            }
        }
        return singletonLazy;
    }
    private SingletonLazy(){

    }
}

这样就不会再出现问题了