什麼是線程?
線程是操做系統可以進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運做單位,能夠使用多線程對進行運算提速。html
好比,若是一個線程完成一個任務要100毫秒,那麼用十個線程完成改任務只需10毫秒java
什麼是線程安全和線程不安全?
通俗的說:加鎖的就是是線程安全的,不加鎖的就是是線程不安全的git
線程安全
線程安全: 就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問,直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致或者數據污染。程序員
一個線程安全的計數器類的同一個實例對象在被多個線程使用的狀況下也不會出現計算失誤。很顯然你能夠將集合類分紅兩組,線程安全和非線程安全的。 Vector 是用同步方法來實現線程安全的, 而和它類似的ArrayList不是線程安全的。github
線程不安全
線程不安全:就是不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據是髒數據面試
若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。算法
線程安全問題都是由全局變量及靜態變量引發的。 若每一個線程中對全局變量、靜態變量只有讀操做,而無寫操做,通常來講,這個全局變量是線程安全的;如有多個線程同時執行寫操做,通常都須要考慮線程同步,不然的話就可能影響線程安全。spring
什麼是自旋鎖?
基本概念
自旋鎖是SMP架構中的一種low-level的同步機制。數據庫
當線程A想要獲取一把自選鎖而該鎖又被其它線程鎖持有時,線程A會在一個循環中自選以檢測鎖是否是已經可用了。編程
自選鎖須要注意:
-
因爲自旋時不釋放CPU,於是持有自旋鎖的線程應該儘快釋放自旋鎖,不然等待該自旋鎖的線程會一直在那裏自旋,這就會浪費CPU時間。
-
持有自旋鎖的線程在sleep以前應該釋放自旋鎖以便其它線程能夠得到自旋鎖。
實現自旋鎖
參考
一個簡單的while就能夠知足你的要求。
目前的JVM實現自旋會消耗CPU,若是長時間不調用doNotify方法,doWait方法會一直自旋,CPU會消耗太大。
public class MyWaitNotify3{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
什麼是Java內存模型?
Java內存模型描述了在多線程代碼中哪些行爲是合法的,以及線程如何經過內存進行交互。它描述了「程序中的變量「 和 」從內存或者寄存器獲取或存儲它們的底層細節」之間的關係。Java內存模型經過使用各類各樣的硬件和編譯器的優化來正確實現以上事情。
Java包含了幾個語言級別的關鍵字,包括:volatile, final以及synchronized,目的是爲了幫助程序員向編譯器描述一個程序的併發需求。Java內存模型定義了volatile和synchronized的行爲,更重要的是保證了同步的java程序在全部的處理器架構下面都能正確的運行。
「一個線程的寫操做對其餘線程可見」這個問題是由於編譯器對代碼進行重排序致使的。例如,只要代碼移動不會改變程序的語義,當編譯器認爲程序中移動一個寫操做到後面會更有效的時候,編譯器就會對代碼進行移動。若是編譯器推遲執行一個操做,其餘線程可能在這個操做執行完以前都不會看到該操做的結果,這反映了緩存的影響。
此外,寫入內存的操做可以被移動到程序裏更前的時候。在這種狀況下,其餘的線程在程序中可能看到一個比它實際發生更早的寫操做。全部的這些靈活性的設計是爲了經過給編譯器,運行時或硬件靈活性使其能在最佳順序的狀況下來執行操做。在內存模型的限定以內,咱們可以獲取到更高的性能。
看下面代碼展現的一個簡單例子:
Cla***eordering { int x = 0, y = 0; public void writer() { x = 1; y = 2; } public void reader() { int r1 = y; int r2 = x; } }
讓咱們看在兩個併發線程中執行這段代碼,讀取Y變量將會獲得2這個值。由於這個寫入比寫到X變量更晚一些,程序員可能認爲讀取X變量將確定會獲得1。可是,寫入操做可能被重排序過。若是重排序發生了,那麼,就能發生對Y變量的寫入操做,讀取兩個變量的操做緊隨其後,並且寫入到X這個操做能發生。程序的結果多是r1變量的值是2,可是r2變量的值爲0。
可是面試官,有時候不這麼認爲,認爲就是JVM內存結構
JVM內存結構主要有三大塊:堆內存、方法區和棧。
堆內存是JVM中最大的一塊由年輕代和老年代組成,而年輕代內存又被分紅三部分,Eden空間、From Survivor空間、To Survivor空間,默認狀況下年輕代按照8:1:1的比例來分配;方法區存儲類信息、常量、靜態變量等數據,是線程共享的區域,爲與Java堆區分,方法區還有一個別名Non-Heap(非堆);棧又分爲java虛擬機棧和本地方法棧主要用於方法的執行。
JAVA的JVM的內存可分爲3個區:堆(heap)、棧(stack)和方法區(method)
java堆(Java Heap)
-
可經過參數 -Xms 和-Xmx設置
-
Java堆是被全部線程共享,是Java虛擬機所管理的內存中最大的一塊 Java堆在虛擬機啓動時建立。
-
Java堆惟一的目的是存放對象實例,幾乎全部的對象實例和數組都在這裏。
-
Java堆爲了便於更好的回收和分配內存,能夠細分爲:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor區。
-
新生代:包括Eden區、From Survivor區、To Survivor區,系統默認大小Eden:Survivor=8:1。
-
老年代:在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。
-
Survivor空間等Java堆能夠處在物理上不連續的內存空間中,只要邏輯上是連續的便可(就像咱們的磁盤空間同樣。在實現時,既能夠實現成固定大小的,也能夠是可擴展的)。
據Java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
java虛擬機棧(stack)
可經過參數 棧幀是方法運行期的基礎數據結構棧容量可由-Xss設置
1.Java虛擬機棧是線程私有的,它的生命週期與線程相同。
-
每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
-
虛擬機棧是執行Java方法的內存模型(也就是字節碼)服務:每一個方法在執行的同時都會建立一個棧幀,用於存儲 局部變量表、操做數棧、動態連接、方法出口等信息。
-
局部變量表:32位變量槽,存放了編譯期可知的各類基本數據類型、對象引用、returnAddress類型。
-
操做數棧:基於棧的執行引擎,虛擬機把操做數棧做爲它的工做區,大多數指令都要從這裏彈出數據、執行運算,而後把結果壓回操做數棧。
-
動態鏈接:每一個棧幀都包含一個指向運行時常量池(方法區的一部分)中該棧幀所屬方法的引用。持有這個引用是爲了支持方法調用過程當中的動態鏈接。Class文件的常量池中有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用一部分會在類加載階段或第一次使用的時候轉化爲直接引用,這種轉化稱爲靜態解析。另外一部分將在每一次的運行期間轉化爲直接應用,這部分稱爲動態鏈接
-
方法出口:返回方法被調用的位置,恢復上層方法的局部變量和操做數棧,若是無返回值,則把它壓入調用者的操做數棧。
-
局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的。
-
在方法運行期間不會改變局部變量表的大小。主要存放了編譯期可知的各類基本數據類型、對象引用 (reference類型)、returnAddress類型)。
java虛擬機棧,規定了兩種異常情況:
-
若是線程請求的深度大於虛擬機所容許的深度,將拋出StackOverflowError異常。
-
若是虛擬機棧動態擴展,而擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。
本地方法棧
可經過參數 棧容量可由-Xss設置
-
虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務。
-
本地方法棧則是爲虛擬機使用到的Native方法服務。有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一
方法區(Method Area)
可經過參數-XX:MaxPermSize設置
-
線程共享內存區域,用於儲存已被虛擬機加載的類信息、常量、靜態變量,即編譯器編譯後的代碼,方法區也稱持久代(Permanent Generation)。
-
雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作Non-Heap(非堆),目的應該是與Java堆區分開來。
-
如何實現方法區,屬於虛擬機的實現細節,不受虛擬機規範約束。
-
方法區主要存放java類定義信息,與垃圾回收關係不大,方法區能夠選擇不實現垃圾回收,但不是沒有垃圾回收。
-
方法區域的內存回收目標主要是針對常量池的回收和對類型的卸載。
-
運行時常量池,也是方法區的一部分,虛擬機加載Class後把常量池中的數據放入運行時常量池。
運行時常量池
JDK1.6以前字符串常量池位於方法區之中。 JDK1.7字符串常量池已經被挪到堆之中。
可經過參數-XX:PermSize和-XX:MaxPermSize設置
-
常量池(Constant Pool):常量池數據編譯期被肯定,是Class文件中的一部分。存儲了類、方法、接口等中的常量,固然也包括字符串常量。
-
字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存儲編譯期類中產生的字符串類型數據。
-
運行時常量池(Runtime Constant Pool):方法區的一部分,全部線程共享。虛擬機加載Class後把常量池中的數據放入到運行時常量池。常量池:能夠理解爲Class文件之中的資源倉庫,它是Class文件結構中與其餘項目資源關聯最多的數據類型。
-
常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic Reference)。
-
字面量:文本字符串、聲明爲final的常量值等。
-
符號引用:類和接口的徹底限定名(Fully Qualified Name)、字段的名稱和描述符(Descriptor)、方法的名稱和描述符。
直接內存
可經過-XX:MaxDirectMemorySize指定,若是不指定,則默認與Java堆的最大值(-Xmx指定)同樣。
-
直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域,可是這部份內存也被頻繁地使用,並且也可能致使OutOfMemoryError異常出現。
總結的簡單一點
java堆(Java Heap)
可經過參數 -Xms 和-Xmx設置
-
Java堆是被全部線程共享,是Java虛擬機所管理的內存中最大的一塊 Java堆在虛擬機啓動時建立
-
Java堆惟一的目的是存放對象實例,幾乎全部的對象實例和數組都在這裏
-
Java堆爲了便於更好的回收和分配內存,能夠細分爲:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor區
-
新生代:包括Eden區、From Survivor區、To Survivor區,系統默認大小Eden:Survivor=8:1。
-
老年代:在年輕代中經歷了N次垃圾回收後仍然存活的對象,就會被放到年老代中。所以,能夠認爲年老代中存放的都是一些生命週期較長的對象。
java虛擬機棧(stack)
可經過參數 棧幀是方法運行期的基礎數據結構棧容量可由-Xss設置
-
Java虛擬機棧是線程私有的,它的生命週期與線程相同。
-
每個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
-
虛擬機棧是執行Java方法的內存模型(也就是字節碼)服務:每一個方法在執行的同時都會建立一個棧幀,用於存儲 局部變量表、操做數棧、動態連接、方法出口等信息
方法區(Method Area)
可經過參數-XX:MaxPermSize設置
-
線程共享內存區域),用於儲存已被虛擬機加載的類信息、常量、靜態變量,即編譯器編譯後的代碼,方法區也稱持久代(Permanent Generation)。
-
方法區主要存放java類定義信息,與垃圾回收關係不大,方法區能夠選擇不實現垃圾回收,但不是沒有垃圾回收。
-
方法區域的內存回收目標主要是針對常量池的回收和對類型的卸載。
-
運行時常量池,也是方法區的一部分,虛擬機加載Class後把常量池中的數據放入運行時常量池。
什麼是CAS?
CAS(compare and swap)的縮寫,中文翻譯成比較並交換。
CAS 不經過JVM,直接利用java本地方 JNI(Java Native Interface爲JAVA本地調用),直接調用CPU 的cmpxchg(是彙編指令)指令。
利用CPU的CAS指令,同時藉助JNI來完成Java的非阻塞算法,實現原子操做。其它原子操做都是利用相似的特性完成的。
整個java.util.concurrent都是創建在CAS之上的,所以對於synchronized阻塞算法,J.U.C在性能上有了很大的提高。
CAS是項樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程並不會被掛起,而是被告知此次競爭中失敗,並能夠再次嘗試。
CAS應用
CAS有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。
CAS優勢
確保對內存的讀-改-寫操做都是原子操做執行
CAS缺點
CAS雖然很高效的解決原子操做,可是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操做
總結
-
使用CAS在線程衝突嚴重時,會大幅下降程序性能;CAS只適合於線程衝突較少的狀況使用。
-
synchronized在jdk1.6以後,已經改進優化。synchronized的底層實現主要依靠Lock-Free的隊列,基本思路是自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但得到了高吞吐量。在線程衝突較少的狀況下,能夠得到和CAS相似的性能;而線程衝突嚴重的狀況下,性能遠高於CAS。
參考 blog.52itstyle.com/archives/94…
什麼是樂觀鎖和悲觀鎖?
悲觀鎖
Java在JDK1.5以前都是靠synchronized關鍵字保證同步的,這種經過使用一致的鎖定協議來協調對共享狀態的訪問,能夠確保不管哪一個線程持有共享變量的鎖,都採用獨佔的方式來訪問這些變量。獨佔鎖其實就是一種悲觀鎖,因此能夠說synchronized是悲觀鎖。
樂觀鎖
樂觀鎖( Optimistic Locking)實際上是一種思想。相對悲觀鎖而言,樂觀鎖假設認爲數據通常狀況下不會形成衝突,因此在數據進行提交更新的時候,纔會正式對數據的衝突與否進行檢測,若是發現衝突了,則讓返回用戶錯誤的信息,讓用戶決定如何去作。
什麼是AQS?
AbstractQueuedSynchronizer簡稱AQS,是一個用於構建鎖和同步容器的框架。事實上concurrent包內許多類都是基於AQS構建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解決了在實現同步容器時設計的大量細節問題。
AQS使用一個FIFO的隊列表示排隊等待鎖的線程,隊列頭節點稱做「哨兵節點」或者「啞節點」,它不與任何線程關聯。其餘的節點與等待線程關聯,每一個節點維護一個等待狀態waitStatus。
CAS 原子操做在concurrent包的實現
參考 blog.52itstyle.com/archives/94…
因爲java的CAS同時具備 volatile 讀和volatile寫的內存語義,所以Java線程之間的通訊如今有了下面四種方式:
-
A線程寫volatile變量,隨後B線程讀這個volatile變量。
-
A線程寫volatile變量,隨後B線程用CAS更新這個volatile變量。
-
A線程用CAS更新一個volatile變量,隨後B線程用CAS更新這個volatile變量。
-
A線程用CAS更新一個volatile變量,隨後B線程讀這個volatile變量。
Java的CAS會使用現代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內存執行讀-改-寫操做,這是在多處理器中實現同步的關鍵(從本質上來講,可以支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,所以任何現代的多處理器都會去支持某種能對內存執行原子性讀-改-寫操做的原子指令)。同時,volatile變量的讀/寫和CAS能夠實現線程之間的通訊。把這些特性整合在一塊兒,就造成了整個concurrent包得以實現的基石。若是咱們仔細分析concurrent包的源代碼實現,會發現一個通用化的實現模式:
首先,聲明共享變量爲volatile; 而後,使用CAS的原子條件更新來實現線程之間的同步;
同時,配合以volatile的讀/寫和CAS所具備的volatile讀和寫的內存語義來實現線程之間的通訊。
AQS,非阻塞數據結構和原子變量類(Java.util.concurrent.atomic包中的類),這些concurrent包中的基礎類都是使用這種模式來實現的,而concurrent包中的高層類又是依賴於這些基礎類來實現的。從總體來看,concurrent包的實現示意圖以下:
AQS沒有鎖之類的概念,它有個state變量,是個int類型,在不一樣場合有着不一樣含義。
AQS圍繞state提供兩種基本操做「獲取」和「釋放」,有條雙向隊列存放阻塞的等待線程,並提供一系列判斷和處理方法,簡單說幾點:
-
state是獨佔的,仍是共享的;
-
state被獲取後,其餘線程須要等待;
-
state被釋放後,喚醒等待線程;
-
線程等不及時,如何退出等待。
至於線程是否能夠得到state,如何釋放state,就不是AQS關心的了,要由子類具體實現。
AQS中還有一個表示狀態的字段state,例如ReentrantLocky用它表示線程重入鎖的次數,Semaphore用它表示剩餘的許可數量,FutureTask用它表示任務的狀態。對state變量值的更新都採用CAS操做保證更新操做的原子性。
AbstractQueuedSynchronizer繼承了AbstractOwnableSynchronizer,這個類只有一個變量:exclusiveOwnerThread,表示當前佔用該鎖的線程,而且提供了相應的get,set方法。
ReentrantLock實現原理
什麼是原子操做?在Java Concurrency API中有哪些原子類(atomic classes)?
原子操做是指一個不受其餘操做影響的操做任務單元。原子操做是在多線程環境下避免數據不一致必須的手段。
int++並非一個原子操做,因此當一個線程讀取它的值並加1時,另一個線程有可能會讀到以前的值,這就會引起錯誤。
爲了解決這個問題,必須保證增長操做是原子的,在JDK1.5以前咱們能夠使用同步技術來作到這一點。
到JDK1.5,java.util.concurrent.atomic包提供了int和long類型的裝類,它們能夠自動的保證對於他們的操做是原子的而且不須要使用同步。
什麼是Executors框架?
Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。
Executor框架是一個根據一組執行策略調用,調度,執行和控制的異步任務的框架。
無限制的建立線程會引發應用程序內存溢出。因此建立一個線程池是個更好的的解決方案,由於能夠限制線程的數量而且能夠回收再利用這些線程。
利用Executors框架能夠很是方便的建立一個線程池,
Java經過Executors提供四種線程池,分別爲:
newCachedThreadPool建立一個可緩存線程池,若是線程池長度超過處理須要,可靈活回收空閒線程,若無可回收,則新建線程。
newFixedThreadPool 建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
newScheduledThreadPool 建立一個定長線程池,支持定時及週期性任務執行。
newSingleThreadExecutor 建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。
什麼是阻塞隊列?如何使用阻塞隊列來實現生產者-消費者模型?
JDK7提供了7個阻塞隊列。(也屬於併發容器)
-
ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。
-
LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。
-
PriorityBlockingQueue :一個支持優先級排序的無界阻塞隊列。
-
DelayQueue:一個使用優先級隊列實現的無界阻塞隊列。
-
SynchronousQueue:一個不存儲元素的阻塞隊列。
-
LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
-
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
什麼是阻塞隊列?
阻塞隊列是一個在隊列基礎上又支持了兩個附加操做的隊列。
2個附加操做:
支持阻塞的插入方法:隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。 支持阻塞的移除方法:隊列空時,獲取元素的線程會等待隊列變爲非空。
阻塞隊列的應用場景
阻塞隊列經常使用於生產者和消費者的場景,生產者是向隊列裏添加元素的線程,消費者是從隊列裏取元素的線程。簡而言之,阻塞隊列是生產者用來存放元素、消費者獲取元素的容器。
幾個方法
在阻塞隊列不可用的時候,上述2個附加操做提供了四種處理方法
方法\處理方式 | 拋出異常 | 返回特殊值 | 一直阻塞 | 超時退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
檢查方法 | element() | peek() | 不可用 | 不可用 |
JAVA裏的阻塞隊列
JDK 7 提供了7個阻塞隊列,以下
一、ArrayBlockingQueue 數組結構組成的有界阻塞隊列。
此隊列按照先進先出(FIFO)的原則對元素進行排序,可是默認狀況下不保證線程公平的訪問隊列,即若是隊列滿了,那麼被阻塞在外面的線程對隊列訪問的順序是不能保證線程公平(即先阻塞,先插入)的。
二、LinkedBlockingQueue一個由鏈表結構組成的有界阻塞隊列
此隊列按照先出先進的原則對元素進行排序
三、PriorityBlockingQueue支持優先級的無界阻塞隊列
四、DelayQueue支持延時獲取元素的無界阻塞隊列,便可以指定多久才能從隊列中獲取當前元素
五、SynchronousQueue不存儲元素的阻塞隊列,每個put必須等待一個take操做,不然不能繼續添加元素。而且他支持公平訪問隊列。
六、LinkedTransferQueue由鏈表結構組成的無界阻塞TransferQueue隊列。相對於其餘阻塞隊列,多了tryTransfer和transfer方法
transfer方法
若是當前有消費者正在等待接收元素(take或者待時間限制的poll方法),transfer能夠把生產者傳入的元素馬上傳給消費者。若是沒有消費者等待接收元素,則將元素放在隊列的tail節點,並等到該元素被消費者消費了才返回。
tryTransfer方法
用來試探生產者傳入的元素可否直接傳給消費者。,若是沒有消費者在等待,則返回false。和上述方法的區別是該方法不管消費者是否接收,方法當即返回。而transfer方法是必須等到消費者消費了才返回。
七、LinkedBlockingDeque鏈表結構的雙向阻塞隊列,優點在於多線程入隊時,減小一半的競爭。
如何使用阻塞隊列來實現生產者-消費者模型?
通知模式實現:所謂通知模式,就是當生產者往滿的隊列裏添加元素時會阻塞住生產者,當消費者消費了一個隊列中的元素後,會通知生產者當前隊列可用。
使用BlockingQueue解決生產者消費者問題
爲何BlockingQueue適合解決生產者消費者問題
任何有效的生產者-消費者問題解決方案都是經過控制生產者put()方法(生產資源)和消費者take()方法(消費資源)的調用來實現的,一旦你實現了對方法的阻塞控制,那麼你將解決該問題。
Java經過BlockingQueue提供了開箱即用的支持來控制這些方法的調用(一個線程建立資源,另外一個消費資源)。java.util.concurrent包下的BlockingQueue接口是一個線程安全的可用於存取對象的隊列。
BlockingQueue是一種數據結構,支持一個線程往裏存資源,另外一個線程從裏取資源。這正是解決生產者消費者問題所須要的,那麼讓咱們開始解決該問題吧。
生產者
如下代碼用於生產者線程
package io.ymq.example.thread; import java.util.concurrent.BlockingQueue; /** * 描述:生產者 * * @author yanpenglei * @create 2018-03-14 15:52 **/ class Producer implements Runnable { protected BlockingQueue<Object> queue; Producer(BlockingQueue<Object> theQueue) { this.queue = theQueue; } public void run() { try { while (true) { Object justProduced = getResource(); queue.put(justProduced); System.out.println("生產者資源隊列大小= " + queue.size()); } } catch (InterruptedException ex) { System.out.println("生產者 中斷"); } } Object getResource() { try { Thread.sleep(100); } catch (InterruptedException ex) { System.out.println("生產者 讀 中斷"); } return new Object(); } }
消費者
如下代碼用於消費者線程
package io.ymq.example.thread; import java.util.concurrent.BlockingQueue; /** * 描述: 消費者 * * @author yanpenglei * @create 2018-03-14 15:54 **/ class Consumer implements Runnable { protected BlockingQueue<Object> queue; Consumer(BlockingQueue<Object> theQueue) { this.queue = theQueue; } public void run() { try { while (true) { Object obj = queue.take(); System.out.println("消費者 資源 隊列大小 " + queue.size()); take(obj); } } catch (InterruptedException ex) { System.out.println("消費者 中斷"); } } void take(Object obj) { try { Thread.sleep(100); // simulate time passing } catch (InterruptedException ex) { System.out.println("消費者 讀 中斷"); } System.out.println("消費對象 " + obj); } }
測試該解決方案是否運行正常
package io.ymq.example.thread; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; /** * 描述: 測試 * * @author yanpenglei * @create 2018-03-14 15:58 **/ public class ProducerConsumerExample { public static void main(String[] args) throws InterruptedException { int numProducers = 4; int numConsumers = 3; BlockingQueue<Object> myQueue = new LinkedBlockingQueue<Object>(5); for (int i = 0; i < numProducers; i++) { new Thread(new Producer(myQueue)).start(); } for (int i = 0; i < numConsumers; i++) { new Thread(new Consumer(myQueue)).start(); } Thread.sleep(1000); System.exit(0); } }
運行結果
生產者資源隊列大小= 1 生產者資源隊列大小= 1 消費者 資源 隊列大小 1 生產者資源隊列大小= 1 消費者 資源 隊列大小 1 消費者 資源 隊列大小 1 生產者資源隊列大小= 1 生產者資源隊列大小= 3 消費對象 java.lang.Object@1e1aa52b 生產者資源隊列大小= 2 生產者資源隊列大小= 5 消費對象 java.lang.Object@6e740a76 消費對象 java.lang.Object@697853f6 ...... 消費對象 java.lang.Object@41a10cbc 消費對象 java.lang.Object@4963c8d1 消費者 資源 隊列大小 5 生產者資源隊列大小= 5 生產者資源隊列大小= 5 消費者 資源 隊列大小 4 消費對象 java.lang.Object@3e49c35d 消費者 資源 隊列大小 4 生產者資源隊列大小= 5
從輸出結果中,咱們能夠發現隊列大小永遠不會超過5,消費者線程消費了生產者生產的資源。
什麼是Callable和Future?
Callable 和 Future 是比較有趣的一對組合。當咱們須要獲取線程的執行結果時,就須要用到它們。Callable用於產生結果,Future用於獲取結果。
Callable接口使用泛型去定義它的返回類型。Executors類提供了一些有用的方法去在線程池中執行Callable內的任務。因爲Callable任務是並行的,必須等待它返回的結果。java.util.concurrent.Future對象解決了這個問題。
在線程池提交Callable任務後返回了一個Future對象,使用它能夠知道Callable任務的狀態和獲得Callable返回的執行結果。Future提供了get()方法,等待Callable結束並獲取它的執行結果。
代碼示例
Callable 是一個接口,它只包含一個call()方法。Callable是一個返回結果而且可能拋出異常的任務。
爲了便於理解,咱們能夠將Callable比做一個Runnable接口,而Callable的call()方法則相似於Runnable的run()方法。
public class CallableFutureTest { public static void main(String[] args) throws InterruptedException, ExecutionException { System.out.println("start main thread "); ExecutorService exec = Executors.newFixedThreadPool(2); //新建一個Callable 任務,並將其提交到一個ExecutorService. 將返回一個描述任務狀況的Future. Callable<String> call = new Callable<String>() { @Override public String call() throws Exception { System.out.println("start new thread "); Thread.sleep(5000); System.out.println("end new thread "); return "我是返回的內容"; } }; Future<String> task = exec.submit(call); Thread.sleep(1000); String retn = task.get(); //關閉線程池 exec.shutdown(); System.out.println(retn + "--end main thread"); } }
控制檯打印
start main thread start new thread end new thread 我是返回的內容--end main thread
什麼是FutureTask?
FutureTask可用於異步獲取執行結果或取消執行任務的場景。經過傳入Runnable或者Callable的任務給FutureTask,直接調用其run方法或者放入線程池執行,以後能夠在外部經過FutureTask的get方法異步獲取執行結果,所以,FutureTask很是適合用於耗時的計算,主線程能夠在完成本身的任務後,再去獲取結果。另外,FutureTask還能夠確保即便調用了屢次run方法,它都只會執行一次Runnable或者Callable任務,或者經過cancel取消FutureTask的執行等。
1.執行多任務計算
FutureTask執行多任務計算的使用場景
利用FutureTask和ExecutorService,能夠用多線程的方式提交計算任務,主線程繼續執行其餘任務,當主線程須要子線程的計算結果時,在異步獲取子線程的執行結果。
import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; public class FutureTaskForMultiCompute { public static void main(String[] args) { FutureTaskForMultiCompute inst = new FutureTaskForMultiCompute(); // 建立任務集合 List<FutureTask<Integer>> taskList = new ArrayList<FutureTask<Integer>>(); // 建立線程池 ExecutorService exec = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { // 傳入Callable對象建立FutureTask對象 FutureTask<Integer> ft = new FutureTask<Integer>(inst.new ComputeTask(i, "" + i)); taskList.add(ft); // 提交給線程池執行任務,也能夠經過exec.invokeAll(taskList)一次性提交全部任務; exec.submit(ft); } System.out.println("全部計算任務提交完畢, 主線程接着幹其餘事情!"); // 開始統計各計算線程計算結果 Integer totalResult = 0; for (FutureTask<Integer> ft : taskList) { try { //FutureTask的get方法會自動阻塞,直到獲取計算結果爲止 totalResult = totalResult + ft.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } // 關閉線程池 exec.shutdown(); System.out.println("多任務計算後的總結果是:" + totalResult); } private class ComputeTask implements Callable<Integer> { private Integer result = 0; private String taskName = ""; public ComputeTask(Integer iniResult, String taskName) { result = iniResult; this.taskName = taskName; System.out.println("生成子線程計算任務: " + taskName); } public String getTaskName() { return this.taskName; } @Override public Integer call() throws Exception { // TODO Auto-generated method stub for (int i = 0; i < 100; i++) { result = +i; } // 休眠5秒鐘,觀察主線程行爲,預期的結果是主線程會繼續執行,到要取得FutureTask的結果是等待直至完成。 Thread.sleep(5000); System.out.println("子線程計算任務: " + taskName + " 執行完成!"); return result; } } }
生成子線程計算任務: 0 生成子線程計算任務: 1 生成子線程計算任務: 2 生成子線程計算任務: 3 生成子線程計算任務: 4 生成子線程計算任務: 5 生成子線程計算任務: 6 生成子線程計算任務: 7 生成子線程計算任務: 8 生成子線程計算任務: 9 全部計算任務提交完畢, 主線程接着幹其餘事情! 子線程計算任務: 0 執行完成! 子線程計算任務: 2 執行完成! 子線程計算任務: 3 執行完成! 子線程計算任務: 4 執行完成! 子線程計算任務: 1 執行完成! 子線程計算任務: 8 執行完成! 子線程計算任務: 7 執行完成! 子線程計算任務: 6 執行完成! 子線程計算任務: 9 執行完成! 子線程計算任務: 5 執行完成! 多任務計算後的總結果是:990
2.高併發環境下
FutureTask在高併發環境下確保任務只執行一次
在不少高併發的環境下,每每咱們只須要某些任務只執行一次。這種使用情景FutureTask的特性恰能勝任。舉一個例子,假設有一個帶key的鏈接池,當key存在時,即直接返回key對應的對象;當key不存在時,則建立鏈接。對於這樣的應用場景,一般採用的方法爲使用一個Map對象來存儲key和鏈接池對應的對應關係,典型的代碼以下面所示:
private Map<String, Connection> connectionPool = new HashMap<String, Connection>(); private ReentrantLock lock = new ReentrantLock(); public Connection getConnection(String key) { try { lock.lock(); if (connectionPool.containsKey(key)) { return connectionPool.get(key); } else { //建立 Connection Connection conn = createConnection(); connectionPool.put(key, conn); return conn; } } finally { lock.unlock(); } } //建立Connection private Connection createConnection() { return null; }
在上面的例子中,咱們經過加鎖確保高併發環境下的線程安全,也確保了connection只建立一次,然而確犧牲了性能。改用ConcurrentHash的狀況下,幾乎能夠避免加鎖的操做,性能大大提升,可是在高併發的狀況下有可能出現Connection被建立屢次的現象。這時最須要解決的問題就是當key不存在時,建立Connection的動做能放在connectionPool以後執行,這正是FutureTask發揮做用的時機,基於ConcurrentHashMap和FutureTask的改造代碼以下:
private ConcurrentHashMap<String, FutureTask<Connection>> connectionPool = new ConcurrentHashMap<String, FutureTask<Connection>>(); public Connection getConnection(String key) throws Exception { FutureTask<Connection> connectionTask = connectionPool.get(key); if (connectionTask != null) { return connectionTask.get(); } else { Callable<Connection> callable = new Callable<Connection>() { @Override public Connection call() throws Exception { // TODO Auto-generated method stub return createConnection(); } }; FutureTask<Connection> newTask = new FutureTask<Connection>(callable); connectionTask = connectionPool.putIfAbsent(key, newTask); if (connectionTask == null) { connectionTask = newTask; connectionTask.run(); } return connectionTask.get(); } } //建立Connection private Connection createConnection() { return null; }
通過這樣的改造,能夠避免因爲併發帶來的屢次建立鏈接及鎖的出現。
什麼是同步容器和併發容器的實現?
1、同步容器
主要表明有Vector和Hashtable,以及Collections.synchronizedXxx等。 鎖的粒度爲當前對象總體。 迭代器是及時失敗的,即在迭代的過程當中發現被修改,就會拋出ConcurrentModificationException。
2、併發容器
主要表明有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。 鎖的粒度是分散的、細粒度的,即讀和寫是使用不一樣的鎖。 迭代器具備弱一致性,便可以容忍併發修改,不會拋出ConcurrentModificationException。
JDK 7 ConcurrentHashMap
採用分離鎖技術,同步容器中,是一個容器一個鎖,但在ConcurrentHashMap中,會將hash表的數組部分分紅若干段,每段維護一個鎖,以達到高效的併發訪問;
JDK 8 ConcurrentHashMap
採用分離鎖技術,同步容器中,是一個容器一個鎖,但在ConcurrentHashMap中,會將hash表的數組部分分紅若干段,每段維護一個鎖,以達到高效的併發訪問;
3、阻塞隊列
主要表明有LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue(Comparable,Comparator)、SynchronousQueue。 提供了可阻塞的put和take方法,以及支持定時的offer和poll方法。 適用於生產者、消費者模式(線程池和工做隊列-Executor),同時也是同步容器
4、雙端隊列
主要表明有ArrayDeque和LinkedBlockingDeque。 意義:正如阻塞隊列適用於生產者消費者模式,雙端隊列一樣適用與另外一種模式,即工做密取。在生產者-消費者設計中,全部消費者共享一個工做隊列,而在工做密取中,每一個消費者都有各自的雙端隊列。 若是一個消費者完成了本身雙端隊列中的所有工做,那麼他就能夠從其餘消費者的雙端隊列末尾祕密的獲取工做。具備更好的可伸縮性,這是由於工做者線程不會在單個共享的任務隊列上發生競爭。 在大多數時候,他們都只是訪問本身的雙端隊列,從而極大的減小了競爭。當工做者線程須要訪問另外一個隊列時,它會從隊列的尾部而不是頭部獲取工做,所以進一步下降了隊列上的競爭。 適用於:網頁爬蟲等任務中
5、比較及適用場景
若是不須要阻塞隊列,優先選擇ConcurrentLinkedQueue; 若是須要阻塞隊列,隊列大小固定優先選擇ArrayBlockingQueue,隊列大小不固定優先選擇LinkedBlockingQueue; 若是須要對隊列進行排序,選擇PriorityBlockingQueue; 若是須要一個快速交換的隊列,選擇SynchronousQueue; 若是須要對隊列中的元素進行延時操做,則選擇DelayQueue。
什麼是多線程?優缺點?
什麼是多線程?
多線程:是指從軟件或者硬件上實現多個線程的併發技術。
多線程的好處:
-
使用多線程能夠把程序中佔據時間長的任務放到後臺去處理,如圖片、視屏的下載
-
發揮多核處理器的優點,併發執行讓系統運行的更快、更流暢,用戶體驗更好
多線程的缺點:
-
大量的線程下降代碼的可讀性;
-
更多的線程須要更多的內存空間
-
當多個線程對同一個資源出現爭奪時候要注意線程安全的問題。
什麼是多線程的上下文切換?
即便是單核CPU也支持多線程執行代碼,CPU經過給每一個線程分配CPU時間片來實現這個機制。時間片是CPU分配給各個線程的時間,由於時間片很是短,因此CPU經過不停地切換線程執行,讓咱們感受多個線程時同時執行的,時間片通常是幾十毫秒(ms)
上下文切換過程當中,CPU會中止處理當前運行的程序,並保存當前程序運行的具體位置以便以後繼續運行
CPU經過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。可是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,能夠再次加載這個任務的狀態
-
從任務保存到再加載的過程就是一次上下文切換
ThreadLocal的設計理念與做用?
Java中的ThreadLocal類容許咱們建立只能被同一個線程讀寫的變量。所以,若是一段代碼含有一個ThreadLocal變量的引用,即便兩個線程同時執行這段代碼,它們也沒法訪問到對方的ThreadLocal變量
ThreadLocal
如何建立ThreadLocal變量
如下代碼展現瞭如何建立一個ThreadLocal變量:
private ThreadLocal myThreadLocal = new ThreadLocal();
經過這段代碼實例化了一個ThreadLocal對象。咱們只須要實例化對象一次,而且也不須要知道它是被哪一個線程實例化。雖然全部的線程都能訪問到這個ThreadLocal實例,可是每一個線程卻只能訪問到本身經過調用ThreadLocal的set()方法設置的值。即便是兩個不一樣的線程在同一個ThreadLocal對象上設置了不一樣的值,他們仍然沒法訪問到對方的值。
如何訪問ThreadLocal變量
一旦建立了一個ThreadLocal變量,你能夠經過以下代碼設置某個須要保存的值:
myThreadLocal.set("A thread local value」);
能夠經過下面方法讀取保存在ThreadLocal變量中的值:
String threadLocalValue = (String) myThreadLocal.get();
get()方法返回一個Object對象,set()對象須要傳入一個Object類型的參數。
爲ThreadLocal指定泛型類型
public static ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();
咱們能夠建立一個指定泛型類型的ThreadLocal對象,這樣咱們就不須要每次對使用get()方法返回的值做強制類型轉換了。下面展現了指定泛型類型的ThreadLocal例子:
ThreadLocal的設計理念與做用
http://blog.csdn.net/u011860731/article/details/48733073http://blog.csdn.net/u011860731/article/details/48733073)
InheritableThreadLocal
public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();
InheritableThreadLocal類是ThreadLocal類的子類。ThreadLocal中每一個線程擁有它本身的值,與ThreadLocal不一樣的是,InheritableThreadLocal容許一個線程以及該線程建立的全部子線程均可以訪問它保存的值。
InheritableThreadLocal 原理
Java 多線程:InheritableThreadLocal 實現原理
ThreadPool(線程池)用法與優點?
爲何要用線程池:
-
減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務。
-
能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止由於消耗過多的內存,而把服務器累趴下(每一個線程須要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)。
-
Java裏面線程池的頂級接口是Executor,可是嚴格意義上講Executor並非一個線程池,而只是一個執行線程的工具。真正的線程池接口是ExecutorService。
new Thread 缺點
-
每次new Thread新建對象性能差。
-
線程缺少統一管理,可能無限制新建線程,相互之間競爭,及可能佔用過多系統資源致使死機或oom。
-
缺少更多功能,如定時執行、按期執行、線程中斷。
ThreadPool 優勢
減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務
能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止由於由於消耗過多的內存,而把服務器累趴下(每一個線程須要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)
-
減小在建立和銷燬線程上所花的時間以及系統資源的開銷
-
如不使用線程池,有可能形成系統建立大量線程而致使消耗完系統內存
Java提供的四種線程池的好處在於:
-
重用存在的線程,減小對象建立、銷燬的開銷,提升性能。
-
可有效控制最大併發線程數,提升系統資源的使用率,同時避免過多資源競爭,避免堵塞。
-
提供定時執行、按期執行、單線程、併發數控制等功能。
比較重要的幾個類:
類 | 描述 |
---|---|
ExecutorService | 真正的線程池接口。 |
ScheduledExecutorService | 能和Timer/TimerTask相似,解決那些須要任務重複執行的問題。 |
ThreadPoolExecutor | ExecutorService的默認實現。 |
ScheduledThreadPoolExecutor | 繼承ThreadPoolExecutor的ScheduledExecutorService接口實現,週期性任務調度的類實現。 |
要配置一個線程池是比較複雜的,尤爲是對於線程池的原理不是很清楚的狀況下,頗有可能配置的線程池不是較優的,所以在Executors類裏面提供了一些靜態工廠,生成一些經常使用的線程池。
Executors提供四種線程池
newCachedThreadPool建立一個可緩存線程池,若是線程池長度超過處理須要,可靈活回收空閒線程,若無可回收,則新建線程。
newFixedThreadPool 建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
newScheduledThreadPool 建立一個定長線程池,支持定時及週期性任務執行。
newSingleThreadExecutor 建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。
通常都不用Executors提供的線程建立方式
使用ThreadPoolExecutor建立線程池
ThreadPoolExecutor的構造函數
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
參數:
-
corePoolSize核心線程數大小,當線程數<corePoolSize ,會建立線程執行runnable
-
maximumPoolSize 最大線程數, 當線程數 >= corePoolSize的時候,會把runnable放入workQueue中
-
keepAliveTime 保持存活時間,當線程數大於corePoolSize的空閒線程能保持的最大時間。
-
unit 時間單位
-
workQueue 保存任務的阻塞隊列
-
threadFactory 建立線程的工廠
-
handler 拒絕策略
任務執行順序:
-
當線程數小於corePoolSize時,建立線程執行任務。
-
當線程數大於等於corePoolSize而且workQueue沒有滿時,放入workQueue中
-
線程數大於等於corePoolSize而且當workQueue滿時,新任務新建線程運行,線程總數要小於maximumPoolSize
-
當線程總數等於maximumPoolSize而且workQueue滿了的時候執行handler的rejectedExecution。也就是拒絕策略。
ThreadPoolExecutor默認有四個拒絕策略:
-
ThreadPoolExecutor.AbortPolicy()
直接拋出異常RejectedExecutionException -
ThreadPoolExecutor.CallerRunsPolicy()
直接調用run方法而且阻塞執行 -
ThreadPoolExecutor.DiscardPolicy()
直接丟棄後來的任務 -
ThreadPoolExecutor.DiscardOldestPolicy()
丟棄在隊列中隊首的任務
固然能夠本身繼承 RejectedExecutionHandler 來寫拒絕策略.
java 四種線程池的使用
Concurrent包裏的其餘東西:ArrayBlockingQueue、CountDownLatch等等。
阻塞隊列
一、ArrayBlockingQueue 數組結構組成的有界阻塞隊列。
此隊列按照先進先出(FIFO)的原則對元素進行排序,可是默認狀況下不保證線程公平的訪問隊列,即若是隊列滿了,那麼被阻塞在外面的線程對隊列訪問的順序是不能保證線程公平(即先阻塞,先插入)的。
CountDownLatch
CountDownLatch 容許一個或多個線程等待其餘線程完成操做。
應用場景
假若有這樣一個需求,當咱們須要解析一個Excel裏多個sheet的數據時,能夠考慮使用多線程,每一個線程解析一個sheet裏的數據,等到全部的sheet都解析完以後,程序須要提示解析完成。
在這個需求中,要實現主線程等待全部線程完成sheet的解析操做,最簡單的作法是使用join。代碼以下:
public class JoinCountDownLatchTest { public static void main(String[] args) throws InterruptedException { Thread parser1 = new Thread(new Runnable() { @Override public void run() { } }); Thread parser2 = new Thread(new Runnable() { @Override public void run() { System.out.println("parser2 finish"); } }); parser1.start(); parser2.start(); parser1.join(); parser2.join(); System.out.println("all parser finish"); } }
join用於讓當前執行線程等待join線程執行結束。其實現原理是不停檢查join線程是否存活,若是join線程存活則讓當前線程永遠wait,代碼片斷以下,wait(0)表示永遠等待下去。
while (isAlive()) { wait(0); }
-
方法isAlive()功能是判斷當前線程是否處於活動狀態。
-
活動狀態就是線程啓動且還沒有終止,好比正在運行或準備開始運行。
CountDownLatch用法
public class Test { public static void main(String[] args) { final CountDownLatch latch = new CountDownLatch(2); new Thread(){ public void run() { try { System.out.println("子線程"+Thread.currentThread().getName()+"正在執行"); Thread.sleep(3000); System.out.println("子線程"+Thread.currentThread().getName()+"執行完畢"); latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); new Thread(){ public void run() { try { System.out.println("子線程"+Thread.currentThread().getName()+"正在執行"); Thread.sleep(3000); System.out.println("子線程"+Thread.currentThread().getName()+"執行完畢"); latch.countDown(); } catch (InterruptedException e) { e.printStackTrace(); } }; }.start(); try { System.out.println("等待2個子線程執行完畢..."); latch.await(); System.out.println("2個子線程已經執行完畢"); System.out.println("繼續執行主線程"); } catch (InterruptedException e) { e.printStackTrace(); } } }
線程Thread-0正在執行 線程Thread-1正在執行 等待2個子線程執行完畢... 線程Thread-0執行完畢 線程Thread-1執行完畢 2個子線程已經執行完畢 繼續執行主線程
new CountDownLatch(2)的構造函數接收一個int類型的參數做爲計數器,若是你想等待N個點完成,這裏就傳入N。
當咱們調用一次CountDownLatch的countDown()方法時,N就會減1,CountDownLatch的await()會阻塞當前線程,直到N變成零。因爲countDown方法能夠用在任何地方,因此這裏說的N個點,能夠是N個線程,也能夠是1個線程裏的N個執行步驟。用在多個線程時,你只須要把這個CountDownLatch的引用傳遞到線程裏。
Java併發編程:CountDownLatch、CyclicBarrier和 Semaphore
synchronized和ReentrantLock的區別?
java在編寫多線程程序時,爲了保證線程安全,須要對數據同步,常常用到兩種同步方式就是Synchronized和重入鎖ReentrantLock。
基礎知識
-
可重入鎖。可重入鎖是指同一個線程能夠屢次獲取同一把鎖。ReentrantLock和synchronized都是可重入鎖。
-
可中斷鎖。可中斷鎖是指線程嘗試獲取鎖的過程當中,是否能夠響應中斷。synchronized是不可中斷鎖,而ReentrantLock則提供了中斷功能。
-
公平鎖與非公平鎖。公平鎖是指多個線程同時嘗試獲取同一把鎖時,獲取鎖的順序按照線程達到的順序,而非公平鎖則容許線程「插隊」。synchronized是非公平鎖,而ReentrantLock的默認實現是非公平鎖,可是也能夠設置爲公平鎖。
-
CAS操做(CompareAndSwap)。CAS操做簡單的說就是比較並交換。CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值。不然,處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該位置的值。CAS 有效地說明了「我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。」
Synchronized
synchronized是java內置的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由JVM實現,用戶不須要顯示的釋放鎖,很是方便。然而synchronized也有必定的侷限性
例如:
-
當線程嘗試獲取鎖的時候,若是獲取不到鎖會一直阻塞。
-
若是獲取鎖的線程進入休眠或者阻塞,除非當前線程異常,不然其餘線程嘗試獲取鎖必須一直等待。
ReentrantLock
ReentrantLock它是JDK 1.5以後提供的API層面的互斥鎖,須要lock()和unlock()方法配合try/finally語句塊來完成。
代碼示例
private Lock lock = new ReentrantLock(); public void test(){ lock.lock(); try{ doSomeThing(); }catch (Exception e){ // ignored }finally { lock.unlock(); } }
-
**lock()**, 若是獲取了鎖當即返回,若是別的線程持有鎖,當前線程則一直處於休眠狀態,直到獲取鎖
-
tryLock(), 若是獲取了鎖當即返回true,若是別的線程正持有鎖,當即返回false;
-
tryLock(long timeout,TimeUnit unit)****,若是獲取了鎖定當即返回true,若是別的線程正持有鎖,會等待參數給定的時間,在等待的過程當中,若是獲取了鎖定,就返回true,若是等待超時,返回false;
-
lockInterruptibly:若是獲取了鎖定當即返回,若是沒有獲取鎖定,當前線程處於休眠狀態,直到或者鎖定,或者當前線程被別的線程中斷
ReentrantLock 一些特性
-
等待可中斷避免,出現死鎖的狀況(若是別的線程正持有鎖,會等待參數給定的時間,在等待的過程當中,若是獲取了鎖定,就返回true,若是等待超時,返回false)
-
公平鎖與非公平鎖多個線程等待同一個鎖時,必須按照申請鎖的時間順序得到鎖,Synchronized鎖非公平鎖,ReentrantLock默認的構造函數是建立的非公平鎖,能夠經過參數true設爲公平鎖,但公平鎖表現的性能不是很好。
公平鎖:線程獲取鎖的順序和調用lock的順序同樣,FIFO;
非公平鎖:線程獲取鎖的順序和調用lock的順序無關,全憑運氣。
Java併發包(java.util.concurrent)中大量使用了CAS操做,涉及到併發的地方都調用了sun.misc.Unsafe類方法進行CAS操做。
ReenTrantLock實現的原理:
簡單來講,ReenTrantLock的實現是一種自旋鎖,經過循環調用CAS操做來實現加鎖。它的性能比較好也是由於避免了使線程進入內核態的阻塞狀態。想盡辦法避免線程進入內核的阻塞狀態是咱們去分析和理解鎖設計的關鍵鑰匙。
總結一下
在Synchronized優化之前,synchronized的性能是比ReenTrantLock差不少的,可是自從Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,二者的性能就差很少了,在兩種方法均可用的狀況下,官方甚至建議使用synchronized,其實synchronized的優化我感受就借鑑了ReenTrantLock中的CAS技術。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。
synchronized:
在資源競爭不是很激烈的狀況下,偶爾會有同步的情形下,synchronized是很合適的。緣由在於,編譯程序一般會盡量的進行優化synchronize,另外可讀性很是好。
ReentrantLock:
ReentrantLock用起來會複雜一些。在基本的加鎖和解鎖上,二者是同樣的,因此無特殊狀況下,推薦使用synchronized。ReentrantLock的優點在於它更靈活、更強大,增長了輪訓、超時、中斷等高級功能。
ReentrantLock默認使用非公平鎖是基於性能考慮,公平鎖爲了保證線程規規矩矩地排隊,須要增長阻塞和喚醒的時間開銷。若是直接插隊獲取非公平鎖,跳過了對隊列的處理,速度會更快。
ReentrantLock實現原理
分析ReentrantLock的實現原理(ReentrantLock和同步工具類的實現基礎都是AQS)
Semaphore有什麼做用?
-
Semaphore就是一個信號量,它的做用是限制某段代碼塊的併發數。
-
Semaphore有一個構造函數,能夠傳入一個int型整數n,表示某段代碼最多隻有n個線程能夠訪問,
-
若是超出了n,那麼請等待,等到某個線程執行完畢這段代碼塊,下一個線程再進入。
-
由此能夠看出若是Semaphore構造函數中傳入的int型整數n=1,至關於變成了一個synchronized了。
Semaphore類位於java.util.concurrent包下,它提供了2個構造器:
//參數permits表示許可數目,即同時能夠容許多少線程進行訪問 public Semaphore(int permits) { sync = new NonfairSync(permits); } //這個多了一個參數fair表示是不是公平的,即等待時間越久的越先獲取許可 public Semaphore(int permits, boolean fair) { sync = (fair)? new FairSync(permits) : new NonfairSync(permits); }
-
Semaphore類中比較重要的幾個方法,首先是acquire()、release()方法:
-
acquire()用來獲取一個許可,若無許可可以得到,則會一直等待,直到得到許可。
-
release()用來釋放許可。注意,在釋放許可以前,必須先獲得到許可。
Semaphore類中比較重要的幾個方法,首先是acquire()、release()方法: acquire()用來獲取一個許可,若無許可可以得到,則會一直等待,直到得到許可。 release()用來釋放許可。注意,在釋放許可以前,必須先獲得到許可。
這4個方法都會被阻塞,若是想當即獲得執行結果,能夠使用下面幾個方法:
//嘗試獲取一個許可,若獲取成功,則當即返回true,若獲取失敗,則當即返回false public boolean tryAcquire() { }; //嘗試獲取一個許可,若在指定的時間內獲取成功,則當即返回true,不然則當即返回false public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { }; //嘗試獲取permits個許可,若獲取成功,則當即返回true,若獲取失敗,則當即返回false public boolean tryAcquire(int permits) { }; //嘗試獲取permits個許可,若在指定的時間內獲取成功,則當即返回true public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //獲得當前可用的許可數目 public int availablePermits();
示例
倘若一個工廠有5臺機器,可是有8個工人,一臺機器同時只能被一個工人使用,只有使用完了,其餘工人才能繼續使用。那麼咱們就能夠經過Semaphore來實現:
public class Test { public static void main(String[] args) { int N = 8; //工人數 Semaphore semaphore = new Semaphore(5); //機器數目 for(int i=0;i<N;i++) new Worker(i,semaphore).start(); } static class Worker extends Thread{ private int num; private Semaphore semaphore; public Worker(int num,Semaphore semaphore){ this.num = num; this.semaphore = semaphore; } @Override public void run() { try { semaphore.acquire(); System.out.println("工人"+this.num+"佔用一個機器在生產..."); Thread.sleep(2000); System.out.println("工人"+this.num+"釋放出機器"); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } } } }
運行結果:
工人0佔用一個機器在生產... 工人1佔用一個機器在生產... 工人2佔用一個機器在生產... 工人4佔用一個機器在生產... 工人5佔用一個機器在生產... 工人0釋放出機器 工人2釋放出機器 工人3佔用一個機器在生產... 工人7佔用一個機器在生產... 工人4釋放出機器 工人5釋放出機器 工人1釋放出機器 工人6佔用一個機器在生產... 工人3釋放出機器 工人7釋放出機器 工人6釋放出機器
Java Concurrency API中的Lock接口(Lock interface)是什麼?對比同步它有什麼優點?
Lock接口比同步方法和同步塊提供了更具擴展性的鎖操做。他們容許更靈活的結構,能夠具備徹底不一樣的性質,而且能夠支持多個相關類的條件對象。
它的優點有:
-
能夠使鎖更公平
-
能夠使線程在等待鎖的時候響應中斷
-
可讓線程嘗試獲取鎖,並在沒法獲取鎖的時候當即返回或者等待一段時間
-
能夠在不一樣的範圍,以不一樣的順序獲取和釋放鎖
Hashtable的size()方法中明明只有一條語句」return count」,爲何還要作同步?
同一時間只能有一條線程執行固定類的同步方法,可是對於類的非同步方法,能夠多條線程同時訪問。因此,這樣就有問題了,可能線程A在執行Hashtable的put方法添加數據,線程B則能夠正常調用size()方法讀取Hashtable中當前元素的個數,那讀取到的值可能不是最新的,可能線程A添加了完了數據,可是沒有對size++,線程B就已經讀取size了,那麼對於線程B來講讀取到的size必定是不許確的。
而給size()方法加了同步以後,意味着線程B調用size()方法只有在線程A調用put方法完畢以後才能夠調用,這樣就保證了線程安全性
ConcurrentHashMap的併發度是什麼?
ConcurrentHashMap的併發度就是segment的大小,默認爲16,這意味着最多同時能夠有16條線程操做ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最大優點
ReentrantReadWriteLock讀寫鎖的使用
Lock比傳統線程模型中的synchronized方式更加面向對象,與生活中的鎖相似,鎖自己也應該是一個對象。兩個線程執行的代碼片斷要實現同步互斥的效果,它們必須用同一個Lock對象。
讀寫鎖:分爲讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由jvm本身控制的,你只要上好相應的鎖便可。 若是你的代碼只讀數據,能夠不少人同時讀,但不能同時寫,那就上讀鎖;
若是你的代碼修改數據,只能有一我的在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!
ReentrantReadWriteLock會使用兩把鎖來解決問題,一個讀鎖,一個寫鎖
線程進入讀鎖的前提條件:
-
沒有其餘線程的寫鎖
-
沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個
線程進入寫鎖的前提條件:
-
沒有其餘線程的讀鎖
-
沒有其餘線程的寫鎖
-
讀鎖的重入是容許多個申請讀操做的線程的,而寫鎖同時只容許單個線程佔有,該線程的寫操做能夠重入。
-
若是一個線程佔有了寫鎖,在不釋放寫鎖的狀況下,它還能佔有讀鎖,即寫鎖降級爲讀鎖。
-
對於同時佔有讀鎖和寫鎖的線程,若是徹底釋放了寫鎖,那麼它就徹底轉換成了讀鎖,之後的寫操做沒法重入,在寫鎖未徹底釋放時寫操做是能夠重入的。
-
公平模式下不管讀鎖仍是寫鎖的申請都必須按照AQS鎖等待隊列先進先出的順序。非公平模式下讀操做插隊的條件是鎖等待隊列head節點後的下一個節點是SHARED型節點,寫鎖則無條件插隊。
-
讀鎖不容許newConditon獲取Condition接口,而寫鎖的newCondition接口實現方法同ReentrantLock。
推薦閱讀
Spring Cloud 系列教程
Spring Boot 系列教程
源碼 + 教程
Github:github.com/souyunku/sp…
