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

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章