一、多线程不安全引例
使用两个线程同时对count变量进行相加,观察结果。
public class Count { public int count; void test(){ count++; } public static void main(String[] args) throws InterruptedException { Count counter = new Count(); Thread t1=new Thread(()->{ for(int i=0;i<3000;i++){ counter.test(); } }); Thread t2=new Thread(()->{ for(int i=0;i<3000;i++){ counter.test(); } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter.count); } }
运行结果:
此时代码的运行结果并不是6000,并且每次运行得到的结果不一致,运行结果在3000~6000之间,此时多线程并发编程就是不安全的。 线程不安全就是多线程并发执行代码没有得到预期的结果。
二、线程不安全的原因
1、线程是抢占式执行
线程的抢占式执行就是在一个线程的执行过程中,另一个更优先的进程会抢占当前线程执行的任务,当前线程就会被迫中断,这是引发线程不安全的根本原因,但是线程的调度是随机的,这是由系统决定的,我们无法改变。
2、多线程共享同一变量
多线程共享同一变量如果只是读操作就不会引发线程安全问题,但是如果多线程都修改同一变量就会引发安全问题,就是引例当中的情况,因为修改操作不是原子性,需要多步完成就有可能发生线程抢占导致中断。
3、对变量的操作不是原子性
操作原子性也就是操作能一步完成,但是修改变量的操作就可以分为:将变量从内存加载到寄存器(load) 、修改变量(update)、把寄存器的值加载回内存(save),那么如果有两个线程对变量进行修改操作由于线程的抢占式执行就会出现以下情况:
安全情况:未发生抢占,两个线程都能按序执行完。
不安全情况:发生了抢占,应该有多种,仅画出两种说明
出现上述情况导致的结果就是更新丢失,例如是自增操作: 假设内存中的变量的初始值为0,t1就先把0加载到寄存器,但是t2进行抢占,也从内存中把0加载到寄存器然后自增为1,然后将1加载回内存,然后t1再自增为1,再将1加载回内存,按道理两次自增应该为2,但是由于线程的抢占以及自增操作是非原子的就会出现上述情况。
4、内存可见性
变量通常存放在内存中,线程对变量操作需要首先从内存中拿出到寄存器,但是一个线程频繁进行读操作,就可能会直接从寄存器上读,不再进入内存这就引发线程不安全,因为线程得不到内存中变量的最新值。
5、指令重排序
指令重排序是编译器的优化操作来提高代码运行的效率,但是对于多线程在进行指令重排序时就可能会出现错误引发线程安全问题。
三、线程不安全问题的解决方案
1、使用synchronized关键字进行加锁
使用synchronized关键字加锁后可以处理内存可见性和保证原子性,使多线程之间互斥。synchronized可以修饰普通方法、代码块、静态方法。
a、 synchronized修饰普通方法
此时synchronized的加锁对象就是this。
例如将引例编程线程安全,就在test方法前加上synchronize即可。
synchronized void test(){ count++; }
运行结果:
b、synchronized修饰代码块
这里需要显示指定锁对象,在Java中任何对象都可以成为锁对象,对引例的test方法进行修改:
void test(){ synchronized(this){ count++; } }
c、synchronized修饰静态方法
这里相当于是给当前类的类对象加锁。
public synchronized static void method() { }
2、使用volatile关键字保证内存可见性
使用volatile只能修饰变量,保证内存的可见性,但是volatile无法保证原子性。
四、死锁问题
1、死锁
多个线程相互竞争资源而引起的一种僵局,若无外力作用,僵局就会一直保持。
2、死锁场景
a、一个线程一把锁
一个线程连续加锁两次,如果是不可重入锁,就会发生死锁
b、两个线程两把锁
两个线程对各自已有的对象进行上锁,但是执行过程中又需要对方的对象,两个线程此时都不释放锁,就都不能继续执行就会产生死锁。
c、m个线程n把锁
例如哲学家吃饭问题。
3、死锁产生的四个必要条件
a、互斥使用
一个锁被一个进程使用之后,其他的线程无法使用。
b、 不可抢占
一个锁被一个进程使用了之后,其余的线程不能抢走该锁。
c、请求和保持
一个锁占用了多把锁之后,除非显示地释放锁,否则这些锁始终都被该线程持有。
d、环路等待
等待关系形成了一个环,A等B,B又等A。
4、wait方法
wait()方法:线程等待方法
线程一旦调用了wait方法后就会进入阻塞状态
调用wait方法后,wait方法内部首先会释放锁,然后进入阻塞状态等待唤醒,收到唤醒通知后重新加锁继续向下执行。
5、notify()方法
notify()方法:唤醒等待的线程。
6、noifyAll()方法
notifyAll()方法:唤醒所有等待的线程。
当多个线程都是wait状态时,使用notify()只能随机唤醒一个线程,而notifyAll能唤醒所有的wait线程。
- 注意:
- notify()/wait()是针对同一个对象操作。
- notify()/wait()都要搭配synchronized使用。