💐个人主页:初晴~
前言
线程安全问题是在多线程学习中一个十分重要的话题。多个线程并发执行就容易产生许多冲突与问题,如何协调好每个线程的执行,让多线程编程“多而不乱”,就是线程安全问题学习所要实现的了。这篇文章就让我们来深入探讨线程安全吧
目录
一、概念
造成线程不安全的主要原因是线程调度是随机的,这是线程安全问题的罪魁祸⾸,随机调度使⼀个程序在多线程环境下, 执⾏顺序存在很多的变数. 程序猿必须保证 在任意执⾏顺序下 , 代码都能正常⼯作.
二、Synchronized
1、修改共享数据问题
让我们先来看一下下面这段代码:
public class Main {
public static int count=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++){
count++;
}
});
Thread t2=new Thread(()->{
for(int i=0;i<50000;i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
如果是在单线程的环境下执行类似逻辑,那结果毫无疑问肯定是100000,但是实际结果却相距甚远:
甚至每次运行的结果都不一样:
1、load 把内存中的数据读取到CPU寄存器中2、add 把cpu寄存器里的数据+13、save 把CPU寄存器里的值写回内存
而CPU在调度执行线程时,随时都有可能切换执行其它线程(抢占式执行,随机调度)
指令是CPU执行的最基本单位,要切换线程,也会等当前线程的指令执行完毕才调走,不会出现指令执行一半的情况。
但由于count++操作需要三个指令,CPU执行了一个指令或两个指令或三个指令的任何时候都有可能被调度走从而使此次count++操作的结果产生偏差,并且由于调度的随机性,这种偏差也是无法预测的,也就导致了我们上面所看到的每次程序执行结果都不同的现象了
还有很多可能得情况,博主就不一一例举了。但只有第一二种情况程序才能正常运行:
这样两个线程的三个指令都能分别完整的被执行完,最后结果就会是count=2了。
错误的情况会类似下面这种:
上述过程中,明明执行了++操作两次,但最终结果却是1。因为这两次加的过程中结果出现了覆盖。
由于五万次循环中,无法确定有多少次的执行顺序是1、2这两种正确的执行顺序,因此最终的结果是不确定的,而这个值是一定小于10万的。
2、解决方法
会出现上述问题的主要原因是java中许多语句的执行都不是原子性的,这导致了如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。
比如说我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就是不具备原⼦性的。
那我们应该如何解决这个问题呢?是不是只要给房间加⼀把锁,A 进去就把⻔锁上,其他⼈是不是就进不来了。这样就保证了这段代码的原⼦性了。
同样的,我们也可以给线程上一把“锁”,把“非原子”的操作变成“原子”,保证线程执行的原子性。在java中,我们就可以用synchronized实现该操作
3、synchronized使用
(1)修饰代码块
用sychronized修饰代码块{},进入{就会自动加锁,出了}就会解锁,如下:
public class Main {
public static int count=0;
public static Object locker=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for(int i=0;i<50000;i++){
synchronized (locker){
count++;
}
}
});
Thread t2=new Thread(()->{
for(int i=0;i<50000;i++){
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
这下程序结果就没问题了: