线程安全问题
文章目录
多线程带来的风险
线程安全问题:在操作系统的随机调度下 ,多个线程的并发执行会产生多种可能,可能会产生BUG
package thread;
class Test{
int a;
public void func(){
a++;
}
}
public class Demo8 {
public static Test test = new Test();
public static void main(String[] args) throws InterruptedException {
Runnable runnable1 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
test.func();
}
}
};
Thread thread1 = new Thread(runnable1);
Runnable runnable2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
test.func();
}
}
};
Thread thread2 = new Thread(runnable2);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(test.a);
}
}
- 我们使用两个线程对同一个变量分别自增了5000次,按理来说最后结果应该是10000,下面我们来看结果
- 可以看出运行多次结果都不相同,不是10000,原因是:
a++; 这一行代码对应了三条机器指令:
从内存读取数据到CPU(load),在CPU寄存器中完成加法运算(add),把运算结果放到内存中(save)
那由于两个线程的执行是调度器随机调度的,所以在某一时刻,这两个线程可能都在CPU的不同核心上运行(并行执行),也可能只有一个在CPU上执行,也可能俩线程都没在CPU上执行。如果俩线程并行执行那就可能会出问题,下面具体来看:
- 如果线程在执行时有这样的时间关系,也就是线程1从Load到Save的时间段内又有线程2的++操作穿插进来了,那么两次Load读到的值是一样的,假如都Load了个0,则Save了1,那这两个++操作的执行效果其实是和一次++一样的,明明是两次自增操作,结果却只增了一次,这就是典型的线程不安全。
- 那其实也有可能线程1和2的++操作是完全串行化的,那两次自增操作,结果就会加2
- 极端情况下线程1和线程2中的所有++操作都如上图一样,那最终的值就是5000,如果线程1和线程2是完全串行化的,那最终结果就是10000,所以最后的值范围是5000~10000之间
线程不安全的原因
操作系统随机调度
操作系统随机调度/抢占式执行,是造成线程安全问题的罪魁祸首,这个是操作系统的调度器的逻辑,我们是无法改变的
多个线程修改同一个变量
在多线程带来的风险演示的代码中test.a在堆区,各个线程共享,如果多个线程同时修改同一个变量,就有可能造成多线程带来的风险中的bug
非原子性
有些修改操作不是原子的(不可分割的最小单位),比如上面的++操作,就对应了三个指令,这就不是原子的,而 =(赋值)就对应了一条机器指令,就是原子的。
内存可见性
可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到
在下面这样的一个场景中:一个线程读,一个线程写就比较容易引发内存可见性问题
线程1在频繁的读和判断,如果中间线程2突然写了一下内存(线程1和线程2都是针对同一个数据),那线程2写完之后,线程1就能立刻读出变化了的内存数据,从而让判断出现变化。
但是程序在运行中,可能会出现优化,这个优化可能是编译器的优化,也可能是JVM的优化,也可能是操作系统的优化,下面具体来看一下如何优化的:
Load是读内存,Test是在寄存器中做判断,所以这个Load操作就比Test消耗时间多得多,那既然线程1频繁读内存操作都是读取到的同一个值,又耗时这么高,那所以JVM就做出优化:不再重复的从内存中读了,直接复用第一次从内存读到寄存器中的值进行每次的判断
那一优化就可能会出现问题:假如判断过程中,线程2执行了一次写操作,把这个共享的数据给改了,那线程1是感知不到内存数据的变化的,线程1还是用的寄存器中的值做判断,没有从内存中读数据,那就不能及时的做出相应的反应,这就是内存可见性问题(内存被修改,但是读不到,看不见),但是对于单线程就没有这样的问题,单线程中,你去写内存,CPU中执行的是写的指令,没有执行判断的指令,然后写完之后,再去判断,就从内存读数据再判断,判断和写肯定是有先后关系的,但是多线程就是并发的关系,写的时候依然可以进行判断
用volatile就可以解决这个内存可见性问题,让某个变量不要优化
指令重排序
指令重排序是对代码的执行顺序进行调整,以提升运行速度,也是编译器/JVM/操作系统的一种优化,但是在多线程的环境下,就可能会产生BUG
比如 Test test = new Test(); 可以分为三个指令 1.创建内存空间,2.往这个内存空间上构造一个对象,3.test引用这块内存空间的地址 ,而2和3是可以调换顺序的,在单线程下调换顺序是没啥影响的,但是如果在多线程下:
如果按照2,3的顺序执行,在另一个线程下获取到的test就是一个有效地址,如果按照3,2执行,那获取到的test就可能是一个无效地址(地址所对应的内存空间中没有
指令重排序也是可以用volatile避免这种问题
synchronized
synchronized作用
synchronized的意思是使同步
synchronized 关键字可以给某个对象加锁,以保证原子性,具体给哪个对象加锁,下面再分析
package thread;
class Test{
int a;
public synchronized void func(){
a++;
}
}
public class Demo8 {
public static Test test = new Test();
public static void main(String[] args) throws InterruptedException {
Runnable runnable1 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
test.func();
}
}
};
Thread thread1 = new Thread(runnable1);
Runnable runnable2 = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
test.func();
}
}
};
Thread thread2 = new Thread(runnable2);
thread1.start();
thread2