Java多线程-死锁,活锁,饥饿详解

什么是死锁?

定义:如果两个或者更多的线程因相互等待对方而被永远暂停(线程的生命周期状态为BLOCKED或者WAITING),那么我们就称这些线程产生了死锁.

由于产生死锁的线程的生命周期状态永远是非运行状态,因此这些线程索要执行的任务也永远无法进展.

通俗的说:当线程A持有独占锁a,并尝试去获取独占锁 b 的同时,线程 B 持有独占锁 b,并尝试获取独占锁a的情况下,而A和B释放其持有的锁的前提又都是先获得对方持有的另一个锁,因此这两个线程最终都无法获得它们申请的另一个锁,最终两个线程都处于无限等待的状态,即产生了死锁.

产生死锁的必要条件

产生死锁的四个必要条件:

(1)资源互斥:涉及的资源必须是独占的,即每个资源一次只能够被一个线程使用.若其它线程访问该资源,只能等待,直至占有该资源的线程使用完成后释放该资源;

(2)占用并等待资源:线程获得一定的资源之后,又对其它资源发出请求,但是该资源可能被其它线程占有,此时请求阻塞,但又对自己获得的资源保持不放;

(3)资源不可剥夺:是指线程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放;

(4)循环等待资源:涉及的线程必须在等待别的线程持有的资源,而这些线程又反过来在等待第一个线程所持有的资源,即若干线程之间形成一种头尾相接的循环等待资源关系.

这些条件是死锁产生的必要条件而非充分条件,这就是说只要产生了死锁,那么上面这些条件一定同时成立,但是上述条件即使同时成立,也不一定就能产生死锁.

如何避免和处理死锁?

处理死锁的基本方法:

我们只要破坏产生死锁的四个条件中的其中一个就可以了.

由于锁具有排他性并且锁只能够由其持有线程主动四方,因此由锁导致的死锁只能够从消除"占用并等待资源"和"循环等待资源"这两个方向入手.

方法一:粗锁法

使用粗粒度的锁代替多个锁,这样涉及的线程都只需要申请一个锁从而避免了死锁.

粗锁法的缺点是它明显降低了并发性并可能导致资源浪费.

方法二:锁排序法

相关线程使用全局统一的顺序申请锁.假设有多个线程需要申请资源(锁),那么我们只需要让这些线程按照一个全局统一的顺序去申请锁,就可以消除"循环等待资源"这个条件,从而规避死锁.

方法三:使用ReentrantLock.tryLock()申请锁

tryLock方法允许我们为锁申请这个操作指定一个超时时间.在超时时间内,如果相应的锁申请成功,方法返回true.如果方法在执行的过程中相应的锁被其他线程持有,该方法会使线程暂停,直到锁申请成功或者等待时间超过指定的超时时间(方法返回false).

使用该方法来申请锁可以避免一个线程无限制地等待另外一个线程持有的资源,从而最终能够消除死锁产生的必要条件中的"占用并等待资源"

死锁的例子

死锁实例

public class DeadLockDemo {
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

输出
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过 Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程A和线程B休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。

我们对线程 2 的代码修改成下面这样就不会产生死锁了。

        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 2").start();
        
输出
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2

Process finished with exit code 0

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

什么是活锁

活锁:线程一直处于运行状态,但是其任务却一直无法进展的一种活性故障.

活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。

什么是线程饥饿

饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。

Java 中导致饥饿的原因:

  1. 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问,比如使用了非公平锁.
  3. 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法),因为其他线程总是被持续地获得唤醒。

什么是无锁

即没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。如果有多个线程修改同一个值必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功无锁(比如JDK的CAS)

发布了24 篇原创文章 · 获赞 8 · 访问量 928

猜你喜欢

转载自blog.csdn.net/kaihuishang666/article/details/103896421