【Java編程的思想】併發總結

線程安全的機制

線程表示一條單獨的執行流,每個線程有自己的執行計數器,有自己的棧,但可以共享內存,共享內存是實現線程協作的基礎,但共享內存有兩個問題:競態條件和內存可見性。

synchronized

synchronized是一個關鍵字,既可以解決競態問題,也可以解決內存可見性問題

synchronized保護的是對象,而不是代碼,只有對同一個對象的synchronized方法調用,synchronized才能保證它們被順序調用。對於實例方法,這個對象是this;對於靜態方法,這個對象是類對象;對於代碼塊,需要指定哪個對象

synchronized不能嘗試獲取鎖,也不能響應中斷,還可能會死鎖。相比於顯式鎖,synchronized簡單易用,JVM也在不斷優化它的實現

顯式鎖

顯式鎖是相當於synchronized隱式鎖而言的,它可以實現synchronized同樣的功能,但需要程序自己創建鎖,調用鎖相關接口,主要接口是Lock,主要實現類是ReentrantLock。

相比synchronized,顯式鎖支持以非阻塞方式獲取鎖,可以響應中斷,可以限時,可以指定公平性,可以解決死鎖問題,所以更加的靈活。

在一些讀多寫少、讀操作可以完全並行的場景中,可以使用讀寫鎖以提高併發度,讀寫鎖的接口是ReadWriteLock,實現類是ReentrantReadWriteLock

volatile

synchronized和顯式鎖都是鎖,使用鎖可以實現安全,但使用鎖是有成本的,獲取不到鎖的線程還需要等待,會有線程的上下文切換開銷等。
如果共享的對象只有一個,操作也只是進行最簡單的get/set操作,set也不依賴於之前的值,那就不存在競態條件問題,而只有內存可見性問題,這時,在變量的聲明上加上關鍵字volatile就可以了。

volatile和synchronized的區別:

  1. volatile 僅能使用在變量級別; synchronized 則可以使用在實例方法、靜態方法和代碼塊。
  2. volatile 僅能實現變量的修改可見性,並不能保證原子性;synchronized 則可以保證變量的修改可見性和原子性
  3. volatile 不會造成線程的阻塞;synchronized 可能會造成線程的阻塞。
  4. volatile 標記的變量不會被編譯器優化; synchronized 標記的變量可以被編譯器優化

原子變量和CAS

使用volatile,set的新值不能依賴於舊值,但很多時候,set的新值與原來的值有關,同時也不一定需要鎖,這個時候就可以考慮原子變量。它們包含了一些以原子方式實現組合操作的方法。

原子變量的基礎是CAS,一般的計算機系統都在硬件層次上直接支持CAS指令。相對於synchronized,它是樂觀的,而synchronized是悲觀的。

寫時複製

之所有會有線程安全的問題,是因爲多個線程併發讀寫同一個對象,如果每個線程讀寫的對象都是不同的,或者如果共享訪問的對象是隻讀的,不能修改,那就不存在線程安全問題了。

寫時複製就是將共享訪問的對象變爲只讀的,寫的時候再使用鎖,保證只有一個線程寫,寫的線程不是直接修改原對象,而是新創建一個對象,對該對象修改完畢後,再原子性地修改共享訪問的變量,讓它指向新的對象。

ThreadLocal

ThreadLocal讓每個線程對同一變量,都有自己的獨有副本。每個線程實際訪問的對象都是自己的,自然也就不存在線程安全問題。

線程的協作機制

常見的協作場景:生產者/消費者協作模式、主從協作模式、同時開始、集合點等

wait/notify

wait/notify與synchronized配合一起使用,是線程的基本協作機制。
每個對象都有一把鎖和兩個等待隊列,一個是鎖等待隊列,放的是等待獲取鎖的線程;另一個是條件等待隊列,放的是等待條件的線程。wait將自己加入條件等待隊列,notify從條件隊列上移除一個線程並喚醒,notifyAll移除所有線程並喚醒。

wait/notify方法只能在synchronized代碼塊內被調用,調用wait時,線程會釋放對象鎖,被notify/notifyAll喚醒後,需要重新競爭鎖,獲取到鎖後纔會從wait調用中返回。

顯式條件

顯式條件和顯式鎖配合使用,與wait/notify相比,可以支持多個條件隊列,代碼更爲易讀,效率更高。

線程中斷

線程中斷並不是強迫終止一個線程,它是一種協作機制,是給線程傳遞一個取消信號,但是由線程來決定如何以及何時退出,線程在不同狀態和IO操作時對中斷有不同的反應。

協作工具類

信號量Semaphore用於限制對資源的併發訪問數

倒計時門閂CountDownLatch主要用於不同角色線程間的同步,比如:同時開始多個線程;主線程等待多個從線程的結果

循環柵欄CyclicBarrier用於同一角色線程間的協調一致,所有線程在到達柵欄後都需要等待其他線程,等所有線程都到達後再一起通過,它是可以循環的。

阻塞隊列

阻塞隊列封裝了鎖和條件,常用於生產者/消費者協作模式,只需要調用隊列的入隊/出隊方法就可以了

  • 無鎖非阻塞併發隊列:ConcurrentLinkedQueue和ConcurrentLinkedDeque
  • 普通阻塞隊列:基於數組的ArrayBlockingQueue,基於鏈表的LinkedBlockingQueue和LinkedBlockingDeque
  • 優先級阻塞隊列:PriorityBlockingQueue
  • 延時阻塞隊列:DelayQueue
  • 其他阻塞隊列:SynchronousQueue和LinkedTransferQueue

Future/FutureTask

Future是一個接口,主要實現類是FutureTask。
Future封裝了調用線程和執行線程關於執行狀態和結果的同步,對於調用線程而言,它只需要通過Future就可以查詢異步任務的狀態、獲取最終結果、取消任務等。
在常見的主從協作模式中,主線程往往需要獲取子線程的結果,就可以使用Future

容器類

線程安全的容器有兩類:同步容器;併發容器

同步容器

Collections類中有一些靜態方法,可以基於普通容器返回線程安全的同步容器

public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)

它們是給所有容器方法都加上synchronized來實現安全的。
同步容器的性能比較低,這裏的線程安全針對的是容器對象,指的是當多個線程併發訪問同一個容器對象時,不需要額外的同步操作。

併發容器

寫時複製的List和Set

CopyOnWriteArrayList實現了List接口,它的用法與其他的List基本是一樣的。CopyOnWirteArrayList的內部也是 一個數組,但這個數組是以原子方式被整體更新的。每次修改操作,都會新建一個數組,複製原數組的內容到新數組,在新數組上進行需要的修改,然後以原子方式設置內部的數組引用。
CopyOnWriteArraySet實現了Set接口,不包含重複的元素。 內部是通過CopyOnWriteArrayList實現的

CopyOnWriteArrayList和CopyOnWriteArraySet適用於讀遠多於寫、集合不太大的場景。它們是以優化讀操作爲目標的,讀不需要同步,性能很高。

ConcurrentHashMap

ConcurrentHashMap是HashMap的併發版本,通過細粒度鎖和其他技術實現了高併發,讀操作完全並行,寫操作支持一定程度的並行,以原子方式支持一些複合操作,迭代不用加鎖。

基於跳錶的Map和Set

Java併發包中與TreeMap/TreeSet對應的併發版本是ConcurrentSkipListMap和ConcurrentSkipListSet。

TreeSet是基於TreeMap實現的,類似地,ConcurrentSkipListSet也是以及ConcurrentSkipListMap實現的。

ConcurrentSkipListMap是基於SkipList實現的,SkipList稱爲跳躍表或跳錶,是一種數據結構。

併發隊列

  • 無鎖非阻塞併發隊列:ConcurrentLinkedQueue和ConcurrentLinkedDeque
  • 普通阻塞隊列:基於數組的ArrayBlockingQueue,基於鏈表的LinkedBlockingQueue和LinkedBlockingDeque
  • 優先級阻塞隊列:PriorityBlockingQueue
  • 延時阻塞隊列:DelayQueue
  • 其他阻塞隊列:SynchronousQueue和LinkedTransferQueue

無鎖非阻塞是指,這些隊列不實用鎖,所有操作總是立即執行,主要通過循環CAS實現併發安全;
阻塞隊列是指,這些隊列使用鎖和條件,很多操作都需要先獲取鎖或滿足特點條件,獲取不到鎖或等待 條件時,會等待(阻塞),直到獲取到鎖或條件滿足

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章