目录
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的数,循环会停止.但是实际情况呢?
在我们运行以后发现,即使我们输入了一个数字循环还是没有结束.这是为什么呢?
原来这是因为编译器为了执行效率,会进行优化,它检测到这个数字一直没有变化.本来的while循环步骤应该是直接在内存中读取a的值,然后在判断是否进行打印.但是为了效率,它优化的时候,发现这个数字并没有什么变化.就省去了去内存中读取这个步骤.直接从寄存器(工作内存)中读取数值a,此时我们在内存中修改这个数字,自然就影响不了结果了.
那么我们应该怎么做,可以让编译器每次在读取的时候,从内存中读取,而不是优化到寄存器中呢?
很简单,在这个变量前加入volatile关键字即可.
可以看到,加了这个volatile关键字以后,程序就能正常读取我们写入的数值了.
这就是volatile关键字的其中一个作用:解决内存可见性问题.
2.2指令重排序
为了举这个例子,我们先介绍一种设计模式-单例模式中的懒汉模式.单例模式是一种设计模式,它通过技巧,让一个类只能有一个对象,如果在实例化这个类,企图创建一个新的的对象就会报错.
当线程执行到new SingletonLazy();的时候
一般来说.new一个实例分为以下三步:
1.申请一块内存
2.实例化对象
3.将内存引用赋值
但是编译器有时候会进行指令重排序,上述的过程可能会变成 1 3 2
在一个线程的时候,不会有什么问题.但是如果是两个线程的话
就会出现上述 的问题,为了解决它.我们可以将这个引用加入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(){
}
}
这样就不会再出现问题了