一分钟用睡前小故事图解Java线程之死锁

Java线程之死锁

      • 深入理解Java线程安全——银行欠我400万!!!
      • 线程执行的六大状态
  • 一、故事引入
  • 二、死锁
    • 死锁的产生
  • 三、线程通信
    • 等待
    • 通知
  • 四、代码案例
    • 线程释放锁标记
    • 主线程通知MyThread线程
    • 主线程通知哪个线程?

为了更好的理解死锁,读者可能需要了解线程安全相关的知识,主要是对线程执行的六大状态有一定的了解,尤其是阻塞状态,这是死锁发生的关键。进而才能对死锁的过程深入理解。这里附上一篇读前分享链接,当然,你也可以直接跳过。也是可以读懂这篇故事型文章的。

  • 深入理解Java线程安全——银行欠我400万!!!

线程执行的六大状态

一分钟用睡前小故事图解Java线程之死锁_第1张图片


一、故事引入

 有一对夫妻,拥有一辆小汽车(可能时劳斯莱斯);同时两人婚前约定,只能用一个钱包(请不要争执电子支付,他们不用),且每次只能由一个带在身上;
 有一天,丈夫准备去和朋友一起去吃饭,需要拿上车钥匙和钱包,妻子想去商场购物,也许要拿上钥匙和钱包;
一分钟用睡前小故事图解Java线程之死锁_第2张图片

类比如下:
一分钟用睡前小故事图解Java线程之死锁_第3张图片

每个对象都有一个互斥标记锁,用来分配给线程的。只有拥有对象互斥锁标记的线程才能进入该对象加锁的同步代码块。线程退出同步代码块时会释放相应的互斥锁标记。

 夫妻双方都有自己的事情去做,这个过程类比的是两个线程的执行;两人如果需要完成自己想做的事情,就必须同时拿到钱包和车钥匙,这里类比的是多个线程访问临界资源,而每个临界资源对象都有且只有一个锁标记,只有拥有锁标记的线程才可以拥有这个对象;

  •  情景一:丈夫顺利的拿到车钥匙和钱包完成了自己的事情,然后把钱包和车钥匙还给妻子;

类比:两个线程,线程1首先拿到OS分配的时间片,然后访问两个临界资源对象并顺利拿到两个锁标记,且在时间片未到期之前执行完毕所有事情,然后释放锁标记,线程2进而继续执行;

  •  情景二:丈夫顺利的拿到钱包,但是这时候突然想去厕所,而在这个时间里,妻子为了去购物,把车钥匙拿走了,但是车开出去之后发现没有钱包,因此等待钱包归来;丈夫上完厕所,发现车钥匙不在了,因此,在家等待车钥匙,两个人无穷尽的等了下去。(你可能会说,打个电话问一下啊!别着急,先不打)

类比:线程1首先拿到OS分配的时间片,然后访问临界资源对象 钱包,但是在拿到该对象的锁标记,准备拿另一个临界资源对象的锁标记的时候,时间片到期了,因为陷入了限期等待状态。而在线程1限期而等待期间,线程2拿到了OS分配的时间片,并拿到了临界资源对象 车的锁标记,因为没有找到 钱包对象的锁标记,因此进入阻塞状态,等待其他线程释放锁标记;此时线程1限期等待结束,并再一次拿到OS分配的时间片,可因为拿不到 车对象的锁标记,因此也陷入了阻塞状态,两个线程因此都陷入了阻塞状态。

二、死锁

一分钟用睡前小故事图解Java线程之死锁_第4张图片

 很显然,两个线程各自拿到一个临界资源对象的锁标记,并且都在阻塞状态等待对方释放锁标记,互相都不愿意释放锁标记,因为陷入了无限期的僵持状态,这就是死锁;

死锁的产生

  • 当第一个线程拥有A对象锁标记,并且等待B对象的锁标记,同时第二个线程拥有B对象锁标记,并且等待A对象锁标记时,产生死锁。
  • 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。

三、线程通信

 夫妻间产生争执时非常正常的事情,但是不能一直僵持,僵持就会陷入“死锁”,一定要沟通交流才能解决完问题,因此,在双方对需要对方的东西去做自己想做的事情时,要去协商,可以把钱包给妻子,让她先去购物,之后妻子完成自己的事情再把车和钥匙交给丈夫,进而解决问题,这就需要“通信”。

  •  情景三:由于两人互相僵持不下,终于还是进行了电话沟通,最终协商结果为先让妻子去购物,丈夫在家先等待,直到妻子购物回来,把车钥匙和钱包交出来并通知丈夫。如果说妻子执行完自己的事情直接把钱包和车钥匙放回原处,而没有通知丈夫,丈夫依然不知道妻子已经回来了,还会继续陷入阻塞状态,只有妻子把钱包和车钥匙亲手交给丈夫,丈夫才能做自己的事情,或者妻子把钱包和钥匙放回原处,让保姆或者其他人拿着去通知丈夫才可以。

类比:线程1将自己所持有的临界资源对象(钱包对象)的锁标记用过调用 obj.wait()释放后,自己进入等待队列,而此时,线程2因为拿到了线程1中的锁标记,因此进入就绪状态开始执行。当线程2执行完毕后,将锁标记释放回临界资源对象,但是此时因为线程1处于阻塞状态,已经无法主动访问临界资源对象拿到相应的锁标记,因此需要其他线程(可以是任意拥有线程1所需锁标记的线程)来通知(notify()方法)该线程,进而线程1才可以执行。

等待

  • public final void wait()
  • public final void wait(long timeout)
  • 必须在对obj加锁的同步代码块中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。因此此线程阻塞在obj的等待队列中。释放锁,进入等待队列。
synchronized (临界资源对象){ //对临界资源加锁
	//同步代码块(原子操作)
}

通知

  • public final void notify()
  • public final void notufyAll()
  • 必须在对obj加锁的同步代码块中。从objwaiting中释放一个或全部线程。对自身没有任何影响。

四、代码案例

 因为线程拿到时间片时随机的,因此无法用代码在再现夫妻出行的例子,该示例主要是帮助读者理解死锁的产生;这里几个代码案例主要是帮助理解线程通信的几个方法。

线程释放锁标记

 现有一个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("线程结束");
    }
}

打印结果:
一分钟用睡前小故事图解Java线程之死锁_第5张图片
 因此当某个线程释放锁标记后,必须有另一个线程来通知该线程,否则该线程将一直处于阻塞状态。

主线程通知MyThread线程

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("线程结束");
    }
}

打印结果:
一分钟用睡前小故事图解Java线程之死锁_第6张图片
 因为此时主线程对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”的线程,因为没有拿到主线程的锁锁标记,将陷入无限期等待。
一分钟用睡前小故事图解Java线程之死锁_第7张图片
如果将主函数中的同步代码块中的notify()方法改为notifyAll()方法呢?

        synchronized (o){
            System.out.println("main函数进入同步代码块");
            o.notifyAll();
            System.out.println("main函数退出同步代码块");
        }

打印结果:
代码执行完毕
一分钟用睡前小故事图解Java线程之死锁_第8张图片
 但是,此时Thread-0Thread-1,谁先执行完毕取决于锁标记的竞争结果,谁竞争到锁标记,谁将会优先执行,上述打印结果,显然Thread-1优先拿到锁标记,然后Thread-0等待Thread-1只想完毕后释放锁标记,然后本线程实行完毕;

你可能感兴趣的:(JAVA学习)