本文目錄一覽:
- 1、深入理解wait–notify機制
- 2、如何使用java的鎖機制
- 3、java多線程,對象鎖是什麼概念?
- 4、如何在 Java 中正確使用 wait,notify 和 notifyAll
- 5、java的等待喚醒機制必須要讓線程等待嗎
深入理解wait–notify機制
前言
我們知道,java的wait/notify的通知機制可以用來實現線程間通信。wait表示線程的等待,調用該方法會導致線程阻塞,直至另一線程調用notify或notifyAll方法才可另其繼續執行。經典的生產者、消費者模式即是使用wait/notify機製得以完成。在這篇文章中,我們將深入解析這一機制,了解其背後的原理。
線程的狀態
在了解wait/notify機制前,先熟悉一下java線程的幾個生命周期。分別為初始(NEW)、運行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超時等待(TIMED_WAITING)、終止(TERMINATED)等狀態(位於java.lang.Thread.State枚舉類中)。
以下是對這幾個狀態的簡要說明,詳細說明見該類注釋。
對於以上線程間的狀態及轉化關係,我們需要知道
WAITING(等待狀態)和TIMED_WAITING(超時等待)都會令線程進入等待狀態,不同的是TIMED_WAITING會在超時後自行返回,而WAITING則需要等待至條件改變。
進入阻塞狀態的唯一前提是在等待獲取同步鎖。java注釋說的很明白,只有兩種情況可以使線程進入阻塞狀態:一是等待進入synchronized塊或方法,另一個是在調用wait()方法後重新進入synchronized塊或方法。下文會有詳細解釋。
Lock類對於鎖的實現不會令線程進入阻塞狀態,Lock底層調用LockSupport.park()方法,使線程進入的是等待狀態。
wait/notify用例
讓我們先通過一個示例解析
wait()方法可以使線程進入等待狀態,而notify()可以使等待的狀態喚醒。這樣的同步機制十分適合生產者、消費者模式:消費者消費某個資源,而生產者生產該資源。當該資源缺失時,消費者調用wait()方法進行自我阻塞,等待生產者的生產;生產者生產完畢後調用notify/notifyAll()喚醒消費者進行消費。
public class ThreadTest {
private static final Object obj = new Object();
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread produce = new Thread(new Produce(), “Produce”);
Thread consume = new Thread(new Consume(), “Consume”);
consume.start();
Thread.sleep(1000);
produce.start();
}
// 生產者線程
static class Produce implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println(“進入生產者線程”);
System.out.println(“生產”);
flag = true;
obj.notify();
System.out.println(“退出生產者線程”);
}
}
}
// 消費者線程
static class Consume implements Runnable {
@Override
public void run() {
synchronized (obj) {
System.out.println(“進入消費者線程”);
System.out.println(“wait flag 1:” + flag);
while (!flag) {
try {
System.out.println(“還沒生產,進入等待”);
obj.wait();
System.out.println(“結束等待”);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(“wait flag 2:” + flag);
System.out.println(“消費”);
System.out.println(“退出消費者線程”);
}
}
}
}
理解了輸出結果的順序,也就明白了wait/notify的基本用法。有以下幾點需要知道:
在示例中沒有體現但很重要的是,wait/notify方法的調用必須處在該對象的鎖(Monitor)中,也即,在調用這些方法時首先需要獲得該對象的鎖。否則會爬出IllegalMonitorStateException異常。
從輸出結果來看,在生產者調用notify()後,消費者並沒有立即被喚醒,而是等到生產者退出同步塊後才喚醒執行。(這點其實也好理解,synchronized同步方法(塊)同一時刻只允許一個線程在裏面,生產者不退出,消費者也進不去)
注意,消費者被喚醒後是從wait()方法(被阻塞的地方)後面執行,而不是重新從同步塊開頭。
深入了解
這一節我們探討wait/notify與線程狀態之間的關係。深入了解線程的生命周期。
由前麵線程的狀態轉化圖可知,當調用wait()方法後,線程會進入WAITING(等待狀態),後續被notify()後,並沒有立即被執行,而是進入等待獲取鎖的阻塞隊列。
對於每個對象來說,都有自己的等待隊列和阻塞隊列。以前面的生產者、消費者為例,我們拿obj對象作為對象鎖,配合圖示。內部流程如下
當線程A(消費者)調用wait()方法後,線程A讓出鎖,自己進入等待狀態,同時加入鎖對象的等待隊列。
線程B(生產者)獲取鎖後,調用notify方法通知鎖對象的等待隊列,使得線程A從等待隊列進入阻塞隊列。
線程A進入阻塞隊列後,直至線程B釋放鎖後,線程A競爭得到鎖繼續從wait()方法後執行。
如何使用java的鎖機制
可以在臨界區代碼開始的位置執行Lock類的lock方法,為代碼塊加鎖,而在臨界區的出口使用相同Lock實例的unlock方法,釋放臨界區資源。
Demo2-12中,主線程先創建了一個lockTest對象test,然後將相同的test對象交給兩個不同的線程執行。子線程1獲取到了lock後,開始執行before sleep輸出語句,遇到sleep後,線程1阻塞將會放棄執行權,這時線程2可以獲取執行權,當線程2執行lock方法時,發現鎖已經被別的線程獲取,所以線程2阻塞等待lock的釋放。線程1從sleep中被喚醒後,將繼續執行after sleep語句,之後釋放了鎖,此時線程2從鎖等待中被喚醒,執行臨近區的內容,因此Demo2-12的輸出是先線程1的兩條語句,之後才輸出線程2的兩條語句。而Demo2-13在沒有鎖的保護下,程序無法保證先將線程1的兩條語句輸出後再執行線程2的輸出,因此,Demo2-13的輸出結果是交叉的。
java多線程,對象鎖是什麼概念?
java線程:
1.線程中一些基本術語和概念
1.1線程的幾個狀態
初始化狀態
就緒狀態
運行狀態
阻塞狀態
終止狀態
1.2 Daemon線程
Daemon線程區別一般線程之處是:主程序一旦結束,Daemon線程就會結束。
1.3鎖的定義
為了協調多個並發運行的線程使用共享資源才引入了鎖的概念。
1.4死鎖
任何多線程應用程序都有死鎖風險。當一組線程中的每一個都在等待一個只
有該組中另一個線程才能引起的事件時,我們就說這組線程死鎖了。換一個說法
就是一組線程中的每一個成員都在等待別的成員佔有的資源時候,就可以說這組
線程進入了死鎖。死鎖的最簡單情形是:線程 A 持有對象 X 的獨佔鎖,並且
在等待對象 Y 的鎖,而線程 B 持有對象 Y 的獨佔鎖,卻在等待對象 X 的鎖。
除非有某種方法來打破對鎖的等待(Java 鎖定不支持這種方法),否則死鎖的線
程將永遠等下去。
1.5.Java對象關於鎖的幾個方法
1.5.1 wait方法
wait方法是java根對象Object含有的方法,表示等待獲取某個鎖。在wait方法進入前,會釋放相應的鎖,在wait方法返回時,會再次獲得某個鎖。
如果wait()方法不帶有參數,那只有當持有該對象鎖的其他線程調用了notify或者notifyAll方法,才有可能再次獲得該對象的鎖。
如果wait()方法帶有參數,比如:wait(10),那當持有該對象鎖的其他線程調用了notify或者notifyAll方法,或者指定時間已經過去了,才有可能再次獲得該對象的鎖。
參考 thread.lock.SleepAndWait
1.5.2 notify/notifyAll方法
這裡我就不再說明了。哈哈,偷點懶。
1.5.3 yield方法
yield()會自動放棄CPU,有時比sleep更能提升性能。
1.6鎖對象(實例方法的鎖)
在同步代碼塊中使用鎖的時候,擔當鎖的對象可以是這個代碼所在對象本身或者一個單獨的對象擔任,但是一定要確保鎖對象不能為空。如果對一個null對象加鎖,會產生異常的。原則上不要選擇一個可能在鎖的作用域中會改變值的實例變量作為鎖對象。
鎖對象,一種是對象自己擔任,一種是定義一個普通的對象作為private property來擔任,另外一種是建立一個新的類,然後用該類的實例來擔任。
參考 :
thread.lock.UseSelfAsLock,使用對象自己做鎖對象
thread.lock.UseObjAsLock 使用一個實例對象作鎖對象
thread.lock.UseAFinalObjAsLock使用常量對象作為一個鎖對象
1.7類鎖
實例方法存在同步的問題,同樣,類方法也存在需要同步的情形。一般類方法的類鎖是一個static object來擔任的。當然也可以採用類本身的類對象來作為類鎖。
一個類的實例方法可以獲得該類實例鎖,還可以嘗試去訪問類方法,包含類同步方法,去獲得類鎖。
一個類的類方法,可以嘗試獲得類鎖,但是不可以嘗試直接獲得實例鎖。需要先生成一個實例,然後在申請獲得這個實例的實例鎖。
參考
thread.lock.UseStaticObjAsStaticLock 使用類的屬性對象作為類鎖。
thread.lock.UseClassAsStaticLock使用類的類對象作為類鎖
1.8.線程安全方法與線程不安全方法
如果一個對象的所有的public方法都是同步方法,也就是說是public方法是線程安全的,那該對象的private方法,在不考慮繼承的情況下,可以設置為不是線程安全的方法。
參考 thread.lock.SynMethrodAndNotSynMethrod
1.9類鎖和實例鎖混合使用
在實例方法中混合使用類鎖和實例鎖;可以根據前面說的那樣使用實例鎖和類鎖。
在類方法中混合使用類鎖和實例鎖,可以根據前面說的那樣使用類鎖,為了使用實例鎖,先得生成一個實例,然後實例鎖。
參考 thread.lock.StaticLockAndObjLock
1.10鎖的粒度問題。
為了解決對象鎖的粒度過粗,會導死鎖出現的可能性加大,鎖的粒度過細,會程序開發維護的工作加大。對於鎖的粒度大小,這完全要根據實際開發需要來考慮,很難有一個統一的標準。
1.11.讀寫鎖
一個讀寫鎖支持多個線程同時訪問一個對象,但是在同一時刻只有一個線程可以修改此對象,並且在訪問進行時不能修改。
有2種調度策略,一種是讀鎖優先,另外就是寫鎖優先。
參考 thread.lock.ReadWriteLock
1.12 volatile
在Java中設置變量值的操作,除了long和double類型的變量外都是原子操作,也就是說,對於變量值的簡單讀寫操作沒有必要進行同步。這在JVM 1.2之前,Java的內存模型實現總是從主存讀取變量,是不需要進行特別的注意的。而隨着JVM的成熟和優化,現在在多線程環境下volatile關鍵字的使用變得非常重要。在當前的Java內存模型下,線程可以把變量保存在本地內存(比如機器的寄存器)中,而不是直接在主存中進行讀寫。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續使用它在寄存器中的變量值的拷貝,造成數據的不一致。要解決這個問題,只需要像在本程序中的這樣,把該變量聲明為volatile(不穩定的)即可,這就指示JVM,這個變量是不穩定的,每次使用它都到主存中進行讀取。一般說來,多任務環境下各任務間共享的標誌都應該加volatile修飾。
2.線程之間的通訊
在其他語言中,線程之間可以通過消息隊列,共享內存,管道等方式來實現
線程之間的通訊,但是java中可以不採用這樣方式,關注的是線程之間的同步。
只要保證相關方法運行的線程安全,信息共享是自然就可以顯現了。
2.1屏障
屏障就是這樣的一個等待點: 一組線程在這一點被同步,這些線程合併各自的結果或者運行到整體任務的下一階段。
參考:
thread.lock. BarrierUseExample
thread.lock.Barrier
2.2.鎖工具類
提供對線程鎖的獲取,釋放功能。展示了鎖的獲取釋放過程。可以作為一個工具類來使用。
參考:thread.lock. BusyFlag
2.3.條件變量
條件變量是POSIX線程模型提供的一種同步類型,和java中的等待通知機制類似。
雖然java中已經有了等待通知機制,但是為了減少在notify/notifyAll方法中
線程調度的開銷,把一些不需要激活的線程屏蔽出去,引入了條件變量。
Java中2個(多個)條件變量可以是同一個互斥體(鎖對象)。
參考:thread.lock.CondVar 條件變量類
常見的應用情形:
一個鎖控制多個信號通道(例如:多個變量),雖然可以採用簡單java等待通知機制,但是線程調度效率不高,而且線程可讀性也不是太好,這時候可以採用創建一個鎖對象(BusyFlag實例),同時使用這個BusyFlag實例來創建多個條件變量(CondVar 實例)。
經常使用到CondVar類的地方是緩衝區管理,比如:管道操作之類的。先創建一個BusyFlag實例,然後創建CondVar 實例,用這個條件變量描述緩衝區是否為空,另外創建CondVar 實例作條件變量述緩衝區是否滿。
現實中,馬路的紅綠燈,就可以採用條件變量來描述。
3. Java線程調度
3.1 Java優先級
java的優先級別共有10種,加上虛擬機自己使用的優先級別=0這種,總共11種。
大多數情況來說,java線程的優先級設置越高(最高=10),那線程越優先運行。
3.2. 綠色線程
線程運行在虛擬機內,操作系統根本不知道這類線程的存在。
線程是由虛擬機調度的。
3.3 本地線程
線程是由運行虛擬機的操作系統完成的。
3.4 Windows本地線程
操作系統,完全能夠看得到虛擬機內的每一個線程,同時虛擬機的線程和操作系統的線程是一一對應的。Java的線程調度室由操作系統底層線程決定的。
在win32平台下,windows線程只有6個優先級別。和java線程優先級別對應如下:
Java線程優先級 Windows 95/nt/2000線程優先級
0 THREAD_ PRIORITY_IDLE
1(Thread.MIN_PRIORITY) THREAD_ PRIORITY_LOWEST
2 THREAD_ PRIORITY_LOWEST
3 THREAD_ PRIORITY_BELOW_NORMAL
4 THREAD_ PRIORITY_BELOW_NORMAL
5 (Thread.NORM_PRIORITY) THREAD_ PRIORITY _NORMAL
6 THREAD_ PRIORITY _ABOVE_NORMAL
7 THREAD_ PRIORITY _ABOVE_NORMA
8 THREAD_ PRIORITY _HIGHEST
9 THREAD_ PRIORITY _HIGHEST
10 (Thread.MAX_PRIORITY) THREAD_ PRIORITY _CRITICAL
3.5線程優先級倒置與繼承
如果一個線程持有鎖(假設該線程名字=ThreadA,優先級別=5),另外一個線程(假設該線程名字=ThreadB,優先級別=7),現在該線程(ThreadA)處於運行狀態,但是線程ThreadB申請需要持有ThreadA所獲得的鎖,這時候,為了避免死鎖,線程A提高其運行的優先級別(提高到ThreadB的優先級別=7),而線程ThreadB為了等待獲得鎖,降低線程優先級別(降低到ThreadA原來的優先級別=5).
上述的這種情況,對於ThreadA,繼承了ThreadB的優先級別,這成為優先級別的繼承;對於ThreadB暫時降低了優先級別,成為優先級別的倒置。
當然,一旦線程ThreadA持有的鎖釋放了,其優先級別也會回到原來的優先級別(優先級別=5)。線程ThreadB獲得了相應的鎖,那優先級別也會恢復到與原來的值(優先級別=7)。
3.6循環調度
具有同樣優先級的線程相互搶佔成為循環調度。
4.線程池
創建一個線程也是需要一定代價的,為了降低這個代價,採用了和普通對象池的思想建立線程池,以供系統使用。
線程消耗包括內存和其它系統資源在內的大量資源。除了 Thread 對象所需的內存之外,每個線程都需要兩個可能很大的執行調用堆棧。除此以外,JVM 可能會為每個 Java 線程創建一個本機線程,這些本機線程將消耗額外的系統資源。最後,雖然線程之間切換的調度開銷很小,但如果有很多線程,環境切換也可能嚴重地影響程序的性能。
使用線程池的方式是,先建立對象池,然後申請使用線程,程序線程運行,運行完畢,把線程返回線程池。
使用線程池的風險:同步錯誤和死鎖,與池有關的死鎖、資源不足和線程泄漏。
大家有空可以研究一下tomcat的線程池實現原理思想。
實際上是tomcat已經在從線程池的使用線程時候加上了事件處理機制。
個人認為,線程池之類的實現,一般不要自己實現,因為自己實現主要是穩定性等方面可能作的不夠好。
可以參考 apache的jakarta-tomcat-5.5.6的相關代碼,具體是:
jakarta-tomcat-connectors\util\java\org\apache\tomcat\util\threads的相關代碼
5工作隊列
使用工作隊列的好處是不象直接使用線程池那樣,當線城池中沒有線程可以使用的時
候,使用者需要處於等待狀態,不能進行其他任務的處理。
工作隊列的工作原理是:
採用後台線程處理方式,客戶端把任務提交給工作隊列,工作隊列有一組內部可以工作線程,這些工作線程從工作隊列中取出任務運行,一個任務完成後,就從隊列獲取下一個任務進行處理。當工作隊列中沒有任務可以處理時候,工作線程就處於等待狀態,直到獲得新的任務時候,才進行新的處理。
如何在 Java 中正確使用 wait,notify 和 notifyAll
我們先來了解一下為什麼要使用wait,notify
首先看一下以下代碼:
synchronized(a){
…//1
synchronized(b){
}
}
synchronized(b){
… //2
synchronized(a){
}
}
假設現在有兩個線程, t1 線程運行到了//1 的位置,而 t2 線程運行到了//2 的位置,接
下來會發生什麼情況呢?
此時, a 對象的鎖標記被 t1 線程獲得,而 b 對象的鎖標記被 t2 線程獲得。對於 t1 線程
而言,為了進入對 b 加鎖的同步代碼塊, t1 線程必須獲得 b 對象的鎖標記。由於 b 對象的鎖標記被 t2 線程獲得, t1 線程無法獲得這個對象的鎖標記,因此它會進入 b 對象的鎖池,等待 b 對象鎖標記的釋放。而對於 t2 線程而言,由於要進入對 a 加鎖的同步代碼塊,由於 a 對象的鎖標記在 t1 線程手中,因此 t2 線程會進入 a 對象的鎖池。
此時, t1 線程在等待 b 對象鎖標記的釋放,而 t2 線程在等待 a 對象鎖標記的釋放。由
於兩邊都無法獲得所需的鎖標記,因此兩個線程都無法運行。這就是「死鎖」問題。
在 Java 中,採用了 wait 和 notify 這兩個方法,來解決死鎖機制。
首先,在 Java 中,每一個對象都有兩個方法: wait 和 notify 方法。這兩個方法是定義
在 Object 類中的方法。對某個對象調用 wait()方法,表明讓線程暫時釋放該對象的鎖標記。
例如,上面的代碼就可以改成:
synchronized(a){
…//1
a.wait();
synchronized(b){
}
}
synchronized(b){
… //2
synchronized(a){
…
a.notify();
}
}
這樣的代碼改完之後,在//1 後面, t1 線程就會調用 a 對象的 wait 方法。此時, t1 線程
會暫時釋放自己擁有的 a 對象的鎖標記,而進入另外一個狀態:等待狀態。
要注意的是,如果要調用一個對象的 wait 方法,前提是線程已經獲得這個對象的鎖標
記。如果在沒有獲得對象鎖標記的情況下調用 wait 方法,則會產生異常。
由於 a 對象的鎖標記被釋放,因此, t2 對象可以獲得 a 對象的鎖標記,從而進入對 a
加鎖的同步代碼塊。在同步代碼塊的最後,調用 a.notify()方法。這個方法與 wait 方法相對應,是讓一個線程從等待狀態被喚醒。
那麼 t2 線程喚醒 t1 線程之後, t1 線程處於什麼狀態呢?由於 t1 線程喚醒之後還要在
對 a 加鎖的同步代碼塊中運行,而 t2 線程調用了 notify()方法之後,並沒有立刻退出對 a 鎖的同步代碼塊,因此此時 t1 線程並不能馬上獲得 a 對象的鎖標記。因此,此時, t1 線程會在 a 對象的鎖池中進行等待,以期待獲得 a 對象的鎖標記。也就是說,一個線程如果之前調用了 wait 方法,則必須要被另一個線程調用 notify()方法喚醒。喚醒之後,會進入鎖池狀態。線程狀態轉換圖如下:
由於可能有多個線程先後調用 a 對象 wait 方法,因此在 a 對象等待狀態中的線程可能
有多個。而調用 a.notify()方法,會從 a 對象等待狀態中的多個線程里挑選一個線程進行喚醒。
與之對應的,有一個 notifyAll()方法, 調用 a.notifyAll() 會把 a 對象等待狀態中的所有線程都喚醒。
下面結合一個實際的例子,來演示如何使用 wait 和 notify / notifyAll 方法。
我使用一個數組來模擬一個比較熟悉的數據結構:棧。代碼如下所示:
class MyStack{
private char[] data = new char[5];
private int index = 0;
public char pop(){
index — ;
return data[index];
}
public void push(char ch){
data[index] = ch;
index++;
}
public void print(){
for (int i=0; iindex; i++){
System.out.print(data[i] + “\t”);
}
System.out.println();
}
public boolean isEmpty(){
return index == 0;
}
public boolean isFull(){
return index == 5;
}
}
注意,我們為 MyStack 增加了兩個方法,一個用來判斷棧是否為空,一個用來判斷棧是
否已經滿了。
然後我們創建兩個線程,一個線程每隔一段隨機的時間,就會往棧中增加一個數據;另
一個線程每隔一段隨機的時間,就會從棧中取走一個數據。為了保證 push 和 pop 的完整性,在線程中應當對 MyStack 對象加鎖。
但是我們發現,入棧線程和出棧線程並不是在任何時候都可以工作的。當數組滿了的時
候,入棧線程將不能工作;當數組空了的時候,出棧線程也不能工作。違反了上面的條件,我們將得到一個數組下標越界異常。
為此,我們可以用 wait/notify 機制。在入棧線程執行入棧操作時,如果發現數組已滿,
則會調用 wait 方法,去等待。同樣,出棧線程在執行出棧操作時,如果發現數組已空,同
樣調用 wait 方法去等待。在入棧線程結束入棧工作之後,會調用 notifyAll 方法,釋放那些正在等待的出棧線程(因為數組現在已經不是空的了,他們可以恢復工作了)。同樣,當出棧線程結束出棧工作之後,也會調用 notifyAll 方法,釋放正在等待的入棧線程。
一段生產者/消費者相關代碼:
class Consumer extends Thread{
private MyStack ms;
public Consumer(MyStack ms) {
this.ms = ms;
}
public void run(){
while(true){
//為了保證 push 和 pop 操作的完整性
//必須加 synchronized
synchronized(ms){
//如果棧空間已滿,則 wait()釋放 ms 的鎖標記
while(ms.isEmpty()){
try {
ms.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
char ch = ms.pop();
System.out.println(“Pop ” + ch);
ms.notifyAll();
}
//push 之後隨機休眠一段時間
try {
sleep( (int)Math.abs(Math.random() * 100) );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//生產者
class Producer extends Thread{
private MyStack ms;
public Producer(MyStack ms) {
this.ms = ms;
}
public void run(){
while(true){
//為了保證 push 和 pop 操作的完整性
//必須加 synchronized
synchronized(ms){
//如果棧空間已滿,則 wait()釋放 ms 的鎖標記
while(ms.isFull()){
try {
ms.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ms.push(‘A’);
System.out.println(“push A”);
ms.notifyAll();
}
//push 之後隨機休眠一段時間
try {
sleep( (int)Math.abs(Math.random() * 200) );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//主方法中,啟用生產者與消費者兩個線程
public class TestWaitNotify {
public static void main(String[] args) {
MyStack ms = new MyStack();
Thread t1 = new Producer(ms);
Thread t2 = new Consumer(ms);
t1.start();
t2.start();
}
}
部分代碼純手打,望採納~
java的等待喚醒機制必須要讓線程等待嗎
1. 線程的掛起和喚醒
掛起實際上是讓線程進入「非可執行」狀態下,在這個狀態下CPU不會分給線程時間片,進入這個狀態可以用來暫停一個線程的運行;在線程掛起後,可以通過重新喚醒線程來使之恢復運行。
掛起的原因可能是如下幾種情況:
(1)通過調用sleep()方法使線程進入休眠狀態,線程在指定時間內不會運行。
(2)通過調用join()方法使線程掛起,使自己等待另一個線程的結果,直到另一個線程執行完畢為止。
(3)通過調用wait()方法使線程掛起,直到線程得到了notify()和notifyAll()消息,線程才會進入「可執行」狀態。
(4)使用suspend掛起線程後,可以通過resume方法喚醒線程。
雖然suspend和resume可以很方便地使線程掛起和喚醒,但由於使用這兩個方法可能會造成死鎖,因此,這兩個方法被標識為deprecated(抗議)標記,這表明在以後的jdk版本中這兩個方法可能被刪除,所以盡量不要使用這兩個方法來操作線程。
調用sleep()、yield()、suspend()的時候並沒有被釋放鎖
調用wait()的時候釋放當前對象的鎖
wait()方法表示,放棄當前對資源的占有權,一直等到有線程通知,才會運行後面的代碼。
notify()方法表示,當前的線程已經放棄對資源的佔有,通知等待的線程來獲得對資源的占有權,但是只有一個線程能夠從wait狀態中恢復,然後繼續運行wait()後面的語句。
notifyAll()方法表示,當前的線程已經放棄對資源的佔有,通知所有的等待線程從wait()方法後的語句開始運行。
2.等待和鎖實現資源競爭
等待機制與鎖機制是密切關聯的,對於需要競爭的資源,首先用synchronized確保這段代碼只能一個線程執行,可以再設置一個標誌位condition判斷該資源是否準備好,如果沒有,則該線程釋放鎖,自己進入等待狀態,直到接收到notify,程序從wait處繼續向下執行。
synchronized(obj) {
while(!condition) {
obj.wait();
}
obj.doSomething();
}
以上程序表示只有一個線程A獲得了obj鎖後,發現條件condition不滿足,無法繼續下一處理,於是線程A釋放該鎖,進入wait()。
在另一線程B中,如果B更改了某些條件,使得線程A的condition條件滿足了,就可以喚醒線程A:
synchronized(obj) {
condition = true;
obj.notify();
}
需要注意的是:
# 調用obj的wait(), notify()方法前,必須獲得obj鎖,也就是必須寫在synchronized(obj) {…} 代碼段內。
# 調用obj.wait()後,線程A就釋放了obj的鎖,否則線程B無法獲得obj鎖,也就無法在synchronized(obj) {…} 代碼段內喚醒A。
# 當obj.wait()方法返回後,線程A需要再次獲得obj鎖,才能繼續執行。
# 如果A1,A2,A3都在obj.wait(),則B調用obj.notify()只能喚醒A1,A2,A3中的一個(具體哪一個由JVM決定)。
# obj.notifyAll()則能全部喚醒A1,A2,A3,但是要繼續執行obj.wait()的下一條語句,必須獲得obj鎖,因此,A1,A2,A3隻有一個有機會獲得鎖繼續執行,例如A1,其餘的需要等待A1釋放obj鎖之後才能繼續執行。
# 當B調用obj.notify/notifyAll的時候,B正持有obj鎖,因此,A1,A2,A3雖被喚醒,但是仍無法獲得obj鎖。直到B退出synchronized塊,釋放obj鎖後,A1,A2,A3中的一個才有機會獲得鎖繼續執行。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/297697.html