1. 程式人生 > 其它 >執行緒的基礎

執行緒的基礎

安全是多執行緒程式設計的核心主題,但並不是只要使用多執行緒就一定會引發安全問題。要了解哪些操作是安全的,哪些是不安全的,就必須先掌握如何使用多執行緒。不過在操作多執行緒之前,我們先了解一下多執行緒的幾種狀態。

執行緒的狀態

在Thread的實現中,包含一個名為State的enum類,用來標識執行緒執行中的各種狀態,其中定義了以下幾個型別:

publicenumState{
/**
*Threadstateforathreadwhichhasnotyetstarted.
*/
NEW,

/**
*Threadstateforarunnablethread.Athreadintherunnable
*stateisexecutingintheJavavirtualmachinebutitmay
*bewaitingforotherresourcesfromtheoperatingsystem
*suchasprocessor.
*/
RUNNABLE,

/**
*Threadstateforathreadblockedwaitingforamonitorlock.
*Athreadintheblockedstateiswaitingforamonitorlock
*toenterasynchronizedblock/methodor
*reenterasynchronizedblock/methodaftercalling
*{@linkObject#wait()Object.wait}.
*/
BLOCKED,

/**
*Threadstateforawaitingthread.
*Athreadisinthewaitingstateduetocallingoneofthe
*followingmethods:
*<ul>
*<li>{@linkObject#wait()Object.wait}withnotimeout</li>
*<li>{@link#join()Thread.join}withnotimeout</li>
*<li>{@linkLockSupport#park()LockSupport.park}</li>
*</ul>
*
*<p>Athreadinthewaitingstateiswaitingforanotherthreadto
*performaparticularaction.
*
*...
*/
WAITING,

/**
*Threadstateforawaitingthreadwithaspecifiedwaitingtime.
*Athreadisinthetimedwaitingstateduetocallingoneof
*thefollowingmethodswithaspecifiedpositivewaitingtime:
*<ul>
*<li>{@link#sleepThread.sleep}</li>
*<li>{@linkObject#wait(long)Object.wait}withtimeout</li>
*<li>{@link#join(long)Thread.join}withtimeout</li>
*<li>{@linkLockSupport#parkNanosLockSupport.parkNanos}</li>
*<li>{@linkLockSupport#parkUntilLockSupport.parkUntil}</li>
*</ul>
*/
TIMED_WAITING,

/**
*Threadstateforaterminatedthread.
*Thethreadhascompletedexecution.
*/
TERMINATED;
}

首先,NEWTERMINATED是兩種特殊的狀態,前者表示執行緒還未開始執行,後者表示執行緒已經執行完畢。在這兩種狀態下,對執行緒進行一些操作是沒有意義的,因為執行緒根本沒有執行,也就不會去響應中斷、睡眠等請求了。

當執行了start方法之後或者執行緒正在執行時,執行緒會進入RUNNABLE狀態,這時執行緒或者正在執行,或者正在等待CPU排程。

BLOCKED表示執行緒正在等待鎖,也就是說此時執行緒要執行的程式碼是同步的,有其他執行緒正在執行此程式碼,所以執行緒需要等待獲取鎖。

WAITINGTIMED_WAITING都表示執行緒要等待一段時間之後再執行,只是後者會有一個超時處理。

執行緒的執行就是在以上這些狀態中不斷切換,當然NEW

TERMINATED這兩種表示執行緒起止的狀態,線上程的生命週期中只會執行一次。

建立執行緒

在Java中建立一個執行緒有兩種方式:繼承Thread和實現Runnable。其實這兩種方式並沒有很大的差別,只是Java僅支援單繼承,實現Runnable的方式更靈活一些,但是一個Runnable物件本身是無法執行的,需要用一個Thread物件來幫助它啟動,就像這樣:

newThread(newMyRunnable()).start();

啟動執行緒

執行緒的啟動要使用start方法,而不是呼叫Runnable的run方法,不過如果多次呼叫start方法會丟擲異常:

publicsynchronizedvoidstart(){
/**
*Thismethodisnotinvokedforthemainmethodthreador"system"
*groupthreadscreated/setupbytheVM.Anynewfunctionalityadded
*tothismethodinthefuturemayhavetoalsobeaddedtotheVM.
*
*Azerostatusvaluecorrespondstostate"NEW".
*/
if(threadStatus!=)
thrownewIllegalThreadStateException();

/*Notifythegroupthatthisthreadisabouttobestarted
*sothatitcanbeaddedtothegroup'slistofthreads
*andthegroup'sunstartedcountcanbedecremented.*/
group.add(this);

booleanstarted=false;
try{
start0();
started=true;
}finally{
//...
}
}

通過判斷threadStatus的值來確保執行緒僅被啟動一次,threadStatus對應一個列舉的執行緒狀態,前面已經分析過它。

呼叫start方法之後並不是說執行緒馬上就開始運行了,因為CPU可能處於忙碌中,沒有多餘的時間片,因此start方法只是把Thread的狀態變為RUNNABLE,等待CPU排程。

執行緒休眠

如果要讓執行緒停止一段時間再繼續執行,可以使用sleep(long millis)方法,sleep會讓當前執行緒進入TIMED_WAITING狀態,經過millis時間之後執行緒會自動甦醒,並重新進入RUNNABLE狀態等待系統排程。

yield放棄時間片

當一個執行緒獲得CPU時間片之後,可以通過呼叫yield方法放棄所獲得的時間片,並重新進入RUNNABLE狀態等待系統排程。和sleep不同之處在於,我們無法知道yield方法呼叫後執行緒會等待多久,因為它和其他所有的執行緒一樣處於RUNNABLE狀態,那麼CPU就可能在任何時候排程它,也可能永遠不會排程它。

中斷停止執行緒

通過start方式可以啟動執行緒,我們很容易就會想到使用stop方法來停止,然而stop方法已經過時了,如下:

@Deprecated
publicfinalvoidstop(){
//...
}

在說明如何正確的停止執行緒之前,我們先說明一下為什麼stop方法會過時。stop會釋放持有的鎖,以使資料可以被其他執行緒訪問,我們知道計算機解決任何問題都不是一蹴而就的,完成一個任務需要很多步驟,每一步都會對結果產生一定的影響,而如果讓執行緒在某一步直接停止,就很可能得到一個不完整的資料。例如給一個二手買號地圖使用者依次設定姓名和暱稱,如果stop正好發生在設定姓名和設定暱稱之間,我們得到的使用者資訊就不再完整。

正確的停止執行緒方法是使用中斷。中斷不是說會立即打斷執行緒的執行,而是給執行緒傳送一箇中斷的訊號,由執行緒來決定何時響應,這樣我們就有了足夠的時間對資料進行清理。

既然有傳送訊號,那就一定有辦法判斷是否接收到了中斷訊號,Thread中有兩種方式來判斷是否中斷:

publicstaticbooleaninterrupted(){
returncurrentThread().isInterrupted(true);
}

publicbooleanisInterrupted(){
returnisInterrupted(false);
}

這兩種方式最終都是呼叫了一個native方法實現的:

/**
*TestsifsomeThreadhasbeeninterrupted.Theinterruptedstate
*isresetornotbasedonthevalueofClearInterruptedthatis
*passed.
*/
privatenativebooleanisInterrupted(booleanClearInterrupted);

可以看到,兩種方式的區別在於,interrupted是判斷當前的執行緒是否中斷,並且會清除中斷標記;而isInterrupted是判斷呼叫此方法的Thread物件是否中斷,並且不會清除中斷標記。我們要區別執行緒物件和當前執行緒的區別,前者表示的就是某個執行緒,而當前執行緒表示的是執行的某段程式碼所處的執行緒,例如,在main執行緒中,執行以下兩處程式碼時,當前執行緒currentThread值是不同的:

publicclassMyThreadextendsThread{
publicMyThread(){
System.out.println("MyThreadconstructor:"+Thread.currentThread().getName());
}

@Override
publicvoidrun(){
super.run();
System.out.println("MyThreadrun:"+Thread.currentThread().getName());
}
}

在主執行緒中,呼叫該執行緒的start方法,可以得到以下的輸出結果:

MyThreadconstructor:main
MyThreadrun:Thread-

也就是說,呼叫的程式碼是在哪個執行緒中執行的,Thread.currentThread的值就是哪個執行緒。明白了這個區別,我們就知道了interruptedisInterrupted兩個方法在何時有區別了。

前面說過,中斷只是給執行緒傳送了一個訊號,至於如何響應還是由執行緒決定,可以不理會中斷的訊號,也可以根據中斷的訊號做一些資料的處理之後再結束掉當前的執行緒。

不同狀態下的執行緒對中斷的響應方式也有區別,NEWTERMINATED肯定是不會響應的,RUNNABLEBLOCKED則是會接收到中斷訊號,而WAITINGTIMED_WAITING則是會丟擲InterruptedException,並且會清除中斷標記,這從 sleep 和 wait 函式的定義中就可以看出:

//Thread.java
/**
*...
*@throwsInterruptedExceptionifanythreadinterruptedthe
*currentthreadbeforeorwhilethecurrentthread
*waswaitingforanotification.The<i>interrupted
*status</i>ofthecurrentthreadisclearedwhen
*thisexceptionisthrown.
*...
*/
publicstaticnativevoidsleep(longmillis)throwsInterruptedException;
//Object.java
publicfinalnativevoidwait(longtimeout)throwsInterruptedException;

瞭解了中斷的概念,中斷一個執行緒就簡單多了。例如在RUNNABLE狀態下,只需要判斷是否接收到了中斷訊號,就可以在合適的時間中斷執行緒。

publicclassWorkerextendsThread{
publicvoidrun(){
System.out.println("Workerstarted.");
while(!isInterrupted()){
System.out.println("doingsomething.");
}
System.out.println("Workerstopped.");
}
}

然後,給執行緒傳送一箇中斷訊號:

Workerthread=newWorker();
thread.start();
//讓執行緒執行起來
Thread.sleep(10);
thread.interrupt();

就可以得到以下的輸出:

Workerstarted.
doingsomething.
doingsomething.
doingsomething.
...
Workerstopped.

如果執行緒有睡眠等行為時,就可以利用中斷異常來停止執行緒,修改Worker執行緒的run方法如下:

publicclassWorkerextendsThread{
publicvoidrun(){
System.out.println("Workerstarted.");
try{
Thread.sleep(100);
}catch(InterruptedExceptione){
e.printStackTrace();
System.out.println("WorkerInterrupted.");
}
System.out.println("Workerstopped.");
}
}

可以看到如下輸出:

Workerstarted.
java.lang.InterruptedException:sleepinterrupted
atjava.lang.Thread.sleep(NativeMethod)
atchapter01.section1.Interrupt$Worker.run(Interrupt.java:42)
WorkerInterrupted.
Workerstopped.

執行緒從睡眠中被打斷,並立即結束。

總結

瞭解執行緒的基本概念,是學習執行緒的第一步。但是至此我們只是學會了如何使用一個執行緒,接下來我們繼續研究多個執行緒互動時,如何處理資料安全的問題。