最近看到網上流傳着,各類面試經驗及面試題,每每都是一大堆技術題目貼上去,而沒有答案。html
無論你是新程序員仍是老手,你必定在面試中遇到過有關線程的問題。Java語言一個重要的特色就是內置了對併發的支持,讓Java大受企業和程序員的歡迎。大多數待遇豐厚的Java開發職位都要求開發者精通多線程技術而且有豐富的Java程序開發、調試、優化經驗,因此線程相關的問題在面試中常常會被提到。
在典型的Java面試中, 面試官會從線程的基本概念問起java
如:爲何你須要使用線程, 如何建立線程,用什麼方式建立線程比較好(好比:繼承thread類仍是調用Runnable接口),而後逐漸問到併發問題像在Java併發編程的過程當中遇到了什麼挑戰,Java內存模型,JDK1.5引入了哪些更高階的併發工具,併發編程經常使用的設計模式,經典多線程問題如生產者消費者,哲學家就餐,讀寫器或者簡單的有界緩衝區問題。僅僅知道線程的基本概念是遠遠不夠的, 你必須知道如何處理死鎖,競態條件,內存衝突和線程安全等併發問題。掌握了這些技巧,你就能夠輕鬆應對多線程和併發面試了。
許多Java程序員在面試前纔會去看面試題,這很正常。程序員
由於收集面試題和練習很花時間,因此我從許多面試者那裏收集了Java多線程和併發相關的50個熱門問題。面試
下面是Java線程相關的熱門面試題,你能夠用它來好好準備面試。算法
線程是操做系統可以進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運做單位,能夠使用多線程對進行運算提速。編程
好比,若是一個線程完成一個任務要100毫秒,那麼用十個線程完成改任務只需10毫秒網頁爬蟲
通俗的說:加鎖的就是是線程安全的,不加鎖的就是是線程不安全的segmentfault
線程安全: 就是多線程訪問時,採用了加鎖機制,當一個線程訪問該類的某個數據時,進行保護,其餘線程不能進行訪問,直到該線程讀取完,其餘線程纔可以使用。不會出現數據不一致或者數據污染。設計模式
一個線程安全的計數器類的同一個實例對象在被多個線程使用的狀況下也不會出現計算失誤。很顯然你能夠將集合類分紅兩組,線程安全和非線程安全的。
Vector 是用同步方法來實現線程安全的, 而和它類似的ArrayList不是線程安全的。數組
線程不安全:就是不提供數據訪問保護,有可能出現多個線程前後更改數據形成所獲得的數據是髒數據
若是你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。若是每次運行結果和單線程運行的結果是同樣的,並且其餘的變量的值也和預期的是同樣的,就是線程安全的。
線程安全問題都是由全局變量及靜態變量引發的。
若每一個線程中對全局變量、靜態變量只有讀操做,而無寫操做,通常來講,這個全局變量是線程安全的;如有多個線程同時執行寫操做,通常都須要考慮線程同步,不然的話就可能影響線程安全。
自旋鎖是SMP架構中的一種low-level的同步機制。
當線程A想要獲取一把自選鎖而該鎖又被其它線程鎖持有時,線程A會在一個循環中自選以檢測鎖是否是已經可用了。
自選鎖須要注意:
參考
https://segmentfault.com/q/1010000000530936
一個簡單的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包含了幾個語言級別的關鍵字,包括:volatile, final以及synchronized,目的是爲了幫助程序員向編譯器描述一個程序的併發需求。Java內存模型定義了volatile和synchronized的行爲,更重要的是保證了同步的java程序在全部的處理器架構下面都能正確的運行。
「一個線程的寫操做對其餘線程可見」這個問題是由於編譯器對代碼進行重排序致使的。例如,只要代碼移動不會改變程序的語義,當編譯器認爲程序中移動一個寫操做到後面會更有效的時候,編譯器就會對代碼進行移動。若是編譯器推遲執行一個操做,其餘線程可能在這個操做執行完以前都不會看到該操做的結果,這反映了緩存的影響。
此外,寫入內存的操做可以被移動到程序裏更前的時候。在這種狀況下,其餘的線程在程序中可能看到一個比它實際發生更早的寫操做。全部的這些靈活性的設計是爲了經過給編譯器,運行時或硬件靈活性使其能在最佳順序的狀況下來執行操做。在內存模型的限定以內,咱們可以獲取到更高的性能。
看下面代碼展現的一個簡單例子:
ClassReordering { 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中最大的一塊由年輕代和老年代組成,而年輕代內存又被分紅三部分,Eden空間、From Survivor空間、To Survivor空間,默認狀況下年輕代按照8:1:1的比例來分配;方法區存儲類信息、常量、靜態變量等數據,是線程共享的區域,爲與Java堆區分,方法區還有一個別名Non-Heap(非堆);棧又分爲java虛擬機棧和本地方法棧主要用於方法的執行。
JAVA的JVM的內存可分爲3個區:堆(heap)、棧(stack)和方法區(method)
據Java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
可經過參數 棧幀是方法運行期的基礎數據結構棧容量可由-Xss設置
1.Java虛擬機棧是線程私有的,它的生命週期與線程相同。
java虛擬機棧,規定了兩種異常情況:
可經過參數 棧容量可由-Xss設置
可經過參數-XX:MaxPermSize設置
JDK1.6以前字符串常量池位於方法區之中。
JDK1.7字符串常量池已經被挪到堆之中。
可經過參數-XX:PermSize和-XX:MaxPermSize設置
可經過-XX:MaxDirectMemorySize指定,若是不指定,則默認與Java堆的最大值(-Xmx指定)同樣。
java堆(Java Heap)
可經過參數 -Xms 和-Xmx設置
java虛擬機棧(stack)
可經過參數 棧幀是方法運行期的基礎數據結構棧容量可由-Xss設置
方法區(Method Area)
可經過參數-XX:MaxPermSize設置
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有3個操做數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改成B,不然什麼都不作。
確保對內存的讀-改-寫操做都是原子操做執行
CAS雖然很高效的解決原子操做,可是CAS仍然存在三大問題。ABA問題,循環時間長開銷大和只能保證一個共享變量的原子操做
參考
https://blog.52itstyle.com/archives/948/
Java在JDK1.5以前都是靠synchronized關鍵字保證同步的,這種經過使用一致的鎖定協議來協調對共享狀態的訪問,能夠確保不管哪一個線程持有共享變量的鎖,都採用獨佔的方式來訪問這些變量。獨佔鎖其實就是一種悲觀鎖,因此能夠說synchronized是悲觀鎖。
樂觀鎖( Optimistic Locking)實際上是一種思想。相對悲觀鎖而言,樂觀鎖假設認爲數據通常狀況下不會形成衝突,因此在數據進行提交更新的時候,纔會正式對數據的衝突與否進行檢測,若是發現衝突了,則讓返回用戶錯誤的信息,讓用戶決定如何去作。
AbstractQueuedSynchronizer簡稱AQS,是一個用於構建鎖和同步容器的框架。事實上concurrent包內許多類都是基於AQS構建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解決了在實現同步容器時設計的大量細節問題。
AQS使用一個FIFO的隊列表示排隊等待鎖的線程,隊列頭節點稱做「哨兵節點」或者「啞節點」,它不與任何線程關聯。其餘的節點與等待線程關聯,每一個節點維護一個等待狀態waitStatus。
參考
https://blog.52itstyle.com/archives/948/
因爲java的CAS同時具備 volatile 讀和volatile寫的內存語義,所以Java線程之間的通訊如今有了下面四種方式:
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,就不是AQS關心的了,要由子類具體實現。
AQS中還有一個表示狀態的字段state,例如ReentrantLocky用它表示線程重入鎖的次數,Semaphore用它表示剩餘的許可數量,FutureTask用它表示任務的狀態。對state變量值的更新都採用CAS操做保證更新操做的原子性。
AbstractQueuedSynchronizer繼承了AbstractOwnableSynchronizer,這個類只有一個變量:exclusiveOwnerThread,表示當前佔用該鎖的線程,而且提供了相應的get,set方法。
ReentrantLock實現原理
http://www.javashuo.com/article/p-gcsvtqbz-go.html
原子操做是指一個不受其餘操做影響的操做任務單元。原子操做是在多線程環境下避免數據不一致必須的手段。
int++並非一個原子操做,因此當一個線程讀取它的值並加1時,另一個線程有可能會讀到以前的值,這就會引起錯誤。
爲了解決這個問題,必須保證增長操做是原子的,在JDK1.5以前咱們能夠使用同步技術來作到這一點。
到JDK1.5,java.util.concurrent.atomic包提供了int和long類型的裝類,它們能夠自動的保證對於他們的操做是原子的而且不須要使用同步。
Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。
Executor框架是一個根據一組執行策略調用,調度,執行和控制的異步任務的框架。
無限制的建立線程會引發應用程序內存溢出。因此建立一個線程池是個更好的的解決方案,由於能夠限制線程的數量而且能夠回收再利用這些線程。
利用Executors框架能夠很是方便的建立一個線程池,
Java經過Executors提供四種線程池,分別爲:
newCachedThreadPool建立一個可緩存線程池,若是線程池長度超過處理須要,可靈活回收空閒線程,若無可回收,則新建線程。
newFixedThreadPool 建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
newScheduledThreadPool 建立一個定長線程池,支持定時及週期性任務執行。
newSingleThreadExecutor 建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。
JDK7提供了7個阻塞隊列。(也屬於併發容器)
阻塞隊列是一個在隊列基礎上又支持了兩個附加操做的隊列。
2個附加操做:
支持阻塞的插入方法:隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。
支持阻塞的移除方法:隊列空時,獲取元素的線程會等待隊列變爲非空。
阻塞隊列經常使用於生產者和消費者的場景,生產者是向隊列裏添加元素的線程,消費者是從隊列裏取元素的線程。簡而言之,阻塞隊列是生產者用來存放元素、消費者獲取元素的容器。
在阻塞隊列不可用的時候,上述2個附加操做提供了四種處理方法
方法處理方式 | 拋出異常 | 返回特殊值 | 一直阻塞 | 超時退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
檢查方法 | element() | peek() | 不可用 | 不可用 |
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適合解決生產者消費者問題
任何有效的生產者-消費者問題解決方案都是經過控制生產者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接口使用泛型去定義它的返回類型。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可用於異步獲取執行結果或取消執行任務的場景。經過傳入Runnable或者Callable的任務給FutureTask,直接調用其run方法或者放入線程池執行,以後能夠在外部經過FutureTask的get方法異步獲取執行結果,所以,FutureTask很是適合用於耗時的計算,主線程能夠在完成本身的任務後,再去獲取結果。另外,FutureTask還能夠確保即便調用了屢次run方法,它都只會執行一次Runnable或者Callable任務,或者經過cancel取消FutureTask的執行等。
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
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; }
通過這樣的改造,能夠避免因爲併發帶來的屢次建立鏈接及鎖的出現。
主要表明有Vector和Hashtable,以及Collections.synchronizedXxx等。
鎖的粒度爲當前對象總體。
迭代器是及時失敗的,即在迭代的過程當中發現被修改,就會拋出ConcurrentModificationException。
主要表明有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。
鎖的粒度是分散的、細粒度的,即讀和寫是使用不一樣的鎖。
迭代器具備弱一致性,便可以容忍併發修改,不會拋出ConcurrentModificationException。
JDK 7 ConcurrentHashMap
採用分離鎖技術,同步容器中,是一個容器一個鎖,但在ConcurrentHashMap中,會將hash表的數組部分分紅若干段,每段維護一個鎖,以達到高效的併發訪問;
JDK 8 ConcurrentHashMap
採用分離鎖技術,同步容器中,是一個容器一個鎖,但在ConcurrentHashMap中,會將hash表的數組部分分紅若干段,每段維護一個鎖,以達到高效的併發訪問;
主要表明有LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue(Comparable,Comparator)、SynchronousQueue。
提供了可阻塞的put和take方法,以及支持定時的offer和poll方法。
適用於生產者、消費者模式(線程池和工做隊列-Executor),同時也是同步容器
主要表明有ArrayDeque和LinkedBlockingDeque。
意義:正如阻塞隊列適用於生產者消費者模式,雙端隊列一樣適用與另外一種模式,即工做密取。在生產者-消費者設計中,全部消費者共享一個工做隊列,而在工做密取中,每一個消費者都有各自的雙端隊列。
若是一個消費者完成了本身雙端隊列中的所有工做,那麼他就能夠從其餘消費者的雙端隊列末尾祕密的獲取工做。具備更好的可伸縮性,這是由於工做者線程不會在單個共享的任務隊列上發生競爭。
在大多數時候,他們都只是訪問本身的雙端隊列,從而極大的減小了競爭。當工做者線程須要訪問另外一個隊列時,它會從隊列的尾部而不是頭部獲取工做,所以進一步下降了隊列上的競爭。
適用於:網頁爬蟲等任務中
若是不須要阻塞隊列,優先選擇ConcurrentLinkedQueue;
若是須要阻塞隊列,隊列大小固定優先選擇ArrayBlockingQueue,隊列大小不固定優先選擇LinkedBlockingQueue;
若是須要對隊列進行排序,選擇PriorityBlockingQueue;
若是須要一個快速交換的隊列,選擇SynchronousQueue;
若是須要對隊列中的元素進行延時操做,則選擇DelayQueue。
什麼是多線程?
多線程:是指從軟件或者硬件上實現多個線程的併發技術。
多線程的好處:
多線程的缺點:
即便是單核CPU也支持多線程執行代碼,CPU經過給每一個線程分配CPU時間片來實現這個機制。時間片是CPU分配給各個線程的時間,由於時間片很是短,因此CPU經過不停地切換線程執行,讓咱們感受多個線程時同時執行的,時間片通常是幾十毫秒(ms)
上下文切換過程當中,CPU會中止處理當前運行的程序,並保存當前程序運行的具體位置以便以後繼續運行
CPU經過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。可是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,能夠再次加載這個任務的狀態
Java中的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/u0118607...://blog.csdn.net/u011860731/article/details/48733073)
public static ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>();
InheritableThreadLocal類是ThreadLocal類的子類。ThreadLocal中每一個線程擁有它本身的值,與ThreadLocal不一樣的是,InheritableThreadLocal容許一個線程以及該線程建立的全部子線程均可以訪問它保存的值。
InheritableThreadLocal 原理
Java 多線程:InheritableThreadLocal 實現原理
http://blog.csdn.net/ni357103403/article/details/51970748
減小了建立和銷燬線程的次數,每一個工做線程均可以被重複利用,可執行多個任務
能夠根據系統的承受能力,調整線程池中工做線線程的數目,防止由於由於消耗過多的內存,而把服務器累趴下(每一個線程須要大約1MB內存,線程開的越多,消耗的內存也就越大,最後死機)
Java提供的四種線程池的好處在於:
類 | 描述 |
---|---|
ExecutorService | 真正的線程池接口。 |
ScheduledExecutorService | 能和Timer/TimerTask相似,解決那些須要任務重複執行的問題。 |
ThreadPoolExecutor | ExecutorService的默認實現。 |
ScheduledThreadPoolExecutor | 繼承ThreadPoolExecutor的ScheduledExecutorService接口實現,週期性任務調度的類實現。 |
要配置一個線程池是比較複雜的,尤爲是對於線程池的原理不是很清楚的狀況下,頗有可能配置的線程池不是較優的,所以在Executors類裏面提供了一些靜態工廠,生成一些經常使用的線程池。
newCachedThreadPool建立一個可緩存線程池,若是線程池長度超過處理須要,可靈活回收空閒線程,若無可回收,則新建線程。
newFixedThreadPool 建立一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
newScheduledThreadPool 建立一個定長線程池,支持定時及週期性任務執行。
newSingleThreadExecutor 建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。
使用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; }
ThreadPoolExecutor.AbortPolicy()
直接拋出異常RejectedExecutionExceptionThreadPoolExecutor.CallerRunsPolicy()
直接調用run方法而且阻塞執行ThreadPoolExecutor.DiscardPolicy()
直接丟棄後來的任務ThreadPoolExecutor.DiscardOldestPolicy()
丟棄在隊列中隊首的任務固然能夠本身繼承 RejectedExecutionHandler 來寫拒絕策略.
https://juejin.im/post/59df0c1af265da432f301c8d
阻塞隊列
一、ArrayBlockingQueue 數組結構組成的有界阻塞隊列。
此隊列按照先進先出(FIFO)的原則對元素進行排序,可是默認狀況下不保證線程公平的訪問隊列,即若是隊列滿了,那麼被阻塞在外面的線程對隊列訪問的順序是不能保證線程公平(即先阻塞,先插入)的。
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); }
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
http://www.importnew.com/21889.html
java在編寫多線程程序時,爲了保證線程安全,須要對數據同步,常常用到兩種同步方式就是Synchronized和重入鎖ReentrantLock。
synchronized是java內置的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由JVM實現,用戶不須要顯示的釋放鎖,很是方便。然而synchronized也有必定的侷限性
例如:
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(); } }
ReentrantLock 一些特性
公平鎖:線程獲取鎖的順序和調用lock的順序同樣,FIFO;
非公平鎖:線程獲取鎖的順序和調用lock的順序無關,全憑運氣。
Java併發包(java.util.concurrent)中大量使用了CAS操做,涉及到併發的地方都調用了sun.misc.Unsafe類方法進行CAS操做。
簡單來講,ReenTrantLock的實現是一種自旋鎖,經過循環調用CAS操做來實現加鎖。它的性能比較好也是由於避免了使線程進入內核態的阻塞狀態。想盡辦法避免線程進入內核的阻塞狀態是咱們去分析和理解鎖設計的關鍵鑰匙。
在Synchronized優化之前,synchronized的性能是比ReenTrantLock差不少的,可是自從Synchronized引入了偏向鎖,輕量級鎖(自旋鎖)後,二者的性能就差很少了,在兩種方法均可用的狀況下,官方甚至建議使用synchronized,其實synchronized的優化我感受就借鑑了ReenTrantLock中的CAS技術。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。
synchronized:
在資源競爭不是很激烈的狀況下,偶爾會有同步的情形下,synchronized是很合適的。緣由在於,編譯程序一般會盡量的進行優化synchronize,另外可讀性很是好。
ReentrantLock:
ReentrantLock用起來會複雜一些。在基本的加鎖和解鎖上,二者是同樣的,因此無特殊狀況下,推薦使用synchronized。ReentrantLock的優點在於它更靈活、更強大,增長了輪訓、超時、中斷等高級功能。
ReentrantLock默認使用非公平鎖是基於性能考慮,公平鎖爲了保證線程規規矩矩地排隊,須要增長阻塞和喚醒的時間開銷。若是直接插隊獲取非公平鎖,跳過了對隊列的處理,速度會更快。
ReentrantLock實現原理
http://www.javashuo.com/article/p-gcsvtqbz-go.html
分析ReentrantLock的實現原理(ReentrantLock和同步工具類的實現基礎都是AQS)
https://www.jianshu.com/p/fe027772e156
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()用來釋放許可。注意,在釋放許可以前,必須先獲得到許可。
這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釋放出機器
Lock接口比同步方法和同步塊提供了更具擴展性的鎖操做。他們容許更靈活的結構,能夠具備徹底不一樣的性質,而且能夠支持多個相關類的條件對象。
它的優點有:
同一時間只能有一條線程執行固定類的同步方法,可是對於類的非同步方法,能夠多條線程同時訪問。因此,這樣就有問題了,可能線程A在執行Hashtable的put方法添加數據,線程B則能夠正常調用size()方法讀取Hashtable中當前元素的個數,那讀取到的值可能不是最新的,可能線程A添加了完了數據,可是沒有對size++,線程B就已經讀取size了,那麼對於線程B來講讀取到的size必定是不許確的。
而給size()方法加了同步以後,意味着線程B調用size()方法只有在線程A調用put方法完畢以後才能夠調用,這樣就保證了線程安全性
ConcurrentHashMap的併發度就是segment的大小,默認爲16,這意味着最多同時能夠有16條線程操做ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最大優點
Lock比傳統線程模型中的synchronized方式更加面向對象,與生活中的鎖相似,鎖自己也應該是一個對象。兩個線程執行的代碼片斷要實現同步互斥的效果,它們必須用同一個Lock對象。
讀寫鎖:分爲讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由jvm本身控制的,你只要上好相應的鎖便可。
若是你的代碼只讀數據,能夠不少人同時讀,但不能同時寫,那就上讀鎖;
若是你的代碼修改數據,只能有一我的在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!
ReentrantReadWriteLock會使用兩把鎖來解決問題,一個讀鎖,一個寫鎖
線程進入讀鎖的前提條件:
線程進入寫鎖的前提條件: