摘要:下面是線程相關(guān)的熱門面試題,你可以用它來好好準(zhǔn)備面試。線程安全問題都是由全局變量及靜態(tài)變量引起的。持有自旋鎖的線程在之前應(yīng)該釋放自旋鎖以便其它線程可以獲得自旋鎖。
最近看到網(wǎng)上流傳著,各種面試經(jīng)驗及面試題,往往都是一大堆技術(shù)題目貼上去,而沒有答案。
不管你是新程序員還是老手,你一定在面試中遇到過有關(guān)線程的問題。Java語言一個重要的特點就是內(nèi)置了對并發(fā)的支持,讓Java大受企業(yè)和程序員的歡迎。大多數(shù)待遇豐厚的Java開發(fā)職位都要求開發(fā)者精通多線程技術(shù)并且有豐富的Java程序開發(fā)、調(diào)試、優(yōu)化經(jīng)驗,所以線程相關(guān)的問題在面試中經(jīng)常會被提到。
在典型的Java面試中, 面試官會從線程的基本概念問起
如:為什么你需要使用線程, 如何創(chuàng)建線程,用什么方式創(chuàng)建線程比較好(比如:繼承thread類還是調(diào)用Runnable接口),然后逐漸問到并發(fā)問題像在Java并發(fā)編程的過程中遇到了什么挑戰(zhàn),Java內(nèi)存模型,JDK1.5引入了哪些更高階的并發(fā)工具,并發(fā)編程常用的設(shè)計模式,經(jīng)典多線程問題如生產(chǎn)者消費者,哲學(xué)家就餐,讀寫器或者簡單的有界緩沖區(qū)問題。僅僅知道線程的基本概念是遠遠不夠的, 你必須知道如何處理死鎖,競態(tài)條件,內(nèi)存沖突和線程安全等并發(fā)問題。掌握了這些技巧,你就可以輕松應(yīng)對多線程和并發(fā)面試了。
許多Java程序員在面試前才會去看面試題,這很正常。
因為收集面試題和練習(xí)很花時間,所以我從許多面試者那里收集了Java多線程和并發(fā)相關(guān)的50個熱門問題。
下面是Java線程相關(guān)的熱門面試題,你可以用它來好好準(zhǔn)備面試。
什么是線程?
什么是線程安全和線程不安全?
什么是自旋鎖?
什么是Java內(nèi)存模型?
什么是CAS?
什么是樂觀鎖和悲觀鎖?
什么是AQS?
什么是原子操作?在Java Concurrency API中有哪些原子類(atomic classes)?
什么是Executors框架?
什么是阻塞隊列?如何使用阻塞隊列來實現(xiàn)生產(chǎn)者-消費者模型?
什么是Callable和Future?
什么是FutureTask?
什么是同步容器和并發(fā)容器的實現(xiàn)?
什么是多線程?優(yōu)缺點?
什么是多線程的上下文切換?
ThreadLocal的設(shè)計理念與作用?
ThreadPool(線程池)用法與優(yōu)勢?
Concurrent包里的其他東西:ArrayBlockingQueue、CountDownLatch等等。
synchronized和ReentrantLock的區(qū)別?
Semaphore有什么作用?
Java Concurrency API中的Lock接口(Lock interface)是什么?對比同步它有什么優(yōu)勢?
Hashtable的size()方法中明明只有一條語句”return count”,為什么還要做同步?
ConcurrentHashMap的并發(fā)度是什么?
ReentrantReadWriteLock讀寫鎖的使用?
CyclicBarrier和CountDownLatch的用法及區(qū)別?
LockSupport工具?
Condition接口及其實現(xiàn)原理?
Fork/Join框架的理解?
wait()和sleep()的區(qū)別?
線程的五個狀態(tài)(五種狀態(tài),創(chuàng)建、就緒、運行、阻塞和死亡)?
start()方法和run()方法的區(qū)別?
Runnable接口和Callable接口的區(qū)別?
volatile關(guān)鍵字的作用?
Java中如何獲取到線程dump文件?
線程和進程有什么區(qū)別?
線程實現(xiàn)的方式有幾種(四種)?
高并發(fā)、任務(wù)執(zhí)行時間短的業(yè)務(wù)怎樣使用線程池?并發(fā)不高、任務(wù)執(zhí)行時間長的業(yè)務(wù)怎樣使用線程池?并發(fā)高、業(yè)務(wù)執(zhí)行時間長的業(yè)務(wù)怎樣使用線程池?
如果你提交任務(wù)時,線程池隊列已滿,這時會發(fā)生什么?
鎖的等級:方法鎖、對象鎖、類鎖?
如果同步塊內(nèi)的線程拋出異常會發(fā)生什么?
并發(fā)編程(concurrency)并行編程(parallellism)有什么區(qū)別?
如何保證多線程下 i++ 結(jié)果正確?
一個線程如果出現(xiàn)了運行時異常會怎么樣?
如何在兩個線程之間共享數(shù)據(jù)?
生產(chǎn)者消費者模型的作用是什么?
怎么喚醒一個阻塞的線程?
Java中用到的線程調(diào)度算法是什么
單例模式的線程安全性?
線程類的構(gòu)造方法、靜態(tài)塊是被哪個線程調(diào)用的?
同步方法和同步塊,哪個是更好的選擇?
如何檢測死鎖?怎么預(yù)防死鎖?
什么是線程?線程是操作系統(tǒng)能夠進行運算調(diào)度的最小單位,它被包含在進程之中,是進程中的實際運作單位,可以使用多線程對進行運算提速。
比如,如果一個線程完成一個任務(wù)要100毫秒,那么用十個線程完成改任務(wù)只需10毫秒
什么是線程安全和線程不安全?通俗的說:加鎖的就是是線程安全的,不加鎖的就是是線程不安全的
線程安全線程安全: 就是多線程訪問時,采用了加鎖機制,當(dāng)一個線程訪問該類的某個數(shù)據(jù)時,進行保護,其他線程不能進行訪問,直到該線程讀取完,其他線程才可使用。不會出現(xiàn)數(shù)據(jù)不一致或者數(shù)據(jù)污染。
一個線程安全的計數(shù)器類的同一個實例對象在被多個線程使用的情況下也不會出現(xiàn)計算失誤。很顯然你可以將集合類分成兩組,線程安全和非線程安全的。
Vector 是用同步方法來實現(xiàn)線程安全的, 而和它相似的ArrayList不是線程安全的。
線程不安全:就是不提供數(shù)據(jù)訪問保護,有可能出現(xiàn)多個線程先后更改數(shù)據(jù)造成所得到的數(shù)據(jù)是臟數(shù)據(jù)
如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結(jié)果和單線程運行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的。
線程安全問題都是由全局變量及靜態(tài)變量引起的。
若每個線程中對全局變量、靜態(tài)變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執(zhí)行寫操作,一般都需要考慮線程同步,否則的話就可能影響線程安全。
自旋鎖是SMP架構(gòu)中的一種low-level的同步機制。
當(dāng)線程A想要獲取一把自選鎖而該鎖又被其它線程鎖持有時,線程A會在一個循環(huán)中自選以檢測鎖是不是已經(jīng)可用了。
自選鎖需要注意:
由于自旋時不釋放CPU,因而持有自旋鎖的線程應(yīng)該盡快釋放自旋鎖,否則等待該自旋鎖的線程會一直在那里自旋,這就會浪費CPU時間。
持有自旋鎖的線程在sleep之前應(yīng)該釋放自旋鎖以便其它線程可以獲得自旋鎖。
實現(xiàn)自旋鎖參考
https://segmentfault.com/q/1010000000530936
一個簡單的while就可以滿足你的要求。
目前的JVM實現(xiàn)自旋會消耗CPU,如果長時間不調(diào)用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內(nèi)存模型?
Java內(nèi)存模型描述了在多線程代碼中哪些行為是合法的,以及線程如何通過內(nèi)存進行交互。它描述了“程序中的變量“ 和 ”從內(nèi)存或者寄存器獲取或存儲它們的底層細節(jié)”之間的關(guān)系。Java內(nèi)存模型通過使用各種各樣的硬件和編譯器的優(yōu)化來正確實現(xiàn)以上事情。
Java包含了幾個語言級別的關(guān)鍵字,包括:volatile, final以及synchronized,目的是為了幫助程序員向編譯器描述一個程序的并發(fā)需求。Java內(nèi)存模型定義了volatile和synchronized的行為,更重要的是保證了同步的java程序在所有的處理器架構(gòu)下面都能正確的運行。
“一個線程的寫操作對其他線程可見”這個問題是因為編譯器對代碼進行重排序?qū)е碌?。例如,只要代碼移動不會改變程序的語義,當(dāng)編譯器認為程序中移動一個寫操作到后面會更有效的時候,編譯器就會對代碼進行移動。如果編譯器推遲執(zhí)行一個操作,其他線程可能在這個操作執(zhí)行完之前都不會看到該操作的結(jié)果,這反映了緩存的影響。
此外,寫入內(nèi)存的操作能夠被移動到程序里更前的時候。在這種情況下,其他的線程在程序中可能看到一個比它實際發(fā)生更早的寫操作。所有的這些靈活性的設(shè)計是為了通過給編譯器,運行時或硬件靈活性使其能在最佳順序的情況下來執(zhí)行操作。在內(nèi)存模型的限定之內(nèi),我們能夠獲取到更高的性能。
看下面代碼展示的一個簡單例子:
ClassReordering { int x = 0, y = 0; public void writer() { x = 1; y = 2; } public void reader() { int r1 = y; int r2 = x; } }
讓我們看在兩個并發(fā)線程中執(zhí)行這段代碼,讀取Y變量將會得到2這個值。因為這個寫入比寫到X變量更晚一些,程序員可能認為讀取X變量將肯定會得到1。但是,寫入操作可能被重排序過。如果重排序發(fā)生了,那么,就能發(fā)生對Y變量的寫入操作,讀取兩個變量的操作緊隨其后,而且寫入到X這個操作能發(fā)生。程序的結(jié)果可能是r1變量的值是2,但是r2變量的值為0。
但是面試官,有時候不這么認為,認為就是JVM內(nèi)存結(jié)構(gòu)JVM內(nèi)存結(jié)構(gòu)主要有三大塊:堆內(nèi)存、方法區(qū)和棧。
堆內(nèi)存是JVM中最大的一塊由年輕代和老年代組成,而年輕代內(nèi)存又被分成三部分,Eden空間、From Survivor空間、To Survivor空間,默認情況下年輕代按照8:1:1的比例來分配;方法區(qū)存儲類信息、常量、靜態(tài)變量等數(shù)據(jù),是線程共享的區(qū)域,為與Java堆區(qū)分,方法區(qū)還有一個別名Non-Heap(非堆);棧又分為java虛擬機棧和本地方法棧主要用于方法的執(zhí)行。
JAVA的JVM的內(nèi)存可分為3個區(qū):堆(heap)、棧(stack)和方法區(qū)(method)
java堆(Java Heap)可通過參數(shù) -Xms 和-Xmx設(shè)置
Java堆是被所有線程共享,是Java虛擬機所管理的內(nèi)存中最大的一塊 Java堆在虛擬機啟動時創(chuàng)建。
Java堆唯一的目的是存放對象實例,幾乎所有的對象實例和數(shù)組都在這里。
Java堆為了便于更好的回收和分配內(nèi)存,可以細分為:新生代和老年代;再細致一點的有Eden空間、From Survivor空間、To Survivor區(qū)。
新生代:包括Eden區(qū)、From Survivor區(qū)、To Survivor區(qū),系統(tǒng)默認大小Eden:Survivor=8:1。
老年代:在年輕代中經(jīng)歷了N次垃圾回收后仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象。
Survivor空間等Java堆可以處在物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可(就像我們的磁盤空間一樣。在實現(xiàn)時,既可以實現(xiàn)成固定大小的,也可以是可擴展的)。
據(jù)Java虛擬機規(guī)范的規(guī)定,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常。
java虛擬機棧(stack)可通過參數(shù) 棧幀是方法運行期的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)棧容量可由-Xss設(shè)置
1.Java虛擬機棧是線程私有的,它的生命周期與線程相同。
每一個方法被調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程。
虛擬機棧是執(zhí)行Java方法的內(nèi)存模型(也就是字節(jié)碼)服務(wù):每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀,用于存儲 局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。
局部變量表:32位變量槽,存放了編譯期可知的各種基本數(shù)據(jù)類型、對象引用、returnAddress類型。
操作數(shù)棧:基于棧的執(zhí)行引擎,虛擬機把操作數(shù)棧作為它的工作區(qū),大多數(shù)指令都要從這里彈出數(shù)據(jù)、執(zhí)行運算,然后把結(jié)果壓回操作數(shù)棧。
動態(tài)連接:每個棧幀都包含一個指向運行時常量池(方法區(qū)的一部分)中該棧幀所屬方法的引用。持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接。Class文件的常量池中有大量的符號引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用為參數(shù)。這些符號引用一部分會在類加載階段或第一次使用的時候轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。另一部分將在每一次的運行期間轉(zhuǎn)化為直接應(yīng)用,這部分稱為動態(tài)連接
方法出口:返回方法被調(diào)用的位置,恢復(fù)上層方法的局部變量和操作數(shù)棧,如果無返回值,則把它壓入調(diào)用者的操作數(shù)棧。
局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的。
在方法運行期間不會改變局部變量表的大小。主要存放了編譯期可知的各種基本數(shù)據(jù)類型、對象引用 (reference類型)、returnAddress類型)。
java虛擬機棧,規(guī)定了兩種異常狀況:
如果線程請求的深度大于虛擬機所允許的深度,將拋出StackOverflowError異常。
如果虛擬機棧動態(tài)擴展,而擴展時無法申請到足夠的內(nèi)存,就會拋出OutOfMemoryError異常。
本地方法棧可通過參數(shù) 棧容量可由-Xss設(shè)置
虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù)。
本地方法棧則是為虛擬機使用到的Native方法服務(wù)。有的虛擬機(譬如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二為一
方法區(qū)(Method Area)可通過參數(shù)-XX:MaxPermSize設(shè)置
線程共享內(nèi)存區(qū)域,用于儲存已被虛擬機加載的類信息、常量、靜態(tài)變量,即編譯器編譯后的代碼,方法區(qū)也稱持久代(Permanent Generation)。
雖然Java虛擬機規(guī)范把方法區(qū)描述為堆的一個邏輯部分,但是它卻有一個別名叫做Non-Heap(非堆),目的應(yīng)該是與Java堆區(qū)分開來。
如何實現(xiàn)方法區(qū),屬于虛擬機的實現(xiàn)細節(jié),不受虛擬機規(guī)范約束。
方法區(qū)主要存放java類定義信息,與垃圾回收關(guān)系不大,方法區(qū)可以選擇不實現(xiàn)垃圾回收,但不是沒有垃圾回收。
方法區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載。
運行時常量池,也是方法區(qū)的一部分,虛擬機加載Class后把常量池中的數(shù)據(jù)放入運行時常量池。
運行時常量池JDK1.6之前字符串常量池位于方法區(qū)之中。
JDK1.7字符串常量池已經(jīng)被挪到堆之中。
可通過參數(shù)-XX:PermSize和-XX:MaxPermSize設(shè)置
常量池(Constant Pool):常量池數(shù)據(jù)編譯期被確定,是Class文件中的一部分。存儲了類、方法、接口等中的常量,當(dāng)然也包括字符串常量。
字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存儲編譯期類中產(chǎn)生的字符串類型數(shù)據(jù)。
運行時常量池(Runtime Constant Pool):方法區(qū)的一部分,所有線程共享。虛擬機加載Class后把常量池中的數(shù)據(jù)放入到運行時常量池。常量池:可以理解為Class文件之中的資源倉庫,它是Class文件結(jié)構(gòu)中與其他項目資源關(guān)聯(lián)最多的數(shù)據(jù)類型。
常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic Reference)。
字面量:文本字符串、聲明為final的常量值等。
符號引用:類和接口的完全限定名(Fully Qualified Name)、字段的名稱和描述符(Descriptor)、方法的名稱和描述符。
直接內(nèi)存可通過-XX:MaxDirectMemorySize指定,如果不指定,則默認與Java堆的最大值(-Xmx指定)一樣。
直接內(nèi)存(Direct Memory)并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。
總結(jié)的簡單一點java堆(Java Heap)
可通過參數(shù) -Xms 和-Xmx設(shè)置
Java堆是被所有線程共享,是Java虛擬機所管理的內(nèi)存中最大的一塊 Java堆在虛擬機啟動時創(chuàng)建
Java堆唯一的目的是存放對象實例,幾乎所有的對象實例和數(shù)組都在這里
Java堆為了便于更好的回收和分配內(nèi)存,可以細分為:新生代和老年代;再細致一點的有Eden空間、From Survivor空間、To Survivor區(qū)
新生代:包括Eden區(qū)、From Survivor區(qū)、To Survivor區(qū),系統(tǒng)默認大小Eden:Survivor=8:1。
老年代:在年輕代中經(jīng)歷了N次垃圾回收后仍然存活的對象,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命周期較長的對象。
java虛擬機棧(stack)
可通過參數(shù) 棧幀是方法運行期的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)棧容量可由-Xss設(shè)置
Java虛擬機棧是線程私有的,它的生命周期與線程相同。
每一個方法被調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程。
虛擬機棧是執(zhí)行Java方法的內(nèi)存模型(也就是字節(jié)碼)服務(wù):每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀,用于存儲 局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息
方法區(qū)(Method Area)
可通過參數(shù)-XX:MaxPermSize設(shè)置
線程共享內(nèi)存區(qū)域),用于儲存已被虛擬機加載的類信息、常量、靜態(tài)變量,即編譯器編譯后的代碼,方法區(qū)也稱持久代(Permanent Generation)。
方法區(qū)主要存放java類定義信息,與垃圾回收關(guān)系不大,方法區(qū)可以選擇不實現(xiàn)垃圾回收,但不是沒有垃圾回收。
方法區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載。
運行時常量池,也是方法區(qū)的一部分,虛擬機加載Class后把常量池中的數(shù)據(jù)放入運行時常量池。
什么是CAS?CAS(compare and swap)的縮寫,中文翻譯成比較并交換。
CAS 不通過JVM,直接利用java本地方 JNI(Java Native Interface為JAVA本地調(diào)用),直接調(diào)用CPU 的cmpxchg(是匯編指令)指令。
利用CPU的CAS指令,同時借助JNI來完成Java的非阻塞算法,實現(xiàn)原子操作。其它原子操作都是利用類似的特性完成的。
整個java.util.concurrent都是建立在CAS之上的,因此對于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
CAS是項樂觀鎖技術(shù),當(dāng)多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程并不會被掛起,而是被告知這次競爭中失敗,并可以再次嘗試。
CAS應(yīng)用CAS有3個操作數(shù),內(nèi)存值V,舊的預(yù)期值A(chǔ),要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值V修改為B,否則什么都不做。
CAS優(yōu)點確保對內(nèi)存的讀-改-寫操作都是原子操作執(zhí)行
CAS缺點CAS雖然很高效的解決原子操作,但是CAS仍然存在三大問題。ABA問題,循環(huán)時間長開銷大和只能保證一個共享變量的原子操作
總結(jié)使用CAS在線程沖突嚴(yán)重時,會大幅降低程序性能;CAS只適合于線程沖突較少的情況使用。
synchronized在jdk1.6之后,已經(jīng)改進優(yōu)化。synchronized的底層實現(xiàn)主要依靠Lock-Free的隊列,基本思路是自旋后阻塞,競爭切換后繼續(xù)競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量。在線程沖突較少的情況下,可以獲得和CAS類似的性能;而線程沖突嚴(yán)重的情況下,性能遠高于CAS。
參考
https://blog.52itstyle.com/archives/948/
Java在JDK1.5之前都是靠synchronized關(guān)鍵字保證同步的,這種通過使用一致的鎖定協(xié)議來協(xié)調(diào)對共享狀態(tài)的訪問,可以確保無論哪個線程持有共享變量的鎖,都采用獨占的方式來訪問這些變量。獨占鎖其實就是一種悲觀鎖,所以可以說synchronized是悲觀鎖。
樂觀鎖樂觀鎖( Optimistic Locking)其實是一種思想。相對悲觀鎖而言,樂觀鎖假設(shè)認為數(shù)據(jù)一般情況下不會造成沖突,所以在數(shù)據(jù)進行提交更新的時候,才會正式對數(shù)據(jù)的沖突與否進行檢測,如果發(fā)現(xiàn)沖突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。
什么是AQS?AbstractQueuedSynchronizer簡稱AQS,是一個用于構(gòu)建鎖和同步容器的框架。事實上concurrent包內(nèi)許多類都是基于AQS構(gòu)建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解決了在實現(xiàn)同步容器時設(shè)計的大量細節(jié)問題。
AQS使用一個FIFO的隊列表示排隊等待鎖的線程,隊列頭節(jié)點稱作“哨兵節(jié)點”或者“啞節(jié)點”,它不與任何線程關(guān)聯(lián)。其他的節(jié)點與等待線程關(guān)聯(lián),每個節(jié)點維護一個等待狀態(tài)waitStatus。
CAS 原子操作在concurrent包的實現(xiàn)參考
https://blog.52itstyle.com/archives/948/
由于java的CAS同時具有 volatile 讀和volatile寫的內(nèi)存語義,因此Java線程之間的通信現(xiàn)在有了下面四種方式:
A線程寫volatile變量,隨后B線程讀這個volatile變量。
A線程寫volatile變量,隨后B線程用CAS更新這個volatile變量。
A線程用CAS更新一個volatile變量,隨后B線程用CAS更新這個volatile變量。
A線程用CAS更新一個volatile變量,隨后B線程讀這個volatile變量。
Java的CAS會使用現(xiàn)代處理器上提供的高效機器級別原子指令,這些原子指令以原子方式對內(nèi)存執(zhí)行讀-改-寫操作,這是在多處理器中實現(xiàn)同步的關(guān)鍵(從本質(zhì)上來說,能夠支持原子性讀-改-寫指令的計算機器,是順序計算圖靈機的異步等價機器,因此任何現(xiàn)代的多處理器都會去支持某種能對內(nèi)存執(zhí)行原子性讀-改-寫操作的原子指令)。同時,volatile變量的讀/寫和CAS可以實現(xiàn)線程之間的通信。把這些特性整合在一起,就形成了整個concurrent包得以實現(xiàn)的基石。如果我們仔細分析concurrent包的源代碼實現(xiàn),會發(fā)現(xiàn)一個通用化的實現(xiàn)模式:
首先,聲明共享變量為volatile;
然后,使用CAS的原子條件更新來實現(xiàn)線程之間的同步;
同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內(nèi)存語義來實現(xiàn)線程之間的通信。
AQS,非阻塞數(shù)據(jù)結(jié)構(gòu)和原子變量類(Java.util.concurrent.atomic包中的類),這些concurrent包中的基礎(chǔ)類都是使用這種模式來實現(xiàn)的,而concurrent包中的高層類又是依賴于這些基礎(chǔ)類來實現(xiàn)的。從整體來看,concurrent包的實現(xiàn)示意圖如下:
AQS沒有鎖之類的概念,它有個state變量,是個int類型,在不同場合有著不同含義。
AQS圍繞state提供兩種基本操作“獲取”和“釋放”,有條雙向隊列存放阻塞的等待線程,并提供一系列判斷和處理方法,簡單說幾點:
state是獨占的,還是共享的;
state被獲取后,其他線程需要等待;
state被釋放后,喚醒等待線程;
線程等不及時,如何退出等待。
至于線程是否可以獲得state,如何釋放state,就不是AQS關(guān)心的了,要由子類具體實現(xiàn)。
AQS中還有一個表示狀態(tài)的字段state,例如ReentrantLocky用它表示線程重入鎖的次數(shù),Semaphore用它表示剩余的許可數(shù)量,F(xiàn)utureTask用它表示任務(wù)的狀態(tài)。對state變量值的更新都采用CAS操作保證更新操作的原子性。
AbstractQueuedSynchronizer繼承了AbstractOwnableSynchronizer,這個類只有一個變量:exclusiveOwnerThread,表示當(dāng)前占用該鎖的線程,并且提供了相應(yīng)的get,set方法。
ReentrantLock實現(xiàn)原理
https://www.cnblogs.com/maypattis/p/6403682.html
什么是原子操作?在Java Concurrency API中有哪些原子類(atomic classes)?原子操作是指一個不受其他操作影響的操作任務(wù)單元。原子操作是在多線程環(huán)境下避免數(shù)據(jù)不一致必須的手段。
int++并不是一個原子操作,所以當(dāng)一個線程讀取它的值并加1時,另外一個線程有可能會讀到之前的值,這就會引發(fā)錯誤。
為了解決這個問題,必須保證增加操作是原子的,在JDK1.5之前我們可以使用同步技術(shù)來做到這一點。
到JDK1.5,java.util.concurrent.atomic包提供了int和long類型的裝類,它們可以自動的保證對于他們的操作是原子的并且不需要使用同步。
Executor框架同java.util.concurrent.Executor 接口在Java 5中被引入。
Executor框架是一個根據(jù)一組執(zhí)行策略調(diào)用,調(diào)度,執(zhí)行和控制的異步任務(wù)的框架。
無限制的創(chuàng)建線程會引起應(yīng)用程序內(nèi)存溢出。所以創(chuàng)建一個線程池是個更好的的解決方案,因為可以限制線程的數(shù)量并且可以回收再利用這些線程。
利用Executors框架可以非常方便的創(chuàng)建一個線程池,
Java通過Executors提供四種線程池,分別為:
newCachedThreadPool創(chuàng)建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
newFixedThreadPool 創(chuàng)建一個定長線程池,可控制線程最大并發(fā)數(shù),超出的線程會在隊列中等待。
newScheduledThreadPool 創(chuàng)建一個定長線程池,支持定時及周期性任務(wù)執(zhí)行。
newSingleThreadExecutor 創(chuàng)建一個單線程化的線程池,它只會用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行。
JDK7提供了7個阻塞隊列。(也屬于并發(fā)容器)
ArrayBlockingQueue :一個由數(shù)組結(jié)構(gòu)組成的有界阻塞隊列。
LinkedBlockingQueue :一個由鏈表結(jié)構(gòu)組成的有界阻塞隊列。
PriorityBlockingQueue :一個支持優(yōu)先級排序的無界阻塞隊列。
DelayQueue:一個使用優(yōu)先級隊列實現(xiàn)的無界阻塞隊列。
SynchronousQueue:一個不存儲元素的阻塞隊列。
LinkedTransferQueue:一個由鏈表結(jié)構(gòu)組成的無界阻塞隊列。
LinkedBlockingDeque:一個由鏈表結(jié)構(gòu)組成的雙向阻塞隊列。
什么是阻塞隊列?阻塞隊列是一個在隊列基礎(chǔ)上又支持了兩個附加操作的隊列。
2個附加操作:
支持阻塞的插入方法:隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。
支持阻塞的移除方法:隊列空時,獲取元素的線程會等待隊列變?yōu)榉强铡?/p>
阻塞隊列的應(yīng)用場景
阻塞隊列常用于生產(chǎn)者和消費者的場景,生產(chǎn)者是向隊列里添加元素的線程,消費者是從隊列里取元素的線程。簡而言之,阻塞隊列是生產(chǎn)者用來存放元素、消費者獲取元素的容器。
幾個方法在阻塞隊列不可用的時候,上述2個附加操作提供了四種處理方法
方法處理方式 | 拋出異常 | 返回特殊值 | 一直阻塞 | 超時退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
檢查方法 | element() | peek() | 不可用 | 不可用 |
JDK 7 提供了7個阻塞隊列,如下
1、ArrayBlockingQueue 數(shù)組結(jié)構(gòu)組成的有界阻塞隊列。
此隊列按照先進先出(FIFO)的原則對元素進行排序,但是默認情況下不保證線程公平的訪問隊列,即如果隊列滿了,那么被阻塞在外面的線程對隊列訪問的順序是不能保證線程公平(即先阻塞,先插入)的。
2、LinkedBlockingQueue一個由鏈表結(jié)構(gòu)組成的有界阻塞隊列
此隊列按照先出先進的原則對元素進行排序
3、PriorityBlockingQueue支持優(yōu)先級的無界阻塞隊列
4、DelayQueue支持延時獲取元素的無界阻塞隊列,即可以指定多久才能從隊列中獲取當(dāng)前元素
5、SynchronousQueue不存儲元素的阻塞隊列,每一個put必須等待一個take操作,否則不能繼續(xù)添加元素。并且他支持公平訪問隊列。
6、LinkedTransferQueue由鏈表結(jié)構(gòu)組成的無界阻塞TransferQueue隊列。相對于其他阻塞隊列,多了tryTransfer和transfer方法
transfer方法
如果當(dāng)前有消費者正在等待接收元素(take或者待時間限制的poll方法),transfer可以把生產(chǎn)者傳入的元素立刻傳給消費者。如果沒有消費者等待接收元素,則將元素放在隊列的tail節(jié)點,并等到該元素被消費者消費了才返回。
tryTransfer方法
用來試探生產(chǎn)者傳入的元素能否直接傳給消費者。,如果沒有消費者在等待,則返回false。和上述方法的區(qū)別是該方法無論消費者是否接收,方法立即返回。而transfer方法是必須等到消費者消費了才返回。
7、LinkedBlockingDeque鏈表結(jié)構(gòu)的雙向阻塞隊列,優(yōu)勢在于多線程入隊時,減少一半的競爭。
如何使用阻塞隊列來實現(xiàn)生產(chǎn)者-消費者模型?通知模式實現(xiàn):所謂通知模式,就是當(dāng)生產(chǎn)者往滿的隊列里添加元素時會阻塞住生產(chǎn)者,當(dāng)消費者消費了一個隊列中的元素后,會通知生產(chǎn)者當(dāng)前隊列可用。
使用BlockingQueue解決生產(chǎn)者消費者問題為什么BlockingQueue適合解決生產(chǎn)者消費者問題
任何有效的生產(chǎn)者-消費者問題解決方案都是通過控制生產(chǎn)者put()方法(生產(chǎn)資源)和消費者take()方法(消費資源)的調(diào)用來實現(xiàn)的,一旦你實現(xiàn)了對方法的阻塞控制,那么你將解決該問題。
Java通過BlockingQueue提供了開箱即用的支持來控制這些方法的調(diào)用(一個線程創(chuàng)建資源,另一個消費資源)。java.util.concurrent包下的BlockingQueue接口是一個線程安全的可用于存取對象的隊列。
BlockingQueue是一種數(shù)據(jù)結(jié)構(gòu),支持一個線程往里存資源,另一個線程從里取資源。這正是解決生產(chǎn)者消費者問題所需要的,那么讓我們開始解決該問題吧。
生產(chǎn)者
以下代碼用于生產(chǎn)者線程
package io.ymq.example.thread; import java.util.concurrent.BlockingQueue; /** * 描述:生產(chǎn)者 * * @author yanpenglei * @create 2018-03-14 15:52 **/ class Producer implements Runnable { protected BlockingQueue
消費者
以下代碼用于消費者線程
package io.ymq.example.thread; import java.util.concurrent.BlockingQueue; /** * 描述: 消費者 * * @author yanpenglei * @create 2018-03-14 15:54 **/ class Consumer implements Runnable { protected BlockingQueuequeue; Consumer(BlockingQueue 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; BlockingQueuemyQueue = new LinkedBlockingQueue (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); } }
運行結(jié)果
生產(chǎn)者資源隊列大小= 1 生產(chǎn)者資源隊列大小= 1 消費者 資源 隊列大小 1 生產(chǎn)者資源隊列大小= 1 消費者 資源 隊列大小 1 消費者 資源 隊列大小 1 生產(chǎn)者資源隊列大小= 1 生產(chǎn)者資源隊列大小= 3 消費對象 java.lang.Object@1e1aa52b 生產(chǎn)者資源隊列大小= 2 生產(chǎn)者資源隊列大小= 5 消費對象 java.lang.Object@6e740a76 消費對象 java.lang.Object@697853f6 ...... 消費對象 java.lang.Object@41a10cbc 消費對象 java.lang.Object@4963c8d1 消費者 資源 隊列大小 5 生產(chǎn)者資源隊列大小= 5 生產(chǎn)者資源隊列大小= 5 消費者 資源 隊列大小 4 消費對象 java.lang.Object@3e49c35d 消費者 資源 隊列大小 4 生產(chǎn)者資源隊列大小= 5
從輸出結(jié)果中,我們可以發(fā)現(xiàn)隊列大小永遠不會超過5,消費者線程消費了生產(chǎn)者生產(chǎn)的資源。
什么是Callable和Future?Callable 和 Future 是比較有趣的一對組合。當(dāng)我們需要獲取線程的執(zhí)行結(jié)果時,就需要用到它們。Callable用于產(chǎn)生結(jié)果,F(xiàn)uture用于獲取結(jié)果。
Callable接口使用泛型去定義它的返回類型。Executors類提供了一些有用的方法去在線程池中執(zhí)行Callable內(nèi)的任務(wù)。由于Callable任務(wù)是并行的,必須等待它返回的結(jié)果。java.util.concurrent.Future對象解決了這個問題。
在線程池提交Callable任務(wù)后返回了一個Future對象,使用它可以知道Callable任務(wù)的狀態(tài)和得到Callable返回的執(zhí)行結(jié)果。Future提供了get()方法,等待Callable結(jié)束并獲取它的執(zhí)行結(jié)果。
代碼示例
Callable 是一個接口,它只包含一個call()方法。Callable是一個返回結(jié)果并且可能拋出異常的任務(wù)。
為了便于理解,我們可以將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 任務(wù),并將其提交到一個ExecutorService. 將返回一個描述任務(wù)情況的Future. Callablecall = new Callable () { @Override public String call() throws Exception { System.out.println("start new thread "); Thread.sleep(5000); System.out.println("end new thread "); return "我是返回的內(nèi)容"; } }; Future task = exec.submit(call); Thread.sleep(1000); String retn = task.get(); //關(guān)閉線程池 exec.shutdown(); System.out.println(retn + "--end main thread"); } }
控制臺打印
start main thread start new thread end new thread 我是返回的內(nèi)容--end main thread什么是FutureTask?
FutureTask可用于異步獲取執(zhí)行結(jié)果或取消執(zhí)行任務(wù)的場景。通過傳入Runnable或者Callable的任務(wù)給FutureTask,直接調(diào)用其run方法或者放入線程池執(zhí)行,之后可以在外部通過FutureTask的get方法異步獲取執(zhí)行結(jié)果,因此,F(xiàn)utureTask非常適合用于耗時的計算,主線程可以在完成自己的任務(wù)后,再去獲取結(jié)果。另外,F(xiàn)utureTask還可以確保即使調(diào)用了多次run方法,它都只會執(zhí)行一次Runnable或者Callable任務(wù),或者通過cancel取消FutureTask的執(zhí)行等。
1.執(zhí)行多任務(wù)計算FutureTask執(zhí)行多任務(wù)計算的使用場景
利用FutureTask和ExecutorService,可以用多線程的方式提交計算任務(wù),主線程繼續(xù)執(zhí)行其他任務(wù),當(dāng)主線程需要子線程的計算結(jié)果時,在異步獲取子線程的執(zhí)行結(jié)果。
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(); // 創(chuàng)建任務(wù)集合 List> taskList = new ArrayList >(); // 創(chuàng)建線程池 ExecutorService exec = Executors.newFixedThreadPool(5); for (int i = 0; i < 10; i++) { // 傳入Callable對象創(chuàng)建FutureTask對象 FutureTask ft = new FutureTask (inst.new ComputeTask(i, "" + i)); taskList.add(ft); // 提交給線程池執(zhí)行任務(wù),也可以通過exec.invokeAll(taskList)一次性提交所有任務(wù); exec.submit(ft); } System.out.println("所有計算任務(wù)提交完畢, 主線程接著干其他事情!"); // 開始統(tǒng)計各計算線程計算結(jié)果 Integer totalResult = 0; for (FutureTask ft : taskList) { try { //FutureTask的get方法會自動阻塞,直到獲取計算結(jié)果為止 totalResult = totalResult + ft.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } // 關(guān)閉線程池 exec.shutdown(); System.out.println("多任務(wù)計算后的總結(jié)果是:" + totalResult); } private class ComputeTask implements Callable { private Integer result = 0; private String taskName = ""; public ComputeTask(Integer iniResult, String taskName) { result = iniResult; this.taskName = taskName; System.out.println("生成子線程計算任務(wù): " + 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秒鐘,觀察主線程行為,預(yù)期的結(jié)果是主線程會繼續(xù)執(zhí)行,到要取得FutureTask的結(jié)果是等待直至完成。 Thread.sleep(5000); System.out.println("子線程計算任務(wù): " + taskName + " 執(zhí)行完成!"); return result; } } }
生成子線程計算任務(wù): 0 生成子線程計算任務(wù): 1 生成子線程計算任務(wù): 2 生成子線程計算任務(wù): 3 生成子線程計算任務(wù): 4 生成子線程計算任務(wù): 5 生成子線程計算任務(wù): 6 生成子線程計算任務(wù): 7 生成子線程計算任務(wù): 8 生成子線程計算任務(wù): 9 所有計算任務(wù)提交完畢, 主線程接著干其他事情! 子線程計算任務(wù): 0 執(zhí)行完成! 子線程計算任務(wù): 2 執(zhí)行完成! 子線程計算任務(wù): 3 執(zhí)行完成! 子線程計算任務(wù): 4 執(zhí)行完成! 子線程計算任務(wù): 1 執(zhí)行完成! 子線程計算任務(wù): 8 執(zhí)行完成! 子線程計算任務(wù): 7 執(zhí)行完成! 子線程計算任務(wù): 6 執(zhí)行完成! 子線程計算任務(wù): 9 執(zhí)行完成! 子線程計算任務(wù): 5 執(zhí)行完成! 多任務(wù)計算后的總結(jié)果是:9902.高并發(fā)環(huán)境下
FutureTask在高并發(fā)環(huán)境下確保任務(wù)只執(zhí)行一次
在很多高并發(fā)的環(huán)境下,往往我們只需要某些任務(wù)只執(zhí)行一次。這種使用情景FutureTask的特性恰能勝任。舉一個例子,假設(shè)有一個帶key的連接池,當(dāng)key存在時,即直接返回key對應(yīng)的對象;當(dāng)key不存在時,則創(chuàng)建連接。對于這樣的應(yīng)用場景,通常采用的方法為使用一個Map對象來存儲key和連接池對應(yīng)的對應(yīng)關(guān)系,典型的代碼如下面所示:
private MapconnectionPool = new HashMap (); private ReentrantLock lock = new ReentrantLock(); public Connection getConnection(String key) { try { lock.lock(); if (connectionPool.containsKey(key)) { return connectionPool.get(key); } else { //創(chuàng)建 Connection Connection conn = createConnection(); connectionPool.put(key, conn); return conn; } } finally { lock.unlock(); } } //創(chuàng)建Connection private Connection createConnection() { return null; }
在上面的例子中,我們通過加鎖確保高并發(fā)環(huán)境下的線程安全,也確保了connection只創(chuàng)建一次,然而確犧牲了性能。改用ConcurrentHash的情況下,幾乎可以避免加鎖的操作,性能大大提高,但是在高并發(fā)的情況下有可能出現(xiàn)Connection被創(chuàng)建多次的現(xiàn)象。這時最需要解決的問題就是當(dāng)key不存在時,創(chuàng)建Connection的動作能放在connectionPool之后執(zhí)行,這正是FutureTask發(fā)揮作用的時機,基于ConcurrentHashMap和FutureTask的改造代碼如下:
private ConcurrentHashMap> connectionPool = new ConcurrentHashMap >(); public Connection getConnection(String key) throws Exception { FutureTask connectionTask = connectionPool.get(key); if (connectionTask != null) { return connectionTask.get(); } else { Callable callable = new Callable () { @Override public Connection call() throws Exception { // TODO Auto-generated method stub return createConnection(); } }; FutureTask newTask = new FutureTask (callable); connectionTask = connectionPool.putIfAbsent(key, newTask); if (connectionTask == null) { connectionTask = newTask; connectionTask.run(); } return connectionTask.get(); } } //創(chuàng)建Connection private Connection createConnection() { return null; }
經(jīng)過這樣的改造,可以避免由于并發(fā)帶來的多次創(chuàng)建連接及鎖的出現(xiàn)。
什么是同步容器和并發(fā)容器的實現(xiàn)? 一、同步容器主要代表有Vector和Hashtable,以及Collections.synchronizedXxx等。
鎖的粒度為當(dāng)前對象整體。
迭代器是及時失敗的,即在迭代的過程中發(fā)現(xiàn)被修改,就會拋出ConcurrentModificationException。
主要代表有ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentSkipListMap、ConcurrentSkipListSet。
鎖的粒度是分散的、細粒度的,即讀和寫是使用不同的鎖。
迭代器具有弱一致性,即可以容忍并發(fā)修改,不會拋出ConcurrentModificationException。
JDK 7 ConcurrentHashMap
采用分離鎖技術(shù),同步容器中,是一個容器一個鎖,但在ConcurrentHashMap中,會將hash表的數(shù)組部分分成若干段,每段維護一個鎖,以達到高效的并發(fā)訪問;
JDK 8 ConcurrentHashMap
采用分離鎖技術(shù),同步容器中,是一個容器一個鎖,但在ConcurrentHashMap中,會將hash表的數(shù)組部分分成若干段,每段維護一個鎖,以達到高效的并發(fā)訪問;
三、阻塞隊列主要代表有LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue(Comparable,Comparator)、SynchronousQueue。
提供了可阻塞的put和take方法,以及支持定時的offer和poll方法。
適用于生產(chǎn)者、消費者模式(線程池和工作隊列-Executor),同時也是同步容器
主要代表有ArrayDeque和LinkedBlockingDeque。
意義:正如阻塞隊列適用于生產(chǎn)者消費者模式,雙端隊列同樣適用與另一種模式,即工作密取。在生產(chǎn)者-消費者設(shè)計中,所有消費者共享一個工作隊列,而在工作密取中,每個消費者都有各自的雙端隊列。
如果一個消費者完成了自己雙端隊列中的全部工作,那么他就可以從其他消費者的雙端隊列末尾秘密的獲取工作。具有更好的可伸縮性,這是因為工作者線程不會在單個共享的任務(wù)隊列上發(fā)生競爭。
在大多數(shù)時候,他們都只是訪問自己的雙端隊列,從而極大的減少了競爭。當(dāng)工作者線程需要訪問另一個隊列時,它會從隊列的尾部而不是頭部獲取工作,因此進一步降低了隊列上的競爭。
適用于:網(wǎng)頁爬蟲等任務(wù)中
如果不需要阻塞隊列,優(yōu)先選擇ConcurrentLinkedQueue;
如果需要阻塞隊列,隊列大小固定優(yōu)先選擇ArrayBlockingQueue,隊列大小不固定優(yōu)先選擇LinkedBlockingQueue;
如果需要對隊列進行排序,選擇PriorityBlockingQueue;
如果需要一個快速交換的隊列,選擇SynchronousQueue;
如果需要對隊列中的元素進行延時操作,則選擇DelayQueue。
什么是多線程?
多線程:是指從軟件或者硬件上實現(xiàn)多個線程的并發(fā)技術(shù)。
多線程的好處:
使用多線程可以把程序中占據(jù)時間長的任務(wù)放到后臺去處理,如圖片、視屏的下載
發(fā)揮多核處理器的優(yōu)勢,并發(fā)執(zhí)行讓系統(tǒng)運行的更快、更流暢,用戶體驗更好
多線程的缺點:
大量的線程降低代碼的可讀性;
更多的線程需要更多的內(nèi)存空間
當(dāng)多個線程對同一個資源出現(xiàn)爭奪時候要注意線程安全的問題。
什么是多線程的上下文切換?即使是單核CPU也支持多線程執(zhí)行代碼,CPU通過給每個線程分配CPU時間片來實現(xiàn)這個機制。時間片是CPU分配給各個線程的時間,因為時間片非常短,所以CPU通過不停地切換線程執(zhí)行,讓我們感覺多個線程時同時執(zhí)行的,時間片一般是幾十毫秒(ms)
上下文切換過程中,CPU會停止處理當(dāng)前運行的程序,并保存當(dāng)前程序運行的具體位置以便之后繼續(xù)運行
CPU通過時間片分配算法來循環(huán)執(zhí)行任務(wù),當(dāng)前任務(wù)執(zhí)行一個時間片后會切換到下一個任務(wù)。但是,在切換前會保存上一個任務(wù)的狀態(tài),以便下次切換回這個任務(wù)時,可以再次加載這個任務(wù)的狀態(tài)
從任務(wù)保存到再加載的過程就是一次上下文切換
ThreadLocal的設(shè)計理念與作用?Java中的ThreadLocal類允許我們創(chuàng)建只能被同一個線程讀寫的變量。因此,如果一段代碼含有一個ThreadLocal變量的引用,即使兩個線程同時執(zhí)行這段代碼,它們也無法訪問到對方的ThreadLocal變量
ThreadLocal如何創(chuàng)建ThreadLocal變量
以下代碼展示了如何創(chuàng)建一個ThreadLocal變量:
private ThreadLocal myThreadLocal = new ThreadLocal();
通過這段代碼實例化了一個ThreadLocal對象。我們只需要實例化對象一次,并且也不需要知道它是被哪個線程實例化。雖然所有的線程都能訪問到這個ThreadLocal實例,但是每個線程卻只能訪問到自己通過調(diào)用ThreadLocal的set()方法設(shè)置的值。即使是兩個不同的線程在同一個ThreadLocal對象上設(shè)置了不同的值,他們?nèi)匀粺o法訪問到對方的值。
如何訪問ThreadLocal變量
一旦創(chuàng)建了一個ThreadLocal變量,你可以通過如下代碼設(shè)置某個需要保存的值:
myThreadLocal.set("A thread local value”);
可以通過下面方法讀取保存在ThreadLocal變量中的值:
String threadLocalValue = (String) myThreadLocal.get();
get()方法返回一個Object對象,set()對象需要傳入一個Object類型的參數(shù)。
為ThreadLocal指定泛型類型
public static ThreadLocalmyThreadLocal = new ThreadLocal ();
我們可以創(chuàng)建一個指定泛型類型的ThreadLocal對象,這樣我們就不需要每次對使用get()方法返回的值作強制類型轉(zhuǎn)換了。下面展示了指定泛型類型的ThreadLocal例子:
ThreadLocal的設(shè)計理念與作用
http://blog.csdn.net/u0118607...://blog.csdn.net/u011860731/article/details/48733073)
InheritableThreadLocalpublic static ThreadLocalthreadLocal = new InheritableThreadLocal ();
InheritableThreadLocal類是ThreadLocal類的子類。ThreadLocal中每個線程擁有它自己的值,與ThreadLocal不同的是,InheritableThreadLocal允許一個線程以及該線程創(chuàng)建的所有子線程都可以訪問它保存的值。
InheritableThreadLocal 原理
Java 多線程:InheritableThreadLocal 實現(xiàn)原理
http://blog.csdn.net/ni357103403/article/details/51970748
ThreadPool(線程池)用法與優(yōu)勢? 為什么要用線程池:減少了創(chuàng)建和銷毀線程的次數(shù),每個工作線程都可以被重復(fù)利用,可執(zhí)行多個任務(wù)。
可以根據(jù)系統(tǒng)的承受能力,調(diào)整線程池中工作線線程的數(shù)目,防止因為消耗過多的內(nèi)存,而把服務(wù)器累趴下(每個線程需要大約1MB內(nèi)存,線程開的越多,消耗的內(nèi)存也就越大,最后死機)。
Java里面線程池的頂級接口是Executor,但是嚴(yán)格意義上講Executor并不是一個線程池,而只是一個執(zhí)行線程的工具。真正的線程池接口是ExecutorService。
new Thread 缺點每次new Thread新建對象性能差。
線程缺乏統(tǒng)一管理,可能無限制新建線程,相互之間競爭,及可能占用過多系統(tǒng)資源導(dǎo)致死機或oom。
缺乏更多功能,如定時執(zhí)行、定期執(zhí)行、線程中斷。
ThreadPool 優(yōu)點減少了創(chuàng)建和銷毀線程的次數(shù),每個工作線程都可以被重復(fù)利用,可執(zhí)行多個任務(wù)
可以根據(jù)系統(tǒng)的承受能力,調(diào)整線程池中工作線線程的數(shù)目,防止因為因為消耗過多的內(nèi)存,而把服務(wù)器累趴下(每個線程需要大約1MB內(nèi)存,線程開的越多,消耗的內(nèi)存也就越大,最后死機)
減少在創(chuàng)建和銷毀線程上所花的時間以及系統(tǒng)資源的開銷
如不使用線程池,有可能造成系統(tǒng)創(chuàng)建大量線程而導(dǎo)致消耗完系統(tǒng)內(nèi)存
Java提供的四種線程池的好處在于:
重用存在的線程,減少對象創(chuàng)建、銷毀的開銷,提高性能。
可有效控制最大并發(fā)線程數(shù),提高系統(tǒng)資源的使用率,同時避免過多資源競爭,避免堵塞。
提供定時執(zhí)行、定期執(zhí)行、單線程、并發(fā)數(shù)控制等功能。
比較重要的幾個類:類 | 描述 |
---|---|
ExecutorService | 真正的線程池接口。 |
ScheduledExecutorService | 能和Timer/TimerTask類似,解決那些需要任務(wù)重復(fù)執(zhí)行的問題。 |
ThreadPoolExecutor | ExecutorService的默認實現(xiàn)。 |
ScheduledThreadPoolExecutor | 繼承ThreadPoolExecutor的ScheduledExecutorService接口實現(xiàn),周期性任務(wù)調(diào)度的類實現(xiàn)。 |
要配置一個線程池是比較復(fù)雜的,尤其是對于線程池的原理不是很清楚的情況下,很有可能配置的線程池不是較優(yōu)的,因此在Executors類里面提供了一些靜態(tài)工廠,生成一些常用的線程池。
Executors提供四種線程池newCachedThreadPool創(chuàng)建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
newFixedThreadPool 創(chuàng)建一個定長線程池,可控制線程最大并發(fā)數(shù),超出的線程會在隊列中等待。
newScheduledThreadPool 創(chuàng)建一個定長線程池,支持定時及周期性任務(wù)執(zhí)行。
newSingleThreadExecutor 創(chuàng)建一個單線程化的線程池,它只會用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級)執(zhí)行。
一般都不用Executors提供的線程創(chuàng)建方式使用ThreadPoolExecutor創(chuàng)建線程池
ThreadPoolExecutor的構(gòu)造函數(shù)
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue參數(shù):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核心線程數(shù)大小,當(dāng)線程數(shù)
maximumPoolSize 最大線程數(shù), 當(dāng)線程數(shù) >= corePoolSize的時候,會把runnable放入workQueue中
keepAliveTime 保持存活時間,當(dāng)線程數(shù)大于corePoolSize的空閑線程能保持的最大時間。
unit 時間單位
workQueue 保存任務(wù)的阻塞隊列
threadFactory 創(chuàng)建線程的工廠
handler 拒絕策略 當(dāng)線程數(shù)小于corePoolSize時,創(chuàng)建線程執(zhí)行任務(wù)。 當(dāng)線程數(shù)大于等于corePoolSize并且workQueue沒有滿時,放入workQueue中 線程數(shù)大于等于corePoolSize并且當(dāng)workQueue滿時,新任務(wù)新建線程運行,線程總數(shù)要小于maximumPoolSize 當(dāng)線程總數(shù)等于maximumPoolSize并且workQueue滿了的時候執(zhí)行handler的rejectedExecution。也就是拒絕策略。
ThreadPoolExecutor.AbortPolicy() 直接拋出異常RejectedExecutionException
ThreadPoolExecutor.CallerRunsPolicy() 直接調(diào)用run方法并且阻塞執(zhí)行
ThreadPoolExecutor.DiscardPolicy() 直接丟棄后來的任務(wù)
ThreadPoolExecutor.DiscardOldestPolicy() 丟棄在隊列中隊首的任務(wù) 當(dāng)然可以自己繼承 RejectedExecutionHandler 來寫拒絕策略. https://juejin.im/post/59df0c1af265da432f301c8d 阻塞隊列 1、ArrayBlockingQueue 數(shù)組結(jié)構(gòu)組成的有界阻塞隊列。 此隊列按照先進先出(FIFO)的原則對元素進行排序,但是默認情況下不保證線程公平的訪問隊列,即如果隊列滿了,那么被阻塞在外面的線程對隊列訪問的順序是不能保證線程公平(即先阻塞,先插入)的。 CountDownLatch 允許一個或多個線程等待其他線程完成操作。 應(yīng)用場景 假如有這樣一個需求,當(dāng)我們需要解析一個Excel里多個sheet的數(shù)據(jù)時,可以考慮使用多線程,每個線程解析一個sheet里的數(shù)據(jù),等到所有的sheet都解析完之后,程序需要提示解析完成。 在這個需求中,要實現(xiàn)主線程等待所有線程完成sheet的解析操作,最簡單的做法是使用join。代碼如下: join用于讓當(dāng)前執(zhí)行線程等待join線程執(zhí)行結(jié)束。其實現(xiàn)原理是不停檢查join線程是否存活,如果join線程存活則讓當(dāng)前線程永遠wait,代碼片段如下,wait(0)表示永遠等待下去。 方法isAlive()功能是判斷當(dāng)前線程是否處于活動狀態(tài)。 活動狀態(tài)就是線程啟動且尚未終止,比如正在運行或準(zhǔn)備開始運行。 new CountDownLatch(2)的構(gòu)造函數(shù)接收一個int類型的參數(shù)作為計數(shù)器,如果你想等待N個點完成,這里就傳入N。 當(dāng)我們調(diào)用一次CountDownLatch的countDown()方法時,N就會減1,CountDownLatch的await()會
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");
}
}
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()+"正在執(zhí)行");
Thread.sleep(3000);
System.out.println("子線程"+Thread.currentThread().getName()+"執(zhí)行完畢");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
new Thread(){
public void run() {
try {
System.out.println("子線程"+Thread.currentThread().getName()+"正在執(zhí)行");
Thread.sleep(3000);
System.out.println("子線程"+Thread.currentThread().getName()+"執(zhí)行完畢");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
try {
System.out.println("等待2個子線程執(zhí)行完畢...");
latch.await();
System.out.println("2個子線程已經(jīng)執(zhí)行完畢");
System.out.println("繼續(xù)執(zhí)行主線程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
線程Thread-0正在執(zhí)行
線程Thread-1正在執(zhí)行
等待2個子線程執(zhí)行完畢...
線程Thread-0執(zhí)行完畢
線程Thread-1執(zhí)行完畢
2個子線程已經(jīng)執(zhí)行完畢
繼續(xù)執(zhí)行主線程
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/68852.html
摘要:大多數(shù)待遇豐厚的開發(fā)職位都要求開發(fā)者精通多線程技術(shù)并且有豐富的程序開發(fā)調(diào)試優(yōu)化經(jīng)驗,所以線程相關(guān)的問題在面試中經(jīng)常會被提到。掌握了這些技巧,你就可以輕松應(yīng)對多線程和并發(fā)面試了。進入等待通行準(zhǔn)許時,所提供的對象。 最近看到網(wǎng)上流傳著,各種面試經(jīng)驗及面試題,往往都是一大堆技術(shù)題目貼上去,而沒有答案。 不管你是新程序員還是老手,你一定在面試中遇到過有關(guān)線程的問題。Java語言一個重要的特點就...
摘要:為程序員金三銀四精心挑選的余道面試題與答案,歡迎大家向我推薦你在面試過程中遇到的問題我會把大家推薦的問題添加到下面的常用面試題清單中供大家參考。 為Java程序員金三銀四精心挑選的300余道Java面試題與答案,歡迎大家向我推薦你在面試過程中遇到的問題,我會把大家推薦的問題添加到下面的常用面試題清單中供大家參考。 前兩天寫的以下博客,大家比較認可,熱度不錯,希望可以幫到準(zhǔn)備或者正在參加...
摘要:前言從號開始在寫下第一篇文章說是筆記還差不多,驚奇地收到有人收藏我的文章的消息,覺得有點開心。突然腦子抽到想爬下里標(biāo)簽下的文章有多少,哪篇被收藏最多,哪篇被點贊最多。?!,F(xiàn)在和大家分享下,收藏量前的文章,被那么多人收藏應(yīng)該是篇值得看的文章。 前言 從18號開始在sf寫下第一篇文章(說是筆記還差不多),驚奇地收到有人收藏我的文章的消息,覺得有點開心。突然腦子抽到想爬下sf里JAVA標(biāo)簽下...
閱讀 2051·2023-04-25 15:11
閱讀 3515·2021-09-23 11:57
閱讀 1387·2021-07-26 23:38
閱讀 1326·2019-08-30 15:54
閱讀 645·2019-08-30 15:53
閱讀 3255·2019-08-26 13:36
閱讀 997·2019-08-26 12:01
閱讀 2872·2019-08-23 16:21