摘要:注意,和都是隨機選擇一個線程,解除其阻塞狀態,可能會造成死鎖。生產者線程向隊列插入元素,消費者線程從隊列取出元素。當添加時隊列已滿或取出時隊列為空,阻塞隊列導致線程阻塞。里面有個小技巧,一個線程搜索完畢時向阻塞隊列填充,讓所有線程能停下來。
多線程對共享數據的讀寫涉及到同步問題,鎖和條件是線程同步的強大工具。鎖用來保護代碼片段(臨界區),任何時刻只能有一個線程執行被保護的代碼。條件對象用來管理那些已經進入被保護的代碼段但還不能運行的線程。
競爭條件各線程訪問數據的次序不同,可能會產生不同的結果。下面的程序可以實現兩個賬戶之間的轉賬,正常情況下所有賬戶的總金額應該是不變的。
public void transfer(int from, int to, double amount) { if (accounts[from] < amount) { return; } accounts[from] -= amount; accounts[to] += amount; System.out.printf(" Total Balance %10.2f ", getTotalBalance()); }
但是在上面程序的運行中發現輸出的總金額是變化的,這是因為transfer()方法執行的過程中會被中斷,可能存在幾個線程同時讀寫賬戶余額。問題的根源在于轉賬這一系列動作不是原子操作,并且沒有使用同步。當然同步使用不當也會造成死鎖(所有線程都阻塞的狀態)。
鎖對象可以使用鎖和條件對象實現同步數據存取。鎖能夠保護臨界區,確保只有一個線程執行。
注意,在finally子句中不要忘記解鎖操作。若因異常拋出釋放,對象可能受損。
互斥鎖ReentrantLock類能夠有效防止代碼塊受并發訪問的干擾。
private Lock bankLock; private Condition sufficientFunds; public void transfer(int from, int to, double amount) throws InterruptedException { bankLock.lock(); try { while (accounts[from] < amount) { sufficientFunds.await(); } accounts[from] -= amount; accounts[to] += amount; System.out.printf(" Total Balance %10.2f ", getTotalBalance()); sufficientFunds.signalAll(); } finally { bankLock.unlock(); } }
每一個Bank對象有自己的ReentrantLock對象,如果兩個線程試圖訪問同一個Bank對象,那么鎖以串行方式提供服務。但是如果兩個線程訪問的是不同的Bank對象,兩個線程都不會發生阻塞。
對于所有賬戶總金額的獲取方法也需要加鎖才能保證正確執行。鎖是可重入的,也就是說同一個線程可以重復的獲得已經持有的鎖。鎖保持一個持有計數來跟蹤嵌套獲得鎖的次數,當持有計數變為0時,線程釋放鎖。
public double getTotalBalance() { bankLock.lock(); try { double sum = 0; for (double a : accounts) { sum += a; } return sum; } finally{ bankLock.unlock(); } }測試鎖
tryLock()方法用于嘗試獲取鎖而沒有發生阻塞。如果未獲得鎖,線程可以立即離開,去做別的事。
if(myLock.tryLock()) { try { do something } finally { myLock.unlock(); } } else { do something else }
調用帶有超時參數的tryLock(),線程可以在等待獲取鎖的過程中被中斷,拋出InterruptedException異常。從而允許程序打破死鎖,類似于lockInterruptibly()。
讀寫鎖java.util.concurrent.locks包定義了兩個鎖類:ReentrantLock類和ReentrantReadWriteLock類。在讀多寫少(很多線程從一個數據結構讀取數據,很少線程修改其中數據)的情形中,ReentrantReadWriteLock類是十分實用的。
讀鎖,允許多個讀,排斥所有寫;寫鎖,排斥所有讀和寫。
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); private Lock readLock = rwl.readLock(); private Lock writeLock = rwl.writeLock();條件對象
條件對象用來管理那些已經獲得鎖但不能工作的線程。比如當賬戶中沒有足夠余額時,需等待別的線程的存款操作。
一個鎖對象可以有一個或多個相關的條件對象。當一個線程調用await()等待方法時,它將進入該條件的等待集。當一個線程轉賬完成時會調用sufficientFunds.signalAll()方法,重新激活因為sufficientFunds這一條件而等待的所有線程,使這些線程從等待集中移出,狀態變為可運行。當一個線程處于等待集中時,只能靠其他線程來重新激活自己。
synchronized關鍵字使用synchronized關鍵字聲明的方法,對象的鎖將保護整個方法,其實就是隱式的使用了一個內部對象鎖。內部對象鎖只有一個條件對象,使用wait()/notifyAll()/notify()操作。
public synchronized void myMethod() { while (! (ok to proceed)) { wait(); } do something notifyAll(); }
注意,signal()和notify()都是隨機選擇一個線程,解除其阻塞狀態,可能會造成死鎖。
對于sychronized修飾的方法,顯式使用鎖對象和條件對象,形式如下。
public void myMethod() { this.intrinsic.lock(); try { while(! (ok to proceed)) { condition.await(); } do something condition.signalAll(); } finally { this.intrinsic.unlock(); } }
為了保證操作的原子性,可以安全地使用AtomicInteger作為共享計數器而無需同步,這個類提供方法incrementAndGet()和decrementAndGet()完成自增自減操作。
Volatile域使用volatile關鍵字同步讀寫的必要性:
由于寄存器或緩存的存在同一內存地址可能會取到不同的值;
編譯器優化中假定內存中的值僅在代碼中有顯式修改指令時會改變。
volatile關鍵字為實例域的同步訪問提供了一種免鎖機制,當被聲明為volatile域時,編譯器和虛擬機就知道該域可能被另一個線程并發更新。使用鎖或volatile修飾符,多個線程可以安全地讀取一個域,但volatile不提供原子性。。另外,將域聲明為final,也可以保證安全的訪問這個共享域。
線程局部變量在線程間共享變量時有風險的,可以使用ThreadLocal輔助類為各個線程提供各自的實例。比如,SimpleDateFormat類不是線程安全的,內部數據結構會被下面形式的并發訪問破壞。
public static final SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd"); String dateStamp = dateFormat.format(new Date());
如果不使用synchronized或鎖等開銷較大的同步,可以使用線程局部變量ThreadLocal解決變量并發訪問的問題。
public static final ThreadLocaldateFormat = new ThreadLocal () { protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; String dateStamp = dateFormat.get().format(new Date());
在一個線程中首次調用get()時,會調用initialValue()方法,此后會返回屬于當前線程的實例。
對于java.util.Random類,雖是線程安全的,但多線程共享隨機數生成器卻是低效的。可以使用上面提到的ThreadLocal為各個線程提供一個多帶帶的生成器,還可以使用ThreadLocalRandom這個便利類。
int random = ThreadLocalRandom.current().nextInt(upperBound);阻塞隊列
上面關于同步的實現方式是Java并發程序設計基礎的底層構建塊,在實際的編程使用中,使用較高層次的類庫會相對安全方便。對于典型的生產者和消費者問題,可以使用阻塞隊列解決,這樣就不用考慮鎖和條件的問題了。
生產者線程向隊列插入元素,消費者線程從隊列取出元素。當添加時隊列已滿或取出時隊列為空,阻塞隊列導致線程阻塞。將阻塞隊列用于線程管理工具時,主要用到put()和take()方法。對于offer()、poll()、peek()方法不能完成時,只是給出一個錯誤提示而不會拋出異常。
java.util.concurrent包提供了幾種形式的阻塞隊列:
LinkedBlockingQueue:無容量限制,鏈表實現;
LinkedBlockingDeque:雙向隊列,鏈表實現;
ArrayBlockingQueue:需指定容量,可指定公平性,循環數組實現;
PriorityBlockingQueue:無邊界優先隊列,用堆實現。
這里有一個用阻塞隊列控制一組線程的示例,實現的功能是搜索指定目錄及子目錄中的所有文件并找出含有查詢關鍵字的行。里面有個小技巧,一個線程搜索完畢時向阻塞隊列填充DUMMY,讓所有線程能停下來。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/65742.html
摘要:整個包,按照功能可以大致劃分如下鎖框架原子類框架同步器框架集合框架執行器框架本系列將按上述順序分析,分析所基于的源碼為。后,根據一系列常見的多線程設計模式,設計了并發包,其中包下提供了一系列基礎的鎖工具,用以對等進行補充增強。 showImg(https://segmentfault.com/img/remote/1460000016012623); 本文首發于一世流云專欄:https...
摘要:在時,引入了包,該包中的大多數同步器都是基于來構建的。框架提供了一套通用的機制來管理同步狀態阻塞喚醒線程管理等待隊列。指針用于在結點線程被取消時,讓當前結點的前驅直接指向當前結點的后驅完成出隊動作。 showImg(https://segmentfault.com/img/remote/1460000016012438); 本文首發于一世流云的專欄:https://segmentfau...
摘要:好了,繼續向下執行,嘗試獲取鎖失敗后,會調用首先通過方法,將包裝成共享結點,插入等待隊列,插入完成后隊列結構如下然后會進入自旋操作,先嘗試獲取一次鎖,顯然此時是獲取失敗的主線程還未調用,同步狀態還是。 showImg(https://segmentfault.com/img/remote/1460000016012541); 本文首發于一世流云的專欄:https://segmentfa...
摘要:同步包裝器任何集合類使用同步包裝器都會變成線程安全的,會將集合的方法使用鎖加以保護,保證線程的安全訪問。線程池中的線程執行完畢并不會馬上死亡,而是在池中準備為下一個請求提供服務。 多線程并發修改一個數據結構,很容易破壞這個數據結構,如散列表。鎖能夠保護共享數據結構,但選擇線程安全的實現更好更容易,如阻塞隊列就是線程安全的集合。 線程安全的集合 Vector和HashTable類提供了線...
摘要:當線程使用完共享資源后,可以歸還許可,以供其它需要的線程使用。所以,并不會阻塞調用線程。立即減少指定數目的可用許可數。方法用于將可用許可數清零,并返回清零前的許可數六的類接口聲明類聲明構造器接口聲明 showImg(https://segmentfault.com/img/bVbfdnC?w=1920&h=1200); 本文首發于一世流云的專欄:https://segmentfault...
閱讀 2034·2023-04-26 00:16
閱讀 3486·2021-11-15 11:38
閱讀 3177·2019-08-30 12:50
閱讀 3188·2019-08-29 13:59
閱讀 759·2019-08-29 13:54
閱讀 2509·2019-08-29 13:42
閱讀 3313·2019-08-26 11:45
閱讀 2195·2019-08-26 11:36