今天開始咱們聊聊 Java 併發工具包中提供的一些工具類,本文主要從併發同步容器和併發集合工具角度入手,簡單介紹下相關 API 的用法與部分實現原理,旨在幫助你們更好的使用和理解 JUC 工具類。java
在開始今天的內容以前,咱們還須要簡單回顧下線程、 syncronized 的相關知識。git
Java 線程的運行週期中的幾種狀態, 在 java.lang.Thread 包中有詳細定義和說明:github
NEW 狀態是指線程剛建立, 還沒有啓動數組
RUNNABLE 狀態是線程正在正常運行中緩存
BLOCKED 阻塞狀態 安全
WAITING 等待另外一個線程來執行某一特定操做的線程處於這種狀態。這裏要區分 BLOCKED 和 WATING 的區別, BLOCKED 是在臨界點外面等待進入, WATING 是在臨界點裏面 wait 等待其餘線程喚醒(notify)多線程
TIMEDWAITING 這個狀態就是有限的(時間限制)的 WAITING併發
TERMINATED 這個狀態下表示 該線程的 run 方法已經執行完畢了, 基本上就等於死亡了(當時若是線程被持久持有, 可能不會被回收)dom
synchronized 實現同步的基礎:Java 中的每個對象均可以做爲鎖。高併發
具體表現爲如下 3 種形式:
對於普通同步方法,鎖是當前實例對象。
對於靜態同步方法,鎖是當前類的 Class 對象。
對於同步方法塊,鎖是 synchronized 括號裏配置的對象。當一個線程試圖訪問同步代碼塊時,它首先必須獲得鎖,退出或拋出異常時必須釋放鎖。
那麼同步方法(syncronized ) 與 靜態同步方法(static syncronized ) 的有什麼區別呢? 咱們來看一個簡單的例子:
class Phone { public /*static*/ synchronized void sendEmail() throws InterruptedException { TimeUnit.SECONDS.sleep(4); System.out.println("--------sendEmail"); } public /*static*/ synchronized void getMessage() { System.out.println("--------getMessage"); } public void getHello() { System.out.println("--------getHello");}main{ Phone p = new Phone(); p.sendEmail(); p.getMessage(); p.getHello();}}
經過以上代碼回答下面問題:
標準訪問的時候,請問先打印郵件仍是短信?
sendEmail方法暫停4秒鐘,請問先打印郵件仍是短信?
新增Hello廣泛方法,請問先打印郵件仍是Hello?
兩部手機,請問先打印郵件仍是短信?
兩個靜態同步方法,同1部手機 ,請問先打印郵件仍是短信?
兩個靜態同步方法,有2部手機 ,請問先打印郵件仍是短信?
1個靜態同步方法,1個普通同步方法,有1部手機 ,請問先打印郵件仍是短信?
1個靜態同步方法,1個普通同步方法,有2部手機 ,請問先打印郵件仍是短信?
思考一下,咱們再作分析~
一個對象裏面若是有多個 synchronized 方法,某一個時刻內,只要一個線程去調用 其中的一個 synchronized 方法了,其它的線程都只能等待,換句話說,某一個時刻內,只能有惟一一個線程去訪問這些 synchronized 方法;
全部的非靜態同步方法用的都是同一把鎖——實例對象自己,synchronized 方法鎖的是當前對象 this,被鎖定後,其它的線程都不能進入到當前對象的其它 synchronized 方法,也就是說若是一個實例對象的非靜態同步方法獲取鎖後,該實例對象的其餘非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖;
加個普通方法後發現和同步鎖無關;
換成兩個對象後,不是同一把鎖了,毋須等待互不影響。
由於別的實例對象的非靜態同步方法跟該實例對象的非靜態同步方法用的是不一樣的鎖,因此毋須等待;
全部的靜態同步方法用的是同一把鎖——類對象自己(鎖的是類模板),一旦一個靜態同步方法獲取鎖後,其餘的靜態同步方法都必須等待該方法釋放鎖後才能獲取鎖,而不論是同一個實例對象的靜態同步方法之間,仍是不一樣的實例對象的靜態同步方法之間,只要它們是同一個類模板的實例對象就要爭取同一把鎖;
第1和第5中的這兩把鎖是兩個不一樣的對象,因此靜態同步方法與非靜態同步方法之間是不會有競態條件的。
通過分析,答案也就否則而喻了。
簡單回顧以後,回到正文,JUC 中提供了比 synchronized 更加高級的同步結構,包括 CountDownLatch,CyclicBarrier,Semaphone 等能夠實現更加豐富的多線程操做。
另外還提供了各類線程安全的容器 ConcurrentHashMap、有序的 ConcurrentSkipListMap,CopyOnWriteArrayList 等。
CountDownLatch (計數器)
讓一些線程阻塞直到另外一些線程完成一系列操做後才被喚醒。
CountDownLatch 主要有 countDown、await 兩個方法,當一個或多個線程調用 await 方法時,這些線程會阻塞。其它線程調用 countDown 方法會將計數器減 1 (調用 countDown 方法的線程不會阻塞),當計數器的值變爲 0 時,因 await 方法阻塞的線程會被喚醒,繼續執行。
代碼案例:圖書館下班 ,等讀者所有離開後,圖書管理員才能關閉圖書館。
main 主線程必需要等前面線程完成所有工做後,本身才能執行。
public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(5);//參數表明讀者的數量 for (int i = 1; i <= 5 ; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t 號讀者離開了圖書館"); countDownLatch.countDown(); } ,CountryEnum.getKey(i).getName()).start(); } countDownLatch.await(); System.out.println(Thread.currentThread().getName()+"\t ------圖書管理員閉館");}}結果以下:3 號讀者離開了圖書館2 號讀者離開了圖書館4 號讀者離開了圖書館1 號讀者離開了圖書館5 號讀者離開了圖書館main ------圖書管理員閉館
CyclicBarrier (循環屏障)
CyclicBarrier 的字面意思是可循環(Cyclic)使用的屏障(Barrier)。
它要作的事情是,讓一組線程到達一個屏障(也能夠叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,全部被屏障攔截的線程纔會繼續幹活。
線程進入屏障經過 CyclicBarrier 的 await() 方法。
代碼案例:集齊10張卡牌才能夠召開獎
public class CyclicBarrierDemo {private static final int NUMBER = 10;public static void main(String[] args){ //構造方法 CyclicBarrier(int parties,Runnable action) CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Thread(() -> { System.out.println("集齊卡牌 開始開獎"); })); for (int i = 1; i <= NUMBER ; i++) { final int tempInt = i; new Thread(() -> { try { System.out.println(Thread.currentThread().getName()+"\t 收集了"+tempInt+"號卡牌"); cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } } ,String.valueOf(i)).start(); }}}結果以下:1 收集了1號卡牌8 收集了8號卡牌4 收集了4號卡牌3 收集了3號卡牌5 收集了5號卡牌7 收集了7號卡牌9 收集了9號卡牌6 收集了6號卡牌2 收集了2號卡牌 10 收集了10號卡牌集齊卡牌 開始開獎
Semaphone (信號量)
信號量典型應用場景是多個線程搶多個資源。
在信號量上咱們定義兩種操做:
acquire(獲取): 當一個線程調用 acquire 操做時,它要麼經過成功獲取信號量(信號量減 1 ),要麼一直等下去,直到有線程釋放信號量,或超時。
release(釋放):實際上會將信號量的值加 1,而後喚醒等待的線程。
信號量主要用於兩個目的,一個是用於多個共享資源的互斥使用,另外一個用於併發線程數的控制。
代碼案例:停車場停車 ,車搶車位
public class SemaphoreDemo {public static void main(String[] args){ Semaphore semaphore = new Semaphore(3);// 模擬 3 個停車位 for (int i = 1; i <= 6 ; i++) {//6 輛車 new Thread(() -> { try{ semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"\t 搶到停車位"); TimeUnit.SECONDS.sleep(new Random().nextInt(5)); System.out.println(Thread.currentThread().getName()+"\t 離開停車位"); }catch(Exception e){ e.printStackTrace(); }finally{ semaphore.release(); } } ,String.valueOf(i)).start(); }}}結果以下:2 搶到停車位4 搶到停車位1 搶到停車位2 離開停車位6 搶到停車位6 離開停車位5 搶到停車位4 離開停車位1 離開停車位3 搶到停車位3 離開停車位5 離開停車位
接下來,咱們來梳理下併發包裏提供的線程安全的集合類,基本代碼以下:
public class NotSafeDemo { public static void main(String[] args){ //高併發 list List<Object> list = new CopyOnWriteArrayList<>(); /高併發 set Set<Object> objects = new CopyOnWriteArraySet<>(); /高併發 map Map<String,String> map = new ConcurrentHashMap<String,String>(); for (int i = 0; i < 50 ; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0,6)); System.out.println(list); } ,String.valueOf(i)).start(); } } }
CopyOnWrite 容器也被稱爲寫時複製的容器。
往一個容器添加元素的時候,不直接往當前容器 Object[] 添加,而是先將當前容器 Object[] 進行 Copy,複製出一個新的容器 Object[] newElements,而後新的容器 Object[] newElements 裏添加元素,添加完元素以後,再將原容器的引用指向新的容器 setArray(newElements)。
這樣作的好處是能夠對 CopyOnWrite 容器進行併發的讀,而不須要加鎖,由於當前容器不會添加任何元素。因此 CopyOnWrite 容器也是一種讀寫分離的思想,讀和寫不一樣的容器,可是因爲經過對底層數組複製來實現的,通常須要很大的開銷。當遍歷次數大大超過修改次數的時,這種方法比其餘替代方法更有效。部分源碼以下:
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1);//先複製,再添加一個空元素 newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
而帶有 Concurrent 的通常纔是真正的適用併發的工具,ConcurrentHashMap 被認爲是弱一致性的,本質緣由在於 ConcurrentHashMap 在讀數據是並無加鎖。
關於併發集合的應用還要在實際開發中多多體會,實踐纔是最好的老師。
擴展知識:
今天的擴展知識簡單介紹下 Java 經常使用的 4 種線程池:
newCachedThreadPool
建立可緩存的線程,底層是依靠 SynchronousQueue 實現的,建立線程數量幾乎沒有限制(最大爲 Integer.MAX_VALUE)。
若是長時間沒有往線程池提交任務,即若是工做線程空閒了指定時間(默認1分鐘),該工做線程自動終止。終止後若是又有了新的任務,則會建立新的線程。
在使用 CachedTreadPool 時,要注意控制任務數量,不然因爲大量線程同時運行,頗有可能形成系統癱瘓。
newFixedThreadPool
建立指定數量的工做線程,底層是依靠 LinkedBlockingQueue 實現的,沒提交一個任務就建立一個工做線程,當工做線程數量達到線程池初始的最大數,則將提交的任務存入到池隊列中。
在線程空閒時,不會釋放工做線程,還會佔用必定的系統資源。
newSingleThreadExecutor
建立單線程,底層是 LinkedBlockingQueue 實現的,它只會用一個工做線程來執行任務,保證全部的任務按指定順序執行。若是這個線程異常結束,會有另外一個取代它,保證順序執行。
最大的特色是可保證順序地執行各個任務,並在任意時間是不會有過個線程活動的。
newScheduleThreadPool
建立一個定長的線程池,支持定時以及週期性的任務調度。
參考資料:
https://github.com/fanpengyi/java-util-concurrent.git ---- 文中代碼git庫
關注一下,我寫的就更來勁兒啦