1. 程式人生 > >Java併發程式設計(3):執行緒掛起、恢復與終止的正確方法(含程式碼)

Java併發程式設計(3):執行緒掛起、恢復與終止的正確方法(含程式碼)

JAVA大資料中高階架構 2018-11-06 14:24:56
掛起和恢復執行緒
Thread 的API中包含兩個被淘汰的方法,它們用於臨時掛起和重啟某個執行緒,這些方法已經被淘汰,因為它們是不安全的,不穩定的。如果在不合適的時候掛起執行緒(比如,鎖定共享資源時),此時便可能會發生死鎖條件——其他執行緒在等待該執行緒釋放鎖,但該執行緒卻被掛起了,便會發生死鎖。另外,在長時間計算期間掛起執行緒也可能導致問題。

下面的程式碼演示了通過休眠來延緩執行,模擬長時間執行的情況,使執行緒更可能在不適當的時候被掛起:

public class DeprecatedSuspendResume extends Object implements Runnable{

//volatile關鍵字,表示該變數可能在被一個執行緒使用的同時,被另一個執行緒修改
private volatile int firstVal;
private volatile int secondVal;

//判斷二者是否相等
public boolean areValuesEqual(){
return ( firstVal == secondVal);
}

public void run() {
try{
firstVal = 0;
secondVal = 0;
workMethod();
}catch(InterruptedException x){
System.out.println("interrupted while in workMethod()");
}
}

private void workMethod() throws InterruptedException {
int val = 1;
while (true){
stepOne(val);
stepTwo(val);
val++;
Thread.sleep(200); //再次迴圈錢休眠200毫秒
}
}

//賦值後,休眠300毫秒,從而使執行緒有機會在stepOne操作和stepTwo操作之間被掛起
private void stepOne(int newVal) throws InterruptedException{
firstVal = newVal;
Thread.sleep(300); //模擬長時間執行的情況
}

private void stepTwo(int newVal){
secondVal = newVal;
}

public static void main(String[] args){
DeprecatedSuspendResume dsr = new DeprecatedSuspendResume();
Thread t = new Thread(dsr);
t.start();

//休眠1秒,讓其他執行緒有機會獲得執行
try {
Thread.sleep(1000);}
catch(InterruptedException x){}
for (int i = 0; i < 10; i++){
//掛起執行緒
t.suspend();
System.out.println("dsr.areValuesEqual()=" + dsr.areValuesEqual());
//恢復執行緒
t.resume();
try{
//執行緒隨機休眠0~2秒
Thread.sleep((long)(Math.random()*2000.0));
}catch(InterruptedException x){
//略
}
}
System.exit(0); //中斷應用程式
}
}
某次執行結果如下:

從areValuesEqual()返回的值有時為true,有時為false。以上程式碼中,在設定firstVal之後,但在設定secondVal之前,掛起新執行緒會產生麻煩,此時輸出的結果會為false(情況1),這段時間不適宜掛起執行緒,但因為執行緒不能控制何時呼叫它的suspend方法,所以這種情況是不可避免的。

當然,即使執行緒不被掛起(註釋掉掛起和恢復執行緒的兩行程式碼),如果在main執行緒中執行asr.areValuesEqual()進行比較時,恰逢stepOne操作執行完,而stepTwo操作還沒執行,那麼得到的結果同樣可能是false(情況2)。

下面我們給出不用上述兩個方法來實現執行緒掛起和恢復的策略——設定標誌位。通過該方法實現執行緒的掛起和恢復有一個很好的地方,就是可以線上程的指定位置實現執行緒的掛起和恢復,而不用擔心其不確定性。

對於上述程式碼的改進程式碼如下:

public class AlternateSuspendResume extends Object implements Runnable {

private volatile int firstVal;
private volatile int secondVal;
//增加標誌位,用來實現執行緒的掛起和恢復
private volatile boolean suspended;

public boolean areValuesEqual() {
return ( firstVal == secondVal );
}

public void run() {
try {
suspended = false;
firstVal = 0;
secondVal = 0;
workMethod();
} catch ( InterruptedException x ) {
System.out.println("interrupted while in workMethod()");
}
}

private void workMethod() throws InterruptedException {
int val = 1;

while ( true ) {
//僅當賢臣掛起時,才執行這行程式碼
waitWhileSuspended();

stepOne(val);
stepTwo(val);
val++;

//僅當執行緒掛起時,才執行這行程式碼
waitWhileSuspended();

Thread.sleep(200);
}
}

private void stepOne(int newVal)
throws InterruptedException {

firstVal = newVal;
Thread.sleep(300);
}

private void stepTwo(int newVal) {
secondVal = newVal;
}

public void suspendRequest() {
suspended = true;
}

public void resumeRequest() {
suspended = false;
}

private void waitWhileSuspended()
throws InterruptedException {

//這是一個“繁忙等待”技術的示例。
//它是非等待條件改變的最佳途徑,因為它會不斷請求處理器週期地執行檢查,
//更佳的技術是:使用Java的內建“通知-等待”機制
while ( suspended ) {
Thread.sleep(200);
}
}

public static void main(String[] args) {
AlternateSuspendResume asr =
new AlternateSuspendResume();

Thread t = new Thread(asr);
t.start();

//休眠1秒,讓其他執行緒有機會獲得執行
try { Thread.sleep(1000); }
catch ( InterruptedException x ) { }

for ( int i = 0; i < 10; i++ ) {
asr.suspendRequest();

//讓執行緒有機會注意到掛起請求
//注意:這裡休眠時間一定要大於
//stepOne操作對firstVal賦值後的休眠時間,即300ms,
//目的是為了防止在執行asr.areValuesEqual()進行比較時,
//恰逢stepOne操作執行完,而stepTwo操作還沒執行
try { Thread.sleep(350); }
catch ( InterruptedException x ) { }

System.out.println("dsr.areValuesEqual()=" +
asr.areValuesEqual());

asr.resumeRequest();

try {
//執行緒隨機休眠0~2秒
Thread.sleep(
( long ) (Math.random() * 2000.0) );
} catch ( InterruptedException x ) {
//略
}
}

System.exit(0); //退出應用程式
}
}
執行結果如下:

由結果可以看出,輸出的所有結果均為true。首先,針對情況1(執行緒掛起的位置不確定),這裡確定了執行緒掛起的位置,不會出現執行緒在stepOne操作和stepTwo操作之間掛起的情況;針對情況2(main執行緒中執行asr.areValuesEqual()進行比較時,恰逢stepOne操作執行完,而stepTwo操作還沒執行),在發出掛起請求後,還沒有執行asr.areValuesEqual()操作前,讓main執行緒休眠450ms(>300ms),如果掛起請求發出時,新執行緒正執行到或即將執行到stepOne操作(如果在其前面的話,就會響應掛起請求,從而掛起執行緒),那麼在stepTwo操作執行前,main執行緒的休眠還沒結束,從而main執行緒休眠結束後執行asr.areValuesEqual()操作進行比較時,stepTwo操作已經執行完,因此也不會出現輸出結果為false的情況。

可以將ars.suspendRequest()程式碼後的sleep程式碼去掉,或將休眠時間改為200(明顯小於300即可)後,檢視執行結果,會發現結果中依然會有出現false的情況。如下圖所示:

總結:執行緒的掛起和恢復實現的正確方法是:通過設定標誌位,讓執行緒在安全的位置掛起

終止執行緒
當呼叫Thread的start()方法,執行完run()方法後,或在run()方法中return,執行緒便會自然消亡。另外Thread API中包含了一個stop()方法,可以突然終止執行緒。但它在JDK1.2後便被淘汰了,因為它可能導致資料物件的崩潰。一個問題是,當執行緒終止時,很少有機會執行清理工作;另一個問題是,當在某個執行緒上呼叫stop()方法時,執行緒釋放它當前持有的所有鎖,持有這些鎖必定有某種合適的理由——也許是阻止其他執行緒訪問尚未處於一致性狀態的資料,突然釋放鎖可能使某些物件中的資料處於不一致狀態,而且不會出現資料可能崩潰的任何警告。

終止執行緒的替代方法:同樣是使用標誌位,通過控制標誌位來終止執行緒。