本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營連接:http://item.jd.com/12299018.htmlhtml
從65節到82節,咱們用了18篇文章討論併發,本節進行簡要總結。java
多線程開發有兩個核心問題,一個是競爭,另外一個是協做。競爭會出現線程安全問題,因此,本節首先總結線程安全的機制,而後是協做的機制。管理競爭和協做是複雜的,因此Java提供了更高層次的服務,好比並發容器類和異步任務執行服務,咱們也會進行總結。本節綱要以下:程序員
線程安全的機制算法
線程表示一條單獨的執行流,每一個線程有本身的執行計數器,有本身的棧,但能夠共享內存,共享內存是實現線程協做的基礎,但共享內存有兩個問題,競態條件和內存可見性,以前章節探討了解決這些問題的多種思路:編程
synchronized數組
synchronized簡單易用,它只是一個關鍵字,大部分狀況下,放到類的方法聲明上就能夠了,既能夠解決競態條件問題,也能夠解決內存可見性問題。安全
須要理解的是,它保護的是對象,而不是代碼,只有對同一個對象的synchronized方法調用,synchronized才能保證它們被順序調用。對於實例方法,這個對象是this,對於靜態方法,這個對象是類對象,對於代碼塊,須要指定哪一個對象。微信
另外,須要注意,它不能嘗試獲取鎖,也不響應中斷,還可能會死鎖。不過,相比顯式鎖,synchronized簡單易用,JVM也能夠不斷優化它的實現,應該被優先使用。數據結構
顯式鎖多線程
顯式鎖是相對於synchronized隱式鎖而言的,它能夠實現synchronzied一樣的功能,但須要程序員本身建立鎖,調用鎖相關的接口,主要接口是Lock,主要實現類是ReentrantLock。
相比synchronized,顯式鎖支持以非阻塞方式獲取鎖、能夠響應中斷、能夠限時、能夠指定公平性、能夠解決死鎖問題,這使得它靈活的多。
在讀多寫少、讀操做能夠徹底並行的場景中,可使用讀寫鎖以提升併發度,讀寫鎖的接口是ReadWriteLock,實現類是ReentrantReadWriteLock。
volatile
synchronized和顯式鎖都是鎖,使用鎖能夠實現安全,但使用鎖是有成本的,獲取不到鎖的線程還須要等待,會有線程的上下文切換開銷等。保證安全不必定須要鎖。若是共享的對象只有一個,操做也只是進行最簡單的get/set操做,set也不依賴於以前的值,那就不存在競態條件問題,而只有內存可見性問題,這時,在變量的聲明上加上volatile就能夠了。
原子變量和CAS
使用volatile,set的新值不能依賴於舊值,但不少時候,set的新值與原來的值有關,這時,也不必定須要鎖,若是須要同步的代碼比較簡單,能夠考慮原子變量,它們包含了一些以原子方式實現組合操做的方法,對於併發環境中的計數、產生序列號等需求,考慮使用原子變量而非鎖。
原子變量的基礎是CAS,比較並設置,通常的計算機系統都在硬件層次上直接支持CAS指令。經過循環CAS的方式實現原子更新是一種重要的思惟,相比synchronized,它是樂觀的,而synchronized是悲觀的,它是非阻塞式的,而synchronized是阻塞式的。CAS是Java併發包的基礎,基於它能夠實現高效的、樂觀、非阻塞式數據結構和算法,它也是併發包中鎖、同步工具和各類容器的基礎。
寫時複製
之因此會有線程安全的問題,是由於多個線程併發讀寫同一個對象,若是每一個線程讀寫的對象都是不一樣的,或者,若是共享訪問的對象是隻讀的,不能修改,那也就不存在線程安全問題了。
咱們在介紹容器類CopyOnWriteArrayList和CopyOnWriteArraySet時介紹了寫時複製技術,寫時複製就是將共享訪問的對象變爲只讀的,寫的時候,再使用鎖,保證只有一個線程寫,寫的線程不是直接修改原對象,而是新建立一個對象,對該對象修改完畢後,再原子性地修改共享訪問的變量,讓它指向新的對象。
ThreadLocal
ThreadLocal就是讓每一個線程,對同一個變量,都有本身的獨有拷貝,每一個線程實際訪問的對象都是本身的,天然也就不存在線程安全問題了。
線程的協做機制
多線程之間的核心問題,除了競爭,就是協做。咱們在67節和68節介紹了多種協做場景,好比生產者/消費者協做模式、主從協做模式、同時開始、集合點等。以前章節探討了協做的多種機制:
wait/notify
wait/notify與synchronized配合一塊兒使用,是線程的基本協做機制,每一個對象都有一把鎖和兩個等待隊列,一個是鎖等待隊列,放的是等待獲取鎖的線程,另外一個是條件等待隊列,放的是等待條件的線程,wait將本身加入條件等待隊列,notify從條件等待隊列上移除一個線程並喚醒,notifyAll移除全部線程並喚醒。
須要注意的是,wait/notify方法只能在synchronized代碼塊內被調用,調用wait時,線程會釋放對象鎖,被notify/notifyAll喚醒後,要從新競爭對象鎖,獲取到鎖後纔會從wait調用中返回,返回後,不表明其等待的條件就必定成立了,須要從新檢查其等待的條件。
wait/notify方法看上去很簡單,但每每難以理解wait等的究竟是什麼,而notify通知的又是什麼,只能有一個條件等待隊列,這也是wait/notify機制的侷限性,這使得對於等待條件的分析變得複雜,67節和68節經過多個例子演示了其用法,這裏就不贅述了。
顯式條件
顯式條件與顯式鎖配合使用,與wait/notify相比,能夠支持多個條件隊列,代碼更爲易讀,效率更高,使用時注意不要將signal/signalAll誤寫爲notify/notifyAll。
中斷
Java中取消/關閉一個線程的方式是中斷,中斷並非強迫終止一個線程,它是一種協做機制,是給線程傳遞一個取消信號,可是由線程來決定如何以及什麼時候退出,線程在不一樣狀態和IO操做時對中斷有不一樣的反應,做爲線程的實現者,應該提供明確的取消/關閉方法,並用文檔清楚描述其行爲,做爲線程的調用者,應該使用其取消/關閉方法,而不是貿然調用interrupt。
協做工具類
除了基本的顯式鎖和條件,針對常見的協做場景,Java併發包提供了多個用於協做的工具類。
信號量類Semaphore用於限制對資源的併發訪問數。
倒計時門栓CountDownLatch主要用於不一樣角色線程間的同步,好比在"裁判"-"運動員"模式中,"裁判"線程讓多個"運動員"線程同時開始,也能夠用於協調主從線程,讓主線程等待多個從線程的結果。
循環柵欄CyclicBarrier用於同一角色線程間的協調一致,全部線程在到達柵欄後都須要等待其餘線程,等全部線程都到達後再一塊兒經過,它是循環的,能夠用做重複的同步。
阻塞隊列
對於最多見的生產者/消費者協做模式,可使用阻塞隊列,阻塞隊列封裝了鎖和條件,生產者線程和消費者線程只須要調用隊列的入隊/出隊方法就能夠了,不須要考慮同步和協做問題。
阻塞隊列有普通的先進先出隊列,包括基於數組的ArrayBlockingQueue和基於鏈表的LinkedBlockingQueue/LinkedBlockingDeque,也有基於堆的優先級阻塞隊列PriorityBlockingQueue,還有可用於定時任務的延時阻塞隊列DelayQueue,以及用於特殊場景的阻塞隊列SynchronousQueue和LinkedTransferQueue。
Future/FutureTask
在常見的主從協做模式中,主線程每每是讓子線程異步執行一項任務,獲取其結果,手工建立子線程的寫法每每比較麻煩,常見的模式是使用異步任務執行服務,再也不手工建立線程,而只是提交任務,提交後立刻獲得一個結果,但這個結果不是最終結果,而是一個Future,Future是一個接口,主要實現類是FutureTask。
Future封裝了主線程和執行線程關於執行狀態和結果的同步,對於主線程而言,它只須要經過Future就能夠查詢異步任務的狀態、獲取最終結果、取消任務等,不須要再考慮同步和協做問題。
容器類
線程安全的容器有兩類,一類是同步容器,另外一類是併發容器。在理解synchronized一節,咱們介紹了同步容器。關於併發容器,咱們介紹了:
同步容器
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來實現安全的。同步容器的性能比較低,另外,還須要注意一些問題,好比複合操做和迭代,須要調用方手工使用synchronized同步,並注意不要同步錯對象。
而併發容器是專爲併發而設計的,線程安全、併發度更高、性能更高、迭代不會拋出ConcurrentModificationException、不少容器以原子方式支持一些複合操做。
寫時拷貝的List和Set
CopyOnWriteArrayList基於數組實現了List接口,CopyOnWriteArraySet基於CopyOnWriteArrayList實現了Set接口,它們採用了寫時拷貝,適用於讀遠多於寫,集合不太大的場合。不適用於數組很大,且修改頻繁的場景。它們是以優化讀操做爲目標的,讀不須要同步,性能很高,但在優化讀的同時就犧牲了寫的性能。
ConcurrentHashMap
HashMap不是線程安全的,在併發更新的狀況下,HashMap的鏈表結構可能造成環,出現死循環,佔滿CPU。ConcurrentHashMap是併發版的HashMap,經過分段鎖和其餘技術實現了高併發,讀操做徹底並行,寫操做支持必定程度的並行,以原子方式支持一些複合操做,迭代不用加鎖,不會拋出ConcurrentModificationException。
基於SkipList的Map和Set
ConcurrentHashMap不能排序,容器類中能夠排序的Map和Set是TreeMap和TreeSet,但它們不是線程安全的。Java併發包中與TreeMap/TreeSet對應的併發版本是ConcurrentSkipListMap和ConcurrentSkipListSet。ConcurrentSkipListMap是基於SkipList實現的,SkipList稱爲跳躍表或跳錶,是一種數據結構,主要操做複雜度爲O(log(N)),併發版本採用跳錶而不是樹,是由於跳錶更易於實現高效併發算法。
ConcurrentSkipListMap沒有使用鎖,全部操做都是無阻塞的,全部操做均可以並行,包括寫。與ConcurrentHashMap相似,迭代器不會拋出ConcurrentModificationException,是弱一致的,也直接支持一些原子複合操做。
各類隊列
各類阻塞隊列主要用於協做,非阻塞隊列適用於多個線程併發使用一個隊列的場合,有兩個非阻塞隊列,ConcurrentLinkedQueue和ConcurrentLinkedDeque,ConcurrentLinkedQueue實現了Queue接口,表示一個先進先出的隊列,ConcurrentLinkedDeque實現了Deque接口,表示一個雙端隊列。它們都是基於鏈表實現的,都沒有限制大小,是無界的,這兩個類最基礎的實現原理是循環CAS,沒有使用鎖。
任務執行服務
關於任務執行服務,咱們介紹了:
基本概念
任務執行服務大大簡化了執行異步任務所需的開發,它引入了一個"執行服務"的概念,將"任務的提交"和"任務的執行"相分離,"執行服務"封裝了任務執行的細節,對於任務提交者而言,它能夠關注於任務自己,如提交任務、獲取結果、取消任務,而不須要關注任務執行的細節,如線程建立、任務調度、線程關閉等。
任務執行服務主要涉及如下接口:
使用者只須要經過ExecutorService提交任務,經過Future操做任務和結果便可,不須要關注線程建立和協調的細節。
線程池
任務執行服務的主要實現機制是線程池,實現類是ThreadPoolExecutor,線程池主要由兩個概念組成,一個是任務隊列,另外一個是工做者線程。任務隊列是一個阻塞隊列,保存待執行的任務。工做者線程主體就是一個循環,循環從隊列中接受任務並執行。ThreadPoolExecutor有一些重要的參數,理解這些參數對於合理使用線程池很是重要,78節對這些參數進行了詳細介紹,這裏就不贅述了。
ThreadPoolExecutor實現了生產者/消費者模式,工做者線程就是消費者,任務提交者就是生產者,線程池本身維護任務隊列。當咱們碰到相似生產者/消費者問題時,應該優先考慮直接使用線程池,而非從新發明輪子,本身管理和維護消費者線程及任務隊列。
CompletionService
在異步任務程序中,一種場景是,主線程提交多個異步任務,而後但願有任務完成就處理結果,而且按任務完成順序逐個處理,對於這種場景,Java併發包提供了一個方便的方法,使用CompletionService,這是一個接口,它的實現類是ExecutorCompletionService,它經過一個額外的結果隊列,方便了對於多個異步任務結果的處理。
定時任務
異步任務中,常見的任務是定時任務。在Java中,有兩種方式實現定時任務:
Timer有一些須要特別注意的事項:
ScheduledExecutorService的主要實現類是ScheduledThreadPoolExecutor,它沒有Timer的問題:
因此,實踐中建議使用ScheduledExecutorService。
小結
針對多線程開發的兩個核心問題,競爭和協做,本節總結了線程安全和協做的多種機制,針對高層服務,本節總結了併發容器和任務執行服務,它們讓咱們在更高的層次上訪問共享的數據結構,執行任務,而避免陷入線程管理的細節。到此爲止,關於併發咱們就告一段落了。
與以前章節同樣,咱們的探討都是基於Java 7的,不過Java 7引入了一個Fork/Join框架,咱們沒有討論。Java 8在併發方面也有一些更新,好比:
關於這些內容,咱們在探討Java 8的時候再繼續討論。
從下一節開始,咱們來探討Java中的一些動態特性,好比反射、註解、動態代理等,它們究竟是什麼呢?
---------------------
併發相關原創文章
(75) 併發容器 - 基於SkipList的Map和Set
----------------
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。