文章目录
一、线程安全是什么?
一个程序在单线程情况下没有问题,但是拿到多线程运行就会产生一些bug
二、为啥会产生线程安全问题?
1.线程调度的随机性(根本原因)(抢占式执行)
2.多个线程对一个变量进行修改
3.修改操作不是原子的
4.内存的可见性
5.指令重排序
举例:
1.count++问题
class add{
public static int count=0;
public void add(){
count++;
}
public int getcount(){
return count;
}
}
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
add coun=new add();
Thread t=new Thread(()->{
for (int i = 0; i <50000; i++) {
coun.add();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i <50000; i++) {
coun.add();
}
});
t.start();
t2.start();
t.join();
t2.join();
System.out.println(coun.getcount());
}
}
在这个多线程程序中,我们的期望应该两个线程各自加了五万次,最后输出count应该是100000,但是实际运行出来
结果并不是100000,这就是多线程导致的线程不安全问题,具体是为什么呢?
这里我们可以把count++这一步分为三个cpu指令,分别是
load:从内存中拿出来
add:CPU运算结果
save:将结果写回内存
由于线程的调度是随机的,所以你无法确定到底是先执行那一句??这就导致三个cpu指令会有很多不同的排序,产生不可预料的BUG。(这里简单举例几种)
此时就拿中间的举例,t1线程从内存中读取count为0,然后给cpu++得到1,但是并不是立即写会内存,此时t2线程读取内存的count也为0,add++得到1,然后写回内存,此时count为1了,但是t1线程再次写会,结果也是1,这就出现了加两次只实现了一次的BUG,这一切都是由于线程的调度是抢占式运行的!!(还有修改操作不是原子性)
2.由于内存的可见性导致线程不安全
import java.util.Scanner;
public class ThreadDemo2 {
public static int flag=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
while (flag==0){
}
System.out.println("循环结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
flag=scanner.nextInt();
});
t1.start();
t2.start();
}
}
这个程序的预期结果是:线程t1和t2一起并发执行,t1一直在循环,当t2线程输入一个非0的数,就会改变flag,从而使t1线程停下来。
但是运行之后我们可以看到,线程并没有停下来,这就是内存的可见性导致的线程不安全问题
具体是因为:我们的编译器都有一些自动优化的功能,在where这一句可以分为两个cpu指令
load:从内存中读取数据
cmp:从寄存器中比较是否为0 此时load的开销是远远大于cmp的
因为线程执行的是非常快的,当我们的编译器发现,load开销非常大,并且每次读取的都是同一个结果,编译器就会自动的把load优化掉,这样就导致,只有第一次load的值,后面再对load的值修改,线程也不会再读取到了,线程就只cmp不load了,所以就导致了后面我们修改了flag线程也并没有结束。
总而言之就是:由于编译器的优化导致的BUG,这些优化对单线程可能没什么问题,有些时候多线程就会产生问题
编译器优化:能够只能的调整你的代码的实行逻辑,保证程序结果不变的情况下,通过加减语句,通过语句变换等操作,让你的代码执行效率大大提升
3.由于指令重排序导致的线程不安全
volatile还有个效果:禁止指令重排序,指令重排序也是编译器的优化,但是可能也会导致线程不安全。
指令重排序:调整代码执行的顺序,让程序更高效,前提也是保证整体逻辑不变~
三、如何避免线程安全问题
1.java中有锁来解决修改操作不是原子性:
public void add(){
synchronized (this){
count++;
}
}
java中使用synchronized来给线程加锁
一旦某个线程/操作加锁之后,其他的线程再想上锁,就不能直接上锁,必须阻塞等待,一直等到拿到锁的线程释放了才能加锁(多个线程抢占式加锁,不管先后)
java使用代码块来修饰,在java中加锁和解锁都是自动的,进入代码块加锁,出了代码块就会自动解锁
这里的this是锁对象,如果两个线程给一个锁对象加锁,此时就会出现“锁竞争”问题,一个线程先拿到锁,另一个线程就得阻塞等待。
注意:()里填什么都是,只要是个Object的实例就行(内置类型不行),具体填什么,看你给谁上锁,如果两个线程对一个对象加锁就会有锁竞争,不同对象就不会有锁竞争
注意join和加锁的区别:加锁是只有加锁的那一块逻辑,这一块逻辑变成串行,其他的逻辑还是可以并发的,join是整个线程完全串行,效率上来说加锁还是比join高效很多
此时再次运行就可以得到正确的结果了
2.使用volatile来让编译器优化失效(指令重排序也是编译器优化,使用volatile也可以解决)
使用volatile修饰需要的变量,就可以让编译器对这个变量不在优化,就可以达到每次都load,都从内存中读取flag。
总结
介绍了三个多线程安全问题和JAVA 是如何解决多线程安全问题的,谢谢老铁支持~