Java筑基——多线程(什么是死锁以及如何避免死锁)

1. 前言

本文会从一个小例子开始,介绍什么是死锁,再针对例子,说明如何避免死锁,最后会介绍一些死锁的理论化知识。

2. 正文

2.1 死锁的小例子

class TaskA implements Runnable {

    @Override
    public void run() {
        while (true) {
            synchronized (DeadLockDemo.lock1) {
                System.out.println(Thread.currentThread().getName() + " holds lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " waits for lock2");
                synchronized (DeadLockDemo.lock2) {
                    System.out.println(Thread.currentThread().getName() + " hold2 lock2");
                }
            }
        }
    }
}

class TaskB implements Runnable {

    @Override
    public void run() {
        while (true) {
            synchronized (DeadLockDemo.lock2) {
                System.out.println(Thread.currentThread().getName() + " holds lock2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                    System.out.println(Thread.currentThread().getName() + " waits for lock1");
                synchronized (DeadLockDemo.lock1) {
                    System.out.println(Thread.currentThread().getName() + " holds lock1");
                }
            }
        }
    }
}
public class DeadLockDemo {
    public static Object lock1 = new Object();
    public static Object lock2 = new Object();

    public static void main(String[] args) {
        Thread threadA = new Thread(new TaskA(), "ThreadA");
        Thread threadB = new Thread(new TaskB(), "ThreadB");

        threadA.start();
        threadB.start();
    }
}

运行这个小例子,会有四种打印结果:
第一种:

ThreadA holds lock1
ThreadB holds lock2
ThreadB waits for lock1
ThreadA waits for lock2

第二种:

ThreadA holds lock1
ThreadB holds lock2
ThreadA waits for lock2
ThreadB waits for lock1

第三种:

ThreadB holds lock2
ThreadA holds lock1
ThreadB waits for lock1
ThreadA waits for lock2

第四种:

ThreadB holds lock2
ThreadA holds lock1
ThreadA waits for lock2
ThreadB waits for lock1

我们只按第一种打印来分析这个小例子。

ThreadA holds lock1
ThreadB holds lock2
ThreadB waits for lock1
ThreadA waits for lock2

我们先看一下程序代码,再去分析打印结果。

TaskATaskB 分别是两个任务:在 TaskArun() 方法中,先获取锁 lock1,休眠 100 ms 后,再去获取锁 lock2;在 TaskBrun() 方法中,先获取锁 lock2,休眠 100 ms 后,再去获取锁 lock1

main() 方法中,开启 ThreadAThreadB 两个线程,分别执行 TaskATaskB 这两个任务。

从打印结果可以看到,

ThreadA 先获取了锁 lock1,接着 ThreadB 获取了锁 lock2

ThreadB 打算获取锁 lock1,但是 lock1ThreadA 持有着不释放,所以 ThreadB 此时因无法获得锁 lock1 而处于阻塞状态;

ThreadA 打算获取锁 lock2,但是 lock2ThreadB 持有着不释放,所以 ThreadA 此时因无法获得锁 lock2 而处于阻塞状态。

到这里,ThreadBThreadA 都处于阻塞状态,因为它们为了获取彼此持有的锁而不得。这种情况就造成了死锁。

2.2 数据库的死锁

一个数据库事务可能由多条SQL更新请求组成。当在一个事务中更新一条记录,这条记录就会被锁住避免其他事务的更新请求,直到第一个事务结束。同一个事务中每一个更新请求都可能会锁住一些记录。

当多个事务同时需要对一些相同的记录做更新操作时,就很可能发生死锁。

Transaction 1, request 1, locks record 1 for update
Transaction 2, request 1, locks record 2 for update
Transaction 1, request 2, tries to lock record 2 for update.
Transaction 2, request 2, tries to lock record 1 for update.

因为锁发生在不同的请求中,并且对于一个事务来说不可能提前知道所有它需要的锁,因此很难检测和避免数据库事务中的死锁。

2.3 对死锁的描述

死锁是两个或更多线程互相持有对方所需要的资源,导致这些线程处于阻塞状态,无法执行的情况。

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

互斥条件:进程使用所分配到的资源时具有排他性,也就是说,在一段时间内只能由获取资源的进程占用该资源;如果在这段时间内,有其他进程请求资源,则请求方只能等待占有资源的进程释放资源。

请求和保持条件:进程已经保持至少一个资源,但又提出了新的资源请求,而新的资源已被其他进程占有,此时请求进程阻塞;但请求进程又对自己已获得的资源保持不释放。

不剥夺条件:进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完毕后由进程自己释放。

环路等待条件:在发生死锁时,必然存在一个进程——资源的环形链,即进程集合 {P0, P1,……,Pn} 中的 P0 正在等待 P1 占用的资源,P1 正在等待 P2 占用的资源,……,Pn正在等待已被 P0 占用的资源。这个条件也要求在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)。

在 2.1 中的死锁小例子满足这 4 个必要条件:

ThreadA 获得了锁 lock1,在没有释放之前 ThreadB 不能使用,所以满足互斥条件。

ThreadA 已经获得了锁 lock1,又去请求获取锁 lock2,而锁 lock2ThreadB 所持有, ThreadA 就处于阻塞状态,这时 ThreadA 在请求锁 lock2,而 ThreadB 又保持锁 lock2 不放,所以满足请求和保持条件。

ThreadB 获取了锁 lock2,只能等 ThreadB 自行释放,而不会被剥夺,所以满足不可剥夺条件。

ThreadA 在等待获取锁 lock2ThreadB 在等待获取锁 lock1,这符合环路等待条件。

2.4 避免死锁

这部分会采取一些办法,针对 2.1 中的死锁小例子,避免死锁。

死锁的 4 个必要条件,只要有一个不满足,就可以避免死锁。

互斥条件,不可剥夺条件,这两个不可以打破,因为这是保证线程同步的条件。

打破保持和请求条件

首先,把 2.1 中的小例子改成使用显式锁 ReentrantLock 的形式,这种形式的效果和 synchronized 内置锁是一样的。

class TaskA implements Runnable {

    @Override
    public void run() {
        while (true) {
            DeadLockDemo.lock1.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " holds lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " waits for lock2");
                DeadLockDemo.lock2.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " hold2 lock2");
                } finally {
                    DeadLockDemo.lock2.unlock();
                }
            } finally {
                DeadLockDemo.lock1.unlock();
            }
        }
    }
}

class TaskB implements Runnable {

    @Override
    public void run() {
        while (true) {
            DeadLockDemo.lock2.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " holds lock2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " waits for lock1");
                DeadLockDemo.lock1.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " holds lock1");
                } finally {
                    DeadLockDemo.lock2.unlock();
                }
            } finally {
                DeadLockDemo.lock2.unlock();
            }
        }
    }
}

public class DeadLockDemo {
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();

    public static void main(String[] args) {
        Thread threadA = new Thread(new TaskA(), "ThreadA");
        Thread threadB = new Thread(new TaskB(), "ThreadB");

        threadA.start();
        threadB.start();
    }
}

其次,使用 ReentrantLocktryLock() 方法:仅在调用时锁未被另一个线程保持的情况下,才获取该锁。

比如现在 ThreadA 获取了锁 lock1ThreadB 获取了锁 lock2,而 ThreadB 调用 lock1.tryLock() 尝试获取锁 lock1,此时锁 lock1ThreadA 保持,lock1.tryLock() 返回 falseThreadB 就继续执行代码,释放自己持有的锁 lock2。而不是像之前那样,因没有获取锁 lock1,而处于阻塞状态。

下面是代码实现:

class TaskA implements Runnable {

    @Override
    public void run() {
        while (true) {
            if (DeadLockDemo.lock1.tryLock()) {
                try {
                    System.out.println(Thread.currentThread().getName() + " holds lock1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " waits for lock2");
                    if (DeadLockDemo.lock2.tryLock()) {
                        try {
                            System.out.println(Thread.currentThread().getName() + " hold2 lock2");
                        } finally {
                            DeadLockDemo.lock2.unlock();
                        }
                    }
                } finally {
                    DeadLockDemo.lock1.unlock();
                }
            }
        }
    }
}

class TaskB implements Runnable {

    @Override
    public void run() {
        while (true) {
            if (DeadLockDemo.lock2.tryLock()) {
                try {
                    System.out.println(Thread.currentThread().getName() + " holds lock2");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " waits for lock1");
                    if (DeadLockDemo.lock1.tryLock()) {
                        try {
                            System.out.println(Thread.currentThread().getName() + " holds lock1");
                        } finally {
                            DeadLockDemo.lock1.unlock();
                        }
                    }
                } finally {
                    DeadLockDemo.lock2.unlock();
                }
            }
        }
    }
}

运行程序,查看日志:

ThreadA holds lock1
ThreadB holds lock2
ThreadA waits for lock2
ThreadA holds lock1
ThreadB waits for lock1
ThreadB holds lock2
ThreadA waits for lock2
ThreadA holds lock1
ThreadB waits for lock1
ThreadB holds lock2
ThreadA waits for lock2
ThreadA holds lock1
ThreadB waits for lock1
ThreadB holds lock2

不会发生死锁,日志会一直打印。但是,大家如果在日志中搜索 ThreadA holds lock2,却找不到。这说明 ThreadA 没有获取到 lock2

这是为什么呢?

ThreadAThreadB 两个线程在尝试拿锁的过程中,发生线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放,这就造成了活锁

解决办法:每个线程休眠随机数,错误拿锁的时间。

在两个任务的 while 循环的最后一行添加代码:

try {
    Thread.sleep(new Random().nextInt(3));
} catch (InterruptedException e) {
    e.printStackTrace();
}

运行一下,看日志:

ThreadB holds lock2
ThreadA holds lock1
ThreadB waits for lock1
ThreadA waits for lock2
ThreadA hold2 lock2
ThreadA holds lock1
ThreadB holds lock2
ThreadB waits for lock1
ThreadA waits for lock2
ThreadA hold2 lock2
ThreadA holds lock1
ThreadB holds lock2

打破环路等待条件

保证所有的线程都按相同的顺序获取锁,避免死锁发生。

针对 2.1 的死锁小例子,修改 TaskB:

class TaskB implements Runnable {

    @Override
    public void run() {
        while (true) {
            synchronized (DeadLockDemo.lock1) {
                System.out.println(Thread.currentThread().getName() + " holds lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                    System.out.println(Thread.currentThread().getName() + " waits for lock2");
                synchronized (DeadLockDemo.lock2) {
                    System.out.println(Thread.currentThread().getName() + " holds lock2");
                }
            }
        }
    }
}

这是为了使 ThreadBThreadA 一样都按照先获取锁 lock1,再获取锁 lock2 的顺序来获取锁。

打印结果:

ThreadA holds lock1
ThreadA waits for lock2
ThreadA hold2 lock2
ThreadA holds lock1
ThreadA waits for lock2
ThreadA hold2 lock2
ThreadA holds lock1
ThreadA waits for lock2
ThreadA hold2 lock2
ThreadA holds lock1

参考

猜你喜欢

转载自blog.csdn.net/willway_wang/article/details/106133169