目录
一、线程不安全的原因
1、抢占式执行、随机调度
2、多线程同时修改同一个变量
3、修改操作不是原子的
4、内存可见性
5、指令重排序
二、解决方法
1、使用synchronized方法加锁
(1)、定义
(2)、使用
(3)、死锁
2、使用volatile关键字
3、使用wait和notify
一、线程不安全的原因
例:
class Counter{
private int count=0;
public void add(){
count++;
}
public int get(){
return count;
}
}
public class demo {
public static void main(String[] args) throws InterruptedException {
Counter counter=new Counter();
//创建两个线程分别对counter自增50000次
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
//结果不为100000!!!
System.out.println(counter.get());
}
}
两个线程针对同一个变量,各自自增50000次。预期结果是100000,实际结果却是个随机值,每次的结果都不同,这就是有多线程引起的线程线程安全问题。以下便是引起线程不安全的多个原因:
1、抢占式执行、随机调度
抢占式执行导致线程不安全的最根本的原因、是万恶之源。因为CPU在进行线程调度时,是随机的,线程中的代码执行到任意一行,都随时可能会被切换出去,因此无法解决。
2、多线程同时修改同一个变量
当多个线程同时修改同一个变量的时候,很容易产生线程的不安全。但可以通过调整代码结构来避免这个问题。
3、修改操作不是原子的
原子性是不可拆分的基本单位,因此如果修改操作是原子的,就不会出现太大问题,但如果是非原子的,出现问题的概率就非常大。
例:
count++操作,本质上是三个cpu指令构成
- load,把内存中的数据读取到cpu寄存器中
- add,把寄存器中的值,进行+1运算
- save,把寄存器中的值写回到内存中
因为count++操作不是原子的,可以发生改变,所以上边的代码便出现了与预期不同的结果。因此针对这个问题,我们可以通过加锁(synchronized),使其变成一个整体。
4、内存可见性
一个线程频繁读,一个线程修改的操作也存在安全问题,可能产生脏读,读的结果不符合预期。
例:t1频繁读取内存,效率较低,于是就被优化成直接读取CPU寄存器;t2修改了内存的结果,由于t1没有读取内存,导致修改不能被识别到,因此产生了线程安全问题。
5、指令重排序
编译器在执行代码时会检测代码,存在编译器自作主张在保证相同的逻辑情况下对代码进行优化和修改,从而加快程序执行的效率,这是发生在单个线程里面的。这就有可能出现安全问题。
二、解决方法
1、使用synchronized方法加锁
(1)、定义
synchronized可以指定一个锁对象加锁,进入synchronized修饰的代码块, 相当于加锁,退出synchronized 修饰的代码块, 相当于解锁,不需要额外的解锁有效的防止遗忘解锁。
例:线程1使用synchronized加锁,在线程1加锁的过程中synchronized会起到互斥效果,线程2无法把自己的指令插入到线程1的修改过程中,使线程1的加锁的修改操作变为原子性。
(2)、使用
class Counter{
private int count=0;
private Object locker=new Object();
//1、锁对象是类对象
// synchronized public void add(){
// count++;
// }
public void add(){
//2、手动指定锁对象
synchronized (locker) {
count++;
}
}
public int get(){
return count;
}
}
public class demo02 {
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.get());
}
}
(3)、死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
2、使用volatile关键字
线程修改一个变量,会把这个变量先从主内存读取到工作内存,然后修改工作内存的值,再写回到主内存中。
通过上文的内存可见性我们可以知道因为t1频繁读取主内存,效率较低,所以直接被优化成读取工作内存,当t2修改了主内存时,由于t1没有读取主内存,导致修改不能被识别到。
归根结底就是编译器在多线程环境下优化时产生了误判。此时,volatile这个关键字就可以发挥作用了。我们可以使用volatile关键字来修饰变量,来告诉编译器,这是一个易变的变量,不可以随意进行优化。以此保证内存可见性,禁止编译器优化。
public class demo {
//此处,flag变量如果不被volatile修饰,程序就会一直运行,while感知不到flag的变化。
volatile public static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (flag) { }
System.out.println("t1线程结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个false");
flag = scanner.nextBoolean();
});
t1.start();
t2.start();
}
}
3、使用wait和notify
线程1调用wait方法后就会进入阻塞,此时处于WAITING状态。如果wait方法中没有参数,就一直等待,直到被notify唤醒。
注:wait的使用需要搭配synchronized。