聊聊執行緒阻塞與恢復 LockSupport類
執行緒阻塞的實現方式
最近在學習Java併發包裡面各種鎖的實現以及AQS框架時,瞭解到執行緒阻塞都是通過LockSupport
這樣一個類來實現的。查閱了相關資料發現早期的執行緒阻塞方式是直接藉助Thread
類的suspend
方法來完成。然而,現在使用suspend
以及resume
方法都會被提醒方法已過期,且容易導致死鎖。下面就裡面的原因進行簡單的探究。
一個Suspend方法導致死鎖的經典案例
public class SuspendTest {
public static void main(String args[]) throws InterruptedException {
Thread t = new Thread(new MyTask());
t.start();
Thread.sleep(100);
t.suspend();
System.out.println("resuming");
t.resume();
Thread.sleep(100);
t.interrupt();
}
public static class MyTask implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted()) {
count++;
System.out.println("looping: " + count);
}
}
}
複製程式碼
上述程式碼在執行的過程中很有可能會在System.out.println("resuming")
這句語句處進入死鎖狀態。這是因為println
方法是一個同步方法,在執行t.suspend()
println
方法。從而導致主執行緒獲取不到阻塞的t執行緒佔據的鎖,進而發生死鎖。
使用suspend
方法帶來的不確定性正是因為執行緒t的阻塞是由外界控制的,也就意味t阻塞的時候執行的程式碼位置、資料狀態、鎖的資訊都是不能確定的。
public static class MyTask implements Runnable {
@Override
public void run() {
boolean state = true;
int count = 0;
while (state && !Thread.currentThread().isInterrupted()) {
count++;
System.out.println("looping: " + count);
try {
Thread.sleep(10);
} catch (InterruptedException ex) {
System.out.println(ex);
state = false;
}
}
}
}
複製程式碼
我們給任務執行緒t的println
方法後面加上一段睡眠時間,這樣t被阻塞時可能正在列印內容,也有可能在睡眠。主執行緒的println
方法就有可能獲取到鎖,從而順利執行下去。改變睡眠的時間,程式是否會發生死鎖的概率也會改變。
LockSupport類
我們可以使用LockSupport
類來實現執行緒的阻塞與恢復,相關的方法是park
與unpark
。
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
複製程式碼
park
方法可以接收一個阻塞塊物件(許可),並關聯到阻塞的執行緒上。unpark
的引數為需要解除阻塞的執行緒。我們可以看到LockSupport
只能夠阻塞當前執行緒,也就意味著執行緒在何處進入阻塞狀態是由自己決定的。下面通過這種方式實現前面的列印程式。
public class SuspendTest {
public static void main(String args[]) throws InterruptedException {
Thread t = new Thread(new MyTask());
t.start();
Thread.sleep(100);
System.out.println("resuming");
LockSupport.unlock(t);
Thread.sleep(100);
t.interrupt();
}
public static class MyTask implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted()) {
count++;
System.out.println("looping: " + count);
LockSupport.lock(this);
}
}
}
複製程式碼
上述程式碼沒有死鎖問題,t執行緒進入阻塞狀態是在println
執行之後。相比suspend
方式缺少一定的靈活性,但是執行緒狀態更加具有確定性。執行緒沒有模糊狀態,那麼死鎖的發生就可能是由程式本身的設計帶來的,比如我們線上程釋放相關鎖之前執行LockSupport.lock()
進入阻塞狀態,如下所示,那麼死鎖就一定會發生。
public class SuspendTest {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String args[]) throws InterruptedException {
Thread t = new Thread(new MyTask());
t.start();
Thread.sleep(100);
lock.lock();
System.out.println("resuming");
lock.unlock();
LockSupport.unlock(t);
Thread.sleep(100);
t.interrupt();
}
public static class MyTask implements Runnable {
@Override
public void run() {
int count = 0;
while (!Thread.currentThread().isInterrupted()) {
count++;
lock.lock();
System.out.println("looping: " + count);
LockSupport.lock(this);
lock.unlock();
}
}
}
複製程式碼