为了更好的理解死锁,读者可能需要了解线程安全相关的知识,主要是对线程执行的六大状态有一定的了解,尤其是阻塞状态,这是死锁发生的关键。进而才能对死锁的过程深入理解。这里附上一篇读前分享链接,当然,你也可以直接跳过。也是可以读懂这篇故事型文章的。
有一对夫妻,拥有一辆小汽车(可能时劳斯莱斯);同时两人婚前约定,只能用一个钱包(请不要争执电子支付,他们不用),且每次只能由一个带在身上;
有一天,丈夫准备去和朋友一起去吃饭,需要拿上车钥匙和钱包,妻子想去商场购物,也许要拿上钥匙和钱包;
类比如下:
每个对象都有一个互斥标记锁,用来分配给线程的。只有拥有对象互斥锁标记的线程才能进入该对象加锁的同步代码块。线程退出同步代码块时会释放相应的互斥锁标记。
夫妻双方都有自己的事情去做,这个过程类比的是两个线程的执行;两人如果需要完成自己想做的事情,就必须同时拿到钱包和车钥匙,这里类比的是多个线程访问临界资源,而每个临界资源对象都有且只有一个锁标记,只有拥有锁标记的线程才可以拥有这个对象;
类比:两个线程,线程1首先拿到OS分配的时间片,然后访问两个临界资源对象并顺利拿到两个锁标记,且在时间片未到期之前执行完毕所有事情,然后释放锁标记,线程2进而继续执行;
类比:线程1首先拿到OS分配的时间片,然后访问临界资源对象 钱包,但是在拿到该对象的锁标记,准备拿另一个临界资源对象的锁标记的时候,时间片到期了,因为陷入了限期等待状态。而在线程1限期而等待期间,线程2拿到了OS分配的时间片,并拿到了临界资源对象 车的锁标记,因为没有找到 钱包对象的锁标记,因此进入阻塞状态,等待其他线程释放锁标记;此时线程1限期等待结束,并再一次拿到OS分配的时间片,可因为拿不到 车对象的锁标记,因此也陷入了阻塞状态,两个线程因此都陷入了阻塞状态。
很显然,两个线程各自拿到一个临界资源对象的锁标记,并且都在阻塞状态等待对方释放锁标记,互相都不愿意释放锁标记,因为陷入了无限期的僵持状态,这就是死锁;
夫妻间产生争执时非常正常的事情,但是不能一直僵持,僵持就会陷入“死锁”,一定要沟通交流才能解决完问题,因此,在双方对需要对方的东西去做自己想做的事情时,要去协商,可以把钱包给妻子,让她先去购物,之后妻子完成自己的事情再把车和钥匙交给丈夫,进而解决问题,这就需要“通信”。
类比:线程1将自己所持有的临界资源对象(钱包对象)的锁标记用过调用
obj.wait()
释放后,自己进入等待队列,而此时,线程2因为拿到了线程1中的锁标记,因此进入就绪状态开始执行。当线程2执行完毕后,将锁标记释放回临界资源对象,但是此时因为线程1处于阻塞状态,已经无法主动访问临界资源对象拿到相应的锁标记,因此需要其他线程(可以是任意拥有线程1所需锁标记的线程)来通知(notify()
方法)该线程,进而线程1才可以执行。
obj
加锁的同步代码块中,调用obj.wait()
时,此线程会释放其拥有的所有锁标记。因此此线程阻塞在obj
的等待队列中。释放锁,进入等待队列。synchronized (临界资源对象){ //对临界资源加锁
//同步代码块(原子操作)
}
obj
的waiting
中释放一个或全部线程。对自身没有任何影响。因为线程拿到时间片时随机的,因此无法用代码在再现夫妻出行的例子,该示例主要是帮助读者理解死锁的产生;这里几个代码案例主要是帮助理解线程通信的几个方法。
现有一个Object临界资源对象,有一个线程MyThread
,当该线程进入同步代码块的时候,主动释放了锁标记(模拟释放锁标记的过程),此时该线程将陷入无限期等待,代码永无止境的不能结束。
public class TestWaitNotify {
public static void main(String[] args){
Object o = new Object();
MyThread myThread = new MyThread(o);
myThread.start();
}
}
class MyThread extends Thread{
Object obj;
public MyThread(Object obj) {
this.obj = obj;
}
@Override
public void run() {
System.out.println("线程启动");
synchronized (obj){
System.out.println("进入同步代码块");
//主动释放obj锁标记,无限期等待
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("退出同步代码块");
}
System.out.println("线程结束");
}
}
打印结果:
因此当某个线程释放锁标记后,必须有另一个线程来通知该线程,否则该线程将一直处于阻塞状态。
public class TestWaitNotify {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
MyThread myThread = new MyThread(o);
myThread.start();
Thread.sleep(1000);
synchronized (o){
System.out.println("main函数进入同步代码块");
o.notify();//从那些因为o对象而进入无限期等待的线程中,通知一个 并执行完该线程
System.out.println("main函数退出同步代码块");
}
}
}
class MyThread extends Thread{
Object obj;
public MyThread(Object obj) {
this.obj = obj;
}
@Override
public void run() {
System.out.println("线程启动");
synchronized (obj){
System.out.println("进入同步代码块");
//主动释放obj锁标记,无限期等待
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("退出同步代码块");
}
System.out.println("线程结束");
}
}
打印结果:
因为此时主线程对o对象加锁并调用了notify()
方法,因此MyThread线程停止了无限期等待,进而执行完毕,那如果多个线程呢?主线程该通知谁呢?
public class TestWaitNotify {
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
MyThread myThread1 = new MyThread(o);
MyThread myThread2 = new MyThread(o);
myThread1.start();
myThread2.start();
Thread.sleep(1000);
synchronized (o){
System.out.println("main函数进入同步代码块");
o.notify();//从那些因为o对象而进入无限期等待的线程中,通知一个 并执行完该线程
System.out.println("main函数退出同步代码块");
}
}
}
class MyThread extends Thread{
Object obj;
public MyThread(Object obj) {
this.obj = obj;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"线程启动");
synchronized (obj){
System.out.println(Thread.currentThread().getName()+"进入同步代码块");
//主动释放obj锁标记,无限期等待
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"退出同步代码块");
}
System.out.println(Thread.currentThread().getName()+"线程结束");
}
}
显然主线程将锁标记拿出来之后,通知其中一个线程,因此谁拿到锁标记,谁将会执行完毕,而此时,名字为“THread-1”的线程,因为没有拿到主线程的锁锁标记,将陷入无限期等待。
如果将主函数中的同步代码块中的notify()
方法改为notifyAll()
方法呢?
synchronized (o){
System.out.println("main函数进入同步代码块");
o.notifyAll();
System.out.println("main函数退出同步代码块");
}
打印结果:
代码执行完毕
但是,此时Thread-0
和Thread-1
,谁先执行完毕取决于锁标记的竞争结果,谁竞争到锁标记,谁将会优先执行,上述打印结果,显然Thread-1
优先拿到锁标记,然后Thread-0
等待Thread-1
只想完毕后释放锁标记,然后本线程实行完毕;