一:线程安全问题的原因:
1.线程是抢占式执行的,即具有随机性
2.多线程的情况下,线程同时去修改同一个数据或者有的线程在修改数据,有的线程在读取数据
3.一条语句对应多个指令,这些指令操作并不是原子的。指令有可能会相互穿插,从而数据的安全性就无法得到可靠保证
4.内存可见性问题:当我们有两个线程t1,t2在执行的时候,假如线程t2对数据进行了修改,然而线程t1读到的数据却是修改之前的数据。
5.指令重排序:主要也是由于编译器优化带来的问题
二:解决方案:
1.对于原因1,这是由操作系统内核实现的,我们一般情况下是无能为力的;
2.对于原因2,由于这是我们的具体的场景下的要求,我们要达到目的不能随意的更改需求,所以这里也没办法做什么文章;
3.对于原因3,这些操作指令不是原子的,那么我们可以想办法将这些指令1打包成一个原子的操作,即加锁。在java中,每一个类对象都会有一把锁,只有当线程持有这把锁的时候才可以继续向下执行,否则线程将会处于阻塞状态。因此我们可以用加锁的方式来保证线程安全,当线程持有对象锁之后,继续向下执行,直到代码块执行完才会释放锁。其他线程将会继续竞争这把锁,同1所述,最终谁能拿到锁,继续向下执行,这是由操作系统内核实现的,或者说具有随机性的,我们无法确定。
4.对于原因4,本质上是编译器在对我们的代码进行优化的时候,将我们的数据看作是不变的数据,因此只会读取一次就不再读取,所以在我们后续对数据进行更改以后,程序也读不到。解决方法就是通过volatile关键字来告诉编译器,在这里不需要对代码进行优化即可。
5.对于原因5,请看如下的场景:
class SingletonLazy {
//懒汉模式
private static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
if(instance == null) {
instance = new SingletonLazy();
}
return instance;
}
//将构造方法私有化,防止在类外被new
private SingletonLazy() {}
}
对于if里面的new操作实际上也是可以分为许多步骤的,这里我们简单分为三个步骤:
1.申请内存,得到内存的首地址;
2.调用构造方法,来初始化实例;
3.把内存地址赋值给实例引用;
那么此时编译器可能就会对代码进行"指令重排序"的优化。
假设在这里的执行顺序是132,那么当一个线程执行到3时,他得到的就是一个内存地址(上面的数据时无效的),这个时候线程2调用了getInstance()方法的时候,他就会认为对象传创建好了,就会将他直接返回,我们在外边也有可能会对这个不完全的对象进行解引用操作(调用里面的方法,使用属性),这就造成了线程的不安全。
解决办法同样也是告诉编译器在这里不需要进行优化即可
我们一般使用volatile或者synchronized关键字