一:什么是线程安全问题:
简而言之如果有多个线程之间同时并发进行执行,由于线程的无序调动已经编译器的优化,可能会导致预期结果与实际结果不一致,这就是线程不安全。
二:线程不安全出现的原因即解决方案:
1.线程的抢占式执行,即线程之间的调度是无序的
2.多个线程同时修改同一个变量
注意:
针对出现的1,2这两种情况来解决线程安全问题是非常困难的,因为要想控制多个线程之间的执行是十分复杂的,所以这里就不去讨论解决方法。
3.修改操作不是原子的
首先这里的“原子”是指不可分割的最小单位,例如以下代码如果要使两个线程分别自增5w次那么理论上结果应该是10w
class ThreadAdd{
public int count=0;
public void add(){
count++;
}
}
public class T {
public static void main(String[] args) throws InterruptedException {
ThreadAdd threadAdd=new ThreadAdd();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
threadAdd.add();
}
});
Thread t2 = new Thread(() -> {
int count2=0;
for (int i = 0; i < 10000; i++) {
threadAdd.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(threadAdd.count);
}
}
但是结果却是一直在变化的
这就是因为在线程中的count++操作不是原子的问题,因为count++还可以分为load,add,save这三个操作,所以对于两个线程t1,t2来说,这三个操作的相对执行顺序会出现问题就会导致结果的不一样。
解决方案:使用synchronize关键字进行加锁操作
class Counter{
private int count=0;
public void add(){
synchronized (this){
count++;
}
}
public int getCount(){
return count;
}
}
public class ThreadDemo10 {
public static void main(String[] args)throws InterruptedException {
Counter counter=new Counter();
Thread t1=new Thread(() ->{
for (int i=0;i<10000;i++){
counter.add();
}
});
Thread t2=new Thread(() ->{
for (int i=0;i<10000;i++){
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
}
}
注意:
这个在main方法里面new出来的counter是锁对象而且这两个线程的锁对象必须是一个对象这样才能产生锁竞争。
加锁和join的区别,可以认为join只是让两个线程完整的进行串行而加锁指两个线程的某个小部分串行,但大部分都是并发的。
4.由于内存可见性引起的线程不安全(即与编译器优化有关)
例如以下代码
mport java.util.Scanner;
public class T {
public static int flag=0;
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(() ->{
while(flag==0){
}
System.out.println("循坏结束,t1结束");
});
Thread t2=new Thread(() ->{
Scanner sc=new Scanner(System.in);
System.out.println("请输入一个整数");
flag=sc.nextInt();
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
理论上来说如果在线程t2输入了一个整数,那么t1之中的while循坏是会结束的,可是运行发现t1这个线程始终不结束,这就是在while(flag==0)出现了编译器优化的问题,也就是内存可见性的锅
解释:
while循坏里面可以分为两个操作load和cmp,load是从内存读取数据到cpu寄存器,cmp是比较寄存器的值是否是0但是此次的load操作是时间开销是远远高于cmp的,所以此时编译器就做了一个大胆的操作就是----把load操作优化了掉了,所以只有第一次执行load才是真正执行了,后续的循环都只是cmp而不进行load,这就导致循坏没有办法停下来。
解决方案:
使用volatile关键字就会让编译器停止优化操作,能够保证每次都是从内存重新读取数据(就是给变量flag加一个volatile修饰)
注意:由于内存可见性导致的线程不安全一般都是体现在多线程的执行下而一般单线程是没有影响的,也就是编译器优化对单线程的执行是没有问题的
5.指令重排序(和内存可见性相似)
简而言之,指令重排序导致的线程不安全问题也是与编译器优化有关的。
什么是指令重排序?
比如你去菜市场买白菜、西红柿、猪肉、黄瓜这四个菜,但这四个菜都在不同的摊位,即你买菜的顺序是有不同的排列组合的,那么线程也是如此,编译器会优化为最优的执行顺序,所以这个时候可能会导致出现线程安全问题
、
解决方案:(同样也是使用volatile关键字)
使用volatile关键字禁止指令重排序(指令重排序也是编译器优化的策略,调整了代码执行的顺序让程序更高效前提也是保证整体逻辑不变)。