從零開始學多執行緒之構建快(四)
前文回顧
上一篇部落格
ofollow,noindex" target="_blank">從零開始學多執行緒之組合物件(三)
主要講解了:
1. 設計執行緒安全的類要考慮的因素.
2. 對於 非執行緒安全的物件 ,我們可以考慮使用 鎖+例項限制 (Java監視器模式)的方式,安全的訪問它們.
3. 擴充套件執行緒安全類的四種方式.
本篇部落格將要講解的知識點
使用java提供的 執行緒安全容器 和 同步工具 .來構建執行緒安全的類.
這些同步工具包括: 同步容器、併發容器和阻塞佇列 .
開始之前先介紹幾個簡單的基礎知識:
Thread、Runnable 和 Callable. Runnable是一個介面,裡面只有一個抽象的方法public void run(),Thread是Runnable的實現類,我們一般開啟一個新執行緒執行一些任務的時候就如此這般:
//宣告一個任務 Runnable r = new Runnable() { @Override public void run() { //你要執行的任務 ); //把任務放入執行執行緒 Thread t = new Thread(r); //執行任務 t.start();
而Callable是一個帶返回值的Runnable.好正文開始.
同步容器
同步容器通過Collections.sychronizedXXX工廠方法建立,可以建立各種執行緒安全的同步容器.
public class Synchronization { private final List<Object> list = Collections.synchronizedList(new ArrayList<>()); }
本質上就是使用上一篇部落格講到的例項限制實現的(把非執行緒安全的物件包裝進一個類,通過這個類的鎖去訪問這個物件).
同步容器都是執行緒安全的 , 但是它有很多的侷限性 ,因為它的方法都是同步的,所以它的併發性會受到影響,如果有其他的執行緒去併發修改容器的時候,同步容器也會出現問題.
對於一些複合操作有時你可能需要使用額外的客戶端加鎖進行保護
再看之前的例子:
1 public class Synchronization { 2 3private final List<Object> list = Collections.synchronizedList(new ArrayList<>()); 4 5public Object getLast(){ 6//獲得最後一個元素的下標 7int lastIndex = list.size() - 1; 8return list.get(lastIndex); 9} 10 11public void removeLast(){ 12int lastIndex = list.size() - 1; 13list.remove(lastIndex); 14} 15 16 }
雖然list是執行緒安全的,但是當併發呼叫getLast()和removeLast()方法的時候還是會出現問題, 當代碼
走到getLast()方法第7行的時候,另一個執行緒可能已經執行完了removeLast()方法,所以此時的lastIndex
下標是一個過期值,會出現陣列下標越界的問題 .
為了解決這個問題,我們可以採用客戶端加鎖的方式:
public Object getLast() { synchronized (list) { //獲得最後一個元素的下標 int lastIndex = list.size() - 1; return list.get(lastIndex); } } public void removeLast() { synchronized (list) { int lastIndex = list.size() - 1; list.remove(lastIndex); } }
同樣的我們在迭代list集合的時候,如果list被其他執行緒修改了,會丟擲ConcurrentModifacationException.
可以使用客戶端加鎖的方式規避,但是 影響併發性
public void forEach(){ synchronized (list) { for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); } } }
接下來給大家展示一個"有趣的""程式碼:
public class HiddenIterator { private Set<Integer> set = new HashSet<>(); public synchronized voidadd(Integer i){ set.add(i); } public synchronizedvoid remove(Integer i){ set.remove(i); } public void addTenThings(){ Random r = new Random(); for (int i = 0; i < 10; i++) { set.add(r.nextInt()); } System.out.println("set = " + set); } }
HiddenIterator限制了非執行緒安全的set的訪問,使它可以被安全的訪問,addTenThings()方法
增加十個隨機值到集合中,最後列印輸出set的值,一切都看上去很完美,然而就是這麼一個人畜無害的程式碼, 卻有著丟擲ConcurrentModifacationException的可能.
這是怎麼回事呢? 答案在 System.out.println("set = " + set);這一行. 這是一個隱藏的迭代過程,字串的拼接操作經過編譯轉換成呼叫StringBuilder.append(Object)來完成,它會呼叫容器的toString方法.標準容器內的toString的實現會通過迭代容器中的每個元素,來獲得關於容器內容格式良好的展現,所以在這個過程進行中,如果有另一個執行緒修改了容器的大小,就會丟擲ConcurrentModifacationException.
容器的hashcode和equals方法也會間接地呼叫迭代, 為了構建更安全的類,我們應該儘量使用執行緒安全的容器.
正如封裝一個物件的狀態,能夠使它更加容易地保持不變約束一樣,封裝它的同步則可以破式它符合同步策略. (封裝同步就是讓物件的成員變數自己去內部同步的意思.)
好了,說了半天同步容器的種種不好和侷限,其實都是為了襯托出接下來的這個容器,我們繼續往下看
併發容器
併發容器類是同步容器的升級版 ,同步容器通過對容器的所有狀態進行序列訪問,從而實現了他們的執行緒安全.這樣做的代價是 削弱併發性 ,當多個執行緒共同競爭容器級的鎖時 ,吞吐量就會降低 .
併發容器就是專門為多程式設計併發訪問設計的!!!! 新的ConcurrentMap介面介入了對常見覆合操作的支援,例如以前提到過的缺少即加入、替換和條件刪除.
用併發容器替換同步容器,這種作法以有很小風險帶來了可擴充套件性顯著的提高.
我們以ConcurrentHashMap和同步的HashMap為例.
ConcurrentHashMap比同步的HashMap提供了更好的併發性和可伸縮性,同步容器使用一個公共鎖同步每一個方法,並嚴格地限制只能有一個執行緒可以訪問容器.而ConcurrentHashMap使用一個更加細化的鎖機制--分離鎖這個鎖機制( 這個鎖機制允許更深層次的共享訪問 ).
任意數量的讀執行緒可以併發訪問Map,有限數量的寫執行緒可以併發修改Map.結果是,為併發訪問帶來更高的吞吐量,同時幾乎沒有損失單個執行緒訪問的效能.
還記得同步容器在迭代時修改會丟擲ConcurrentModifacationException異常嗎,這在併發容器中不會發生. ConcurrentHashMap返回的迭代器具有弱一致性.弱一致性的迭代器可以允許併發修改.當迭代器被建立時,它會遍歷已有的元素,並且可以(但是不保證)感應到在迭代器被建立後對容器的修改.
併發容器的size和isEmpty這樣的方法在併發環境下沒什麼用,因為它們的目標是運動的,所以對這些操作的需求弱化了.
同步容器和併發容器的選擇上已經很清晰了,我們的 第一選擇 應該是 併發容器 (更好的效能,沒有劣勢),然而因為 併發容器使用的是分離鎖,無法獨佔訪問 ,所以 在需要獨佔訪問容器的時候我們還是需要同步容器的 .(需要獨佔訪問的 情況:原子化的加入一些對映add(),或者對元素進行若干次迭代,需要保證元素的順序).
CopyOnWriteArrayList
CopyOnWriteArrayList是同步List的一個併發替代品 ,也提供了更好的併發性,並避免了在迭代期間對容器加鎖和複製.
寫入時複製容器的安全性來源於這樣一個事實,只要有效的不可變物件被正確釋出,那麼訪問它將不再需要更多的同步.
在每次需要修改時,他們會建立一個並重新發佈一個新的容器拷貝,以此來實現可變性.
寫入時賦值容器返回的迭代器不會丟擲ConcurrentModifacationException,並且返回的元素嚴格與容器
建立時相一致,不會考慮後續的修改
public static void main(String [] args) throws InterruptedException { List<Point> list = new CopyOnWriteArrayList<>(); list.add(new Point(1,1)); list.add(new Point(2,2)); list.add(new Point(3,3)); list.add(new Point(4,4)); new Thread(new Runnable() { @Override public void run() { for (Point point : list) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(point); } } }).start(); Thread.sleep(1000); System.out.println("繼續執行了"); list.add(new Point(5,5)); System.out.println("list = " + list); }
輸出結果:
Point{x=1, y=1}
繼續執行了
list = [Point{x=1, y=1}, Point{x=2, y=2}, Point{x=3, y=3}, Point{x=4, y=4}, Point{x=5, y=5}]
Point{x=2, y=2}
Point{x=3, y=3}
Point{x=4, y=4}
即使在迭代中給集合新增一個元素,輸出的元素確實還是與迭代時建立的一致.
限於篇幅這裡就不過多展開CopyOnWriterArrayList這個容器了.
阻塞佇列和生產者-消費者模式
阻塞佇列可以說是 非常有用 的東西,請睜大您的雙眼看仔細了.
阻塞佇列(Blocking queue)提供了 可阻塞的 put和take方法,和可定時的offer,poll是等價的(如果超過規定的時間會停止阻塞繼續執行).
如果Queue滿了,put方法會被阻塞,直到有空間可用;如果Queue是空的,那麼take方法會被
阻塞直到有元素可用,Queue的長度可以有限,也可以無限;無限的Queue永遠不會阻塞,所以
它的put方法永遠不會阻塞.(無限的Queue等於無限的任務,無限的任務對上有限的記憶體 = 程式崩潰,所以我們的選擇顯而易見)
public class Blocking { public static void main(String [] args) throws InterruptedException { //建構函式傳遞的1,代表隊列的容量 Queue<String> queue = new ArrayBlockingQueue<String>(1); //該執行緒3秒後會給佇列加入一個值 new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(3000); ((ArrayBlockingQueue<String>) queue).put("1"); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); //這時候佇列是空的會阻塞...直到上面的執行緒執行新增任務.. ((ArrayBlockingQueue<String>) queue).take(); System.out.println("繼續執行了"); } }
感興趣的讀者可以把這段程式碼copy執行一下, new ArrayBlockingQueue<String>(1); 建構函式傳遞的引數1
是 設定這個佇列的邊界 的,只可以存放一個物件,new Thread().start 聲明瞭一個新執行緒,裡面的任務就是
3s後往阻塞佇列中加入一個元素,主執行緒執行take()操作,這時候因為佇列沒有值,所以被阻塞了沒有輸出
"繼續執行了"這句話,等待3秒以後,成功輸出"繼續執行了".
使用poll()相當於可定時的take,拿出物件:
public class Blocking { public static void main(String [] args) throws InterruptedException { Queue<String> queue = new ArrayBlockingQueue<>(1); ((ArrayBlockingQueue<String>) queue).poll(3,TimeUnit.SECONDS); System.out.println("繼續執行了"); } }
3s後輸出,"繼續執行了".
關於put和take,與offer(可定時的put)和poll(可定時的take)之間如何抉擇呢?
答案是 選擇後者 ,因為put和take有可能會有長時間阻塞的風險,會產生死鎖 ,所以最優的選擇
是使用定時的方法.
有界的Queue和無限的Queue之間也最好選擇前者,因為如果無 限的佇列有可能佔用過多的記憶體
導致程式或者系統崩潰 .
阻塞佇列支援生產者-消費者設計模式,生產者put,消費者take,該模式不會發現一個工作立即處理,而是把工作置入一個任務清單
中(佇列),生產者消費者模式簡化了開發,因為它解除了生產者類和消費者類之間相互依賴的程式碼.
最常見 的生產者-消費者設計是將執行緒池與工作佇列相結合.
如果生產者不能夠足夠快的產生工作,讓消費者忙碌起來,那麼消費者只能一直等待,直到有工作可做
這時候可能需要將生產者執行緒和消費者執行緒進行調整,以獲得更好的資源利用率.
如果生產者產生工作的速度總是比消費者處理的速度快,那麼佇列會越來越大,如果是無界的佇列
記憶體最終會耗盡, 使用put方法的阻塞特性大大簡化了生產者的編碼;當佇列滿的時候生產者就會阻塞,
給消費者追趕的時間.
使用offer方法(可定時的put),如果新增元素失敗,會返回一個false失敗狀態,我們可以用offer返回的狀態
做一些減輕負載、序列化剩餘工作條目並寫入硬碟,減少生產者執行緒或者其它的方法遏制生產者執行緒的處理.
示例:
public static void main(String [] args) throws InterruptedException { Queue<String> queue = new ArrayBlockingQueue<>(1); ((ArrayBlockingQueue<String>) queue).put("a"); ((ArrayBlockingQueue<String>) queue).poll(3,TimeUnit.SECONDS); System.out.println("繼續執行了"); boolean first = ((ArrayBlockingQueue<String>) queue).offer("a", 1, TimeUnit.SECONDS); System.out.println("first = " + first); boolean second = ((ArrayBlockingQueue<String>) queue).offer("a",1,TimeUnit.SECONDS); System.out.println("two = " + second); boolean third = ((ArrayBlockingQueue<String>) queue).offer("a",1,TimeUnit.SECONDS); System.out.println("three = " + third); } }
輸出:
繼續執行了
first = true
two = false
three = false
有界佇列是強大的資源管理工具,用來建立可靠的應用程式;他們遏制那些可以產生過多工作量、具有威脅的活動,從而讓你的程式在面對超負荷工作
時更加健壯.
一些常用的阻塞佇列介紹: 類庫中包含一些BlockingQueue的實現,其中LinkedBlockingQueue和ArrayBlockingQueue
是FIFO(first in first out 先進先出)佇列,與LinkedList和ArrayList相似,但是卻擁有比同步List更好的
併發效能.PriorityBlockingQueue是一個按優先順序順序排序的佇列,可以使用Comparator進行排序
還有一個SynchronousQueue,它不是真正的佇列,因為它不會為佇列元素維護任何儲存空間,它非常
直接地移交工作,減少了在生產者和消費者之間移動資料的延遲時間.SynchronousQueue這類佇列
只有在消費者充足的時候比較合適,它們總能為下一個任務做好準備.
阻塞佇列非常重要,只要使用執行緒池就離不開阻塞佇列.
宣告一個執行緒池,建構函式就需要傳遞阻塞佇列:
對阻塞佇列有一個非常深入的理解,可以幫助構建更加健壯的併發程式.
Deque(雙端佇列)和BlcokingDeque是Queue和BlockIngQueue的升級版.Deque允許高效的在頭和尾分別進行插入和移除.實現他們的是ArrayDeque和LinkedBlockingDeque.
阻塞佇列是和用於生產者-消費者模式, 雙端佇列適用於竊取工作的模式 .. 一個消費者生產者設計中,所有的消費者只共享一個工作佇列;在竊取工作的設計中,每一個消費者都有自己的雙端佇列.如果一個消費者完成了自己雙端佇列中的全部工作,它可以偷取其他消費者的雙端佇列中的末尾任務.因為工作者執行緒並不會競爭一個共享的任務佇列,所以竊取工作模式比傳統的生產者-消費者設計有更佳的可伸縮性;大多數時候他們訪問自己的雙端佇列,減少競爭.當一個工作者必須要訪問另一個佇列時,它會從尾部擷取,而不是頭部,從而進一步降低對雙端佇列的爭奪.
阻塞和可中斷的方法
執行緒可能會因為幾種原因阻塞或暫停: 等待I/O操作結束,等待獲得一個鎖,等待從Thread.sleep中喚醒,或者是等待另一個執行緒的計算結果.當一個執行緒阻塞時,他通常被掛起,並被設定成執行緒阻塞的某個狀態(BLOCKED、WAITING,或是TIMED_WATTING) 一個阻塞的操作和一個普通的操作之間的差別僅僅在於,被阻塞的執行緒必須等待一個事件的發生才能繼續進行,並且這個事件是超越它自己控制的,因而需要花費更長的時間----等待I/O操作完成,鎖可用,或者是外部計算.當外部事件發生後,執行緒被置回RUNNABLE狀態,重新獲得排程的機會.
如果一個方法能夠丟擲InterruptedException異常,說明這是一個可阻塞的方法,進一步看,如果它被中斷,將可以提前結束阻塞狀態.
Thread提供了interrupt方法,用來中斷一個執行緒,或者查詢某執行緒是否已經被中斷,每一個執行緒都有一個Boolean型別的屬性,這個屬性代表了執行緒的中斷狀態;中斷執行緒時需要設定這個值.
中斷執行緒休眠的例項:
1 public static void main(String[] args) { //宣告一個執行緒,休眠10s 2Thread t = new Thread(new Runnable() { 3@Override 4public void run() { 5try { 6Thread.sleep(10000); 7} catch (InterruptedException e) { 8e.printStackTrace(); 9System.out.println("Thread.currentThread().isInterrupted() = " + Thread.currentThread().isInterrupted()); 10} 11} 12}); 13System.out.println("t.isInterrupted() = " + t.isInterrupted()); 14t.start(); 15t.interrupt(); 16System.out.println("t.isInterrupted() = " + t.isInterrupted()); 17 18}
Thread.sleep()方法丟擲了一個受檢查的異常,證明他是一個可以被中斷的方法.在第15行呼叫的t.interrupt()方法可以中斷這個sleep方法.方法的13行、16行、9行 分別在中斷操作前,中斷操作後、捕獲異常後, 列印輸出執行緒的中斷狀態.輸出的結果是 false,true,fasle, 說明預設的中斷狀態是false,執行中斷操作以後狀態為true,捕獲到中斷異常又變為了false.
有兩種方式來響應中斷:
1. 不捕獲中斷異常,而是拋給上層的呼叫者.或者捕獲異常,做一些簡單的處理,然後再重新丟擲異常給上層程式碼
2. 有的時候無法丟擲異常,例如在Runnable中的時候,這時候必須捕獲InterruptedException.而且你還應該呼叫interrupt方法恢復中斷狀態,這樣呼叫棧中更高層的程式碼可以發現中斷已經發生.
示例: 在第8行恢復中斷,重新將中斷狀態設定為true,返回給上層程式碼.
1 new Runnable() { 2@Override 3public void run() { 4try { 5Thread.sleep(10000); 6} catch (InterruptedException e) { 7e.printStackTrace(); 8Thread.currentThread().interrupt() 9} 10} 11}
不應該捕獲InterruptedException之後不做任何處理,這樣做會丟失執行緒中斷的證據,從而剝奪了上層棧的程式碼處理中斷的機會 . 只有一種情況允許掩蓋中斷: 你擴充套件了Thread,並因此控制流所有處於呼叫棧上層的程式碼.
Synchronizer
Synchronizer(同步裝置)是一個物件,它根據本身的狀態調節執行緒的控制流.阻塞佇列可以扮演一個Synchronizer(阻塞的take和put方法來使執行緒阻塞或執行),接下來簡單介紹幾個其他型別的同步裝置:訊號量(semaphore),關卡(barrier)和閉鎖(latch).
所有Synchronizer都有類似的結構特性: 它們封裝狀態,而這些狀態決定著執行緒執行到某一點時是通過還是被迫等待;它們還提供操控狀態的方法,以及高效地等待Synchronizer進入到期望狀態的方法.
1. 閉鎖
閉鎖: 可以延遲執行緒的進度直到執行緒到達終止狀態. 在終止狀態到來之前沒有執行緒能夠通過,終止狀態到來的時候,所有執行緒都允許通過.終止狀態是不可逆的,會永遠保持這個狀態.
閉鎖可以用來確保特定活動指導其他的活動完成後才發生,適合使用閉鎖的情況:
1. 確保一個計算不會執行,直到它需要的資源被初始化.
2. 確保一個服務不會開始,直到它依賴的其他服務都已經開始.
3. 等待,直到活動的所有部分都為就處理做好準備
具體的使用: CountDownLatch是一個靈活的閉鎖實現,用於以上各種情況:允許一個或多個執行緒等待一個事件集的發生.閉鎖的狀態包括一個計數器,初始化為一個整數,用來表現需要等待的事件數,countDown方法對計數器做減操作,表示一個事件已經發生了,而await方法等待計數器到達零,此時所有需要等待的時間都已發生.如果計數器入口時值為非零,await會一直阻塞直到計數器為零,或者等待執行緒中斷以及超時.
示例程式碼:
public static void main(String[] args) { /*建構函式傳入的數字表示的是需要倒計時的次數,這裡傳了三 * 也就是說必須倒計時三次,否則await方法會阻塞住. * */ CountDownLatch countDownLatch = new CountDownLatch(3); //倒計時三次 //1 countDownLatch.countDown(); //2 countDownLatch.countDown(); //3 countDownLatch.countDown(); try { //如果countDown的次數少於構造方法傳入的引數的數量,就會阻塞... countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(1111); }
2. FutureTask
這個用的也挺多的. 主要用在需要長時間執行的操作 .FutureTask也可以作為閉鎖.FutureTask的計算是通過Callable實現的,它等價於一個可攜帶結果的Runnable,並且有3個狀態:等待、執行和完成.完成包括有計算以任意的方式結束,包括正常結束、取消和異常.一旦FutureTask進入完成狀態,它會永遠停止在這個狀態上(是不是和閉鎖的終止狀態一樣).
Future.get方法用來獲取任務的結果,如果完成了就及時返回結果,如果沒完成那就阻塞.FutureTask把計算的結果從執行計算的執行緒傳送到這個需要結果的執行緒:FutureTask的歸約保證了這種傳遞建立在結果的安全釋出基礎之上.
Executor框架(可以理解為執行緒池)利用FutureTask來完成非同步任務,並可以用來進行任何潛在好事計算,而且可以在真正需要計算結果之前就啟動它們開始計算.(儘早開始計算,你可以減少等待結果所需花費的時間),
public static void main(String [] args){ Callable<String> callable = new Callable<String>() { @Override public String call() throws Exception { Thread.sleep(5000); return"執行完畢"; } }; FutureTask<String> futureTask = new FutureTask<String>(callable); Thread t = new Thread(futureTask); t.start(); try { String result = futureTask.get(); System.out.println(result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } }
3.訊號量
使用的方式和閉鎖差不多,計數訊號量(Counting semaphore)用來控制能夠同時訪問某指定資源的活動的數量,或者同時執行某一給定操作的數量.計數訊號量可以用來實現資源池或者給一容器限定邊界.
簡單的方法介紹:
public static void main(String [] args) throws InterruptedException { //構造方法傳入的引數,可以建立一個叫所有集的東西 Semaphore semaphore = new Semaphore(3); //acquire()每次呼叫消耗一個所有集 semaphore.acquire(); System.out.println("使用了一次"); semaphore.acquire(); System.out.println("使用了兩次"); semaphore.acquire(); System.out.println("使用了三次"); //這是第四次呼叫,沒有可用的所有集了,會阻塞.. semaphore.acquire(); // 呼叫semaphore.release()可恢復一個所有集; }
關卡
之 前閉鎖介紹的閉鎖只要到達了終點狀態就沒法再次使用了 ,現在介紹的關卡類似於閉鎖,但是它能迴圈使用,它們都能阻塞一組執行緒,直到某些時間發生,其中關卡與閉鎖關鍵的不同在於,所有執行緒必須同時到達關卡點,才能繼續處理.閉鎖等待的是事件;關卡等待的是執行緒.
簡單的減少了一下CyclicBarrier的使用方法,有興趣的讀者可以複製下來自己測試一下:
public static void main(String [] args) throws BrokenBarrierException, InterruptedException { //建構函式傳了兩個引數,第一個是等待的執行緒數,第二個是當所有執行緒到達關卡點統一執行的任務 CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() { @Override public void run() { System.out.println("嘿,還真一起執行了"); } }); //設定三個執行緒,每個阻塞不同的時間. new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000); cyclicBarrier.await(); System.out.println("是否一起執行"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { cyclicBarrier.await(); System.out.println("是否一起執行"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(3000); cyclicBarrier.await(); System.out.println("是否一起執行"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }).start(); /*下面注掉的這些的程式碼證明關卡可以重複使用. Thread.sleep(1000); Long startTime = System.nanoTime(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(5000); Long endTime = System.nanoTime() - startTime; System.out.println("測試阻塞"+endTime); cyclicBarrier.await(); System.out.println("是否一起執行"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(2000); cyclicBarrier.await(); System.out.println("是否一起執行"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); cyclicBarrier.await(); System.out.println("是否一起執行"); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }).start(); */ } 簡單的使用關卡的例子
關卡的用處: 一個步驟的計算可以並行完成,但是必須完成所有與一個步驟相關的工作後才能進行下一步.
總結
基礎部分到這裡就結束了.以下是基礎部分的總結:
1.所有併發問題都歸結為如何協調訪問併發狀態.可變狀態越少,保證執行緒安全就越容易.
2. 儘量將域宣告為final型別,除非它們的需要是可變的.
3. 不可變物件天生是執行緒安全的.
4. 封裝使管理複雜度變得可行.
5. 用鎖來守護每一個可變變數
6. 對同一不變約束中的所有變數都使用相同的鎖.
7. 在非同步的多執行緒情況下,訪問可變變數的程式是存在隱患的.
8. 在設計過程中就考慮執行緒安全,或者在文件中明確地說明它不是執行緒安全的.
9. 文件化你的同步策略.
本篇筆記分享就到此為止了,博主下一篇會更新構建併發程式(執行緒、Executor)方面的部落格. 我們下篇部落格再見 !