您好,登錄后才能下訂單哦!
本篇內(nèi)容介紹了“Java并發(fā)編程的原理和應(yīng)用”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!
現(xiàn)代操作系統(tǒng)調(diào)度的最小單元是線程,也叫輕量級進程,在一個線程里可以創(chuàng)建多個線程,這些線程都擁有各自的計數(shù)器、堆棧和局部變量等屬性,并且能夠訪問共享的內(nèi)存變量。處理器在這些線程上高速切換,讓使用者感覺到這些線程在同時執(zhí)行。
使用多線程的原因主要有,更多的處理器核心、更快的響應(yīng)時間、更好的編程模型(Java為多線程編程提供了良好、考究并且一致的編程模型,使開發(fā)人員可以更加專注問題的解決)。但是多線程編程仍然存在以下問題需要解決:線程安全問題、線程活性問題、上下文切換、可靠性等問題。
線程分配到的時間片決定了線程使用處理器資源的多少,線程優(yōu)先級是決定線程需要多或者少分配一些處理器資源的線程屬性。但是線程優(yōu)先級不能作為程序程序正確性的依賴,因為操作系統(tǒng)可以完全不用理會Java線程對于優(yōu)先級的設(shè)定。
線程的狀態(tài)主要有NEW(已創(chuàng)建未啟動)、RUNNABLE(分為READY/RUNNING,前者表示可以被線程調(diào)度器調(diào)度,后者表示正在運行)、BLOCKED(阻塞I/O ,獨占資源如鎖,不會占用處理器資源)、WAITING(wait/join/park方法,notify/notifyAll/unpark方法恢復(fù))、TIMED_WAITING(wait/join/sleep設(shè)定時間方法,類似WAITING,有限等待)、TERMINATED(結(jié)束態(tài),正常返回/拋出異常提前終止)
圖-Java線程的狀態(tài)
如何進行線程的監(jiān)視?主要途徑是獲取并查看程序的線程轉(zhuǎn)儲(Thread Dump),具體方式如下:
圖-獲取線程轉(zhuǎn)儲的方法
Daemon線程是一種支持型線程,主要被用作程序中后臺調(diào)度以及支持性工作,這意味著當(dāng)一個虛擬機不存在非Daemon線程的時候,Java虛擬機將會推出。在構(gòu)建Daemon線程時,不能依靠finally塊中的內(nèi)容來確保執(zhí)行關(guān)閉或清理資源的邏輯。
中斷可以理解為線程的一個標(biāo)識屬性,并不是強迫終止一個線程而是一種協(xié)作機制,是給線程傳遞一個取消信號,但是由線程來決定如何及何時退出。對于以線程提供服務(wù)的程序模塊而言,應(yīng)該封裝取消/關(guān)閉操作,提供單獨的取消/關(guān)閉方法給調(diào)用者(也可以通過設(shè)置boolean變量來控制是否停止任務(wù)并終止該線程),外部調(diào)用者應(yīng)該調(diào)用這些方法而不是直接調(diào)用interrupt。
線程的暫停、恢復(fù)、停止操作suspend、resume、stop已經(jīng)廢棄。
Java內(nèi)置的等待通知機制如下如:
圖-等待/通知的相關(guān)方法
等待通知機制,是指一個線程A調(diào)用了對象O的wait方法進入等待狀態(tài),而另一個線程B調(diào)用了對象O的notify或者notifyAll方法,線程A收到通知后從對象O的wait方法返回,進而執(zhí)行后續(xù)操作。上述兩個線程通過對象O來完成交互,而對象上的wait和notify/notifyAll的關(guān)系就如同開關(guān)信號一樣,用來完成等待方和通知方之間的交互工作。
線程A執(zhí)行threadB.join()語句的含義是:當(dāng)前線程A等待threadB線程終止之后才能從threadB.join()返回,同時支持超時機制,如果線程threadB在給定的超時時間里沒有終止,那么將會從該超時方法中直接返回。join底層實現(xiàn)借助等待通知機制,當(dāng)線程終止時會調(diào)用線程自身的notifyAll方法,會通知所有等待在該線程對象上的線程。
開發(fā)在實際中經(jīng)常碰到這樣的調(diào)用場景:調(diào)用一個方法時等待一段時間,如果該方法能夠在給定的時間段內(nèi)得到結(jié)果,那么將立即返回,反之,超時將返回默認(rèn)結(jié)果。
定義如下變量:等待持續(xù)時間,remaining = T,超時時間,future = now + T。實現(xiàn)的偽代碼如下所示:
public synchronized Object get(long mills) throws InterruptedException { // 返回結(jié)果 Object result = new Object(); long future = System.currentTimeMillis() + mills; long remaining = mills; // 當(dāng)超時大于0并且返回值不滿足要求 while (result == null && remaining > 0) { wait(remaining); remaining = future - System.currentTimeMillis(); } return result; }
在并發(fā)編程中,需要解決兩個關(guān)鍵問題:線程之間如何通信以及線程之間如何同步。通信是指線程之間以何種機制來交換信息,一般有兩種:共享內(nèi)存和消息傳遞。在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),通過讀-寫內(nèi)存中的公共狀態(tài)進行隱式通信。在消息傳遞的并發(fā)模型里,線程之間沒有公共狀態(tài),線程之間必須通過發(fā)送消息來顯式進行通信。
同步是指程序中用于控制不同線程間操作發(fā)生相對順序的機制。在共享內(nèi)存并發(fā)模型里,同步是顯式進行的,程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執(zhí)行。在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進行的。
Java的并發(fā)采用的是共享內(nèi)存模型,線程之間的通信總是隱式進行的,整個通信過程對于程序員完全透明。
Java中所有實例域、靜態(tài)域和數(shù)組元素(共享變量)都在存儲在堆內(nèi)存中,堆內(nèi)存在線程之間共享,局部變量,方法定義參數(shù)和異常處理參數(shù)不會在線程之間共享,因此不會有內(nèi)存可見性問題,也不受內(nèi)存模型的影響。Java線程之間的通信由Java內(nèi)存模型(簡稱JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。如下圖所示,線程A和線程B之間要通信的話,必須經(jīng)歷以下兩個步驟:
線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中;
線程B到主內(nèi)存中區(qū)讀取A之前已更新過的共享變量。
從整體上看,這兩個步驟實際上是線程A在向線程B發(fā)送消息,而且這個通信過程必須要經(jīng)過主內(nèi)存。JMM通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互,來為程序提供內(nèi)存可見性保證。
圖-Java內(nèi)存模型抽象示意圖
在執(zhí)行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序,重排序可能導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題,JMM通過插入特定類型的內(nèi)存屏障等方式,禁止特定類型的編譯器重排序和處理器重排序,提供內(nèi)存可見性保證。
圖-從源碼到最終執(zhí)行的指令序列的示意圖
圖-內(nèi)存屏障類型表
在JMM中,如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關(guān)系,happens-before規(guī)則對應(yīng)于一個或多個編譯器和處理器重排序規(guī)則。兩個操作之間具有happens-before關(guān)系并不意味著前一個操作必須要在后一個操作之前執(zhí)行,僅僅要求前一個操作(執(zhí)行的結(jié)果)對后一個操作可見,且前一個操作按順序排在第二個操作之前。常見的規(guī)則有:監(jiān)視器鎖規(guī)則,對于一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖;volatile變量規(guī)則,對于volatile域的寫,happens-before于后續(xù)對這個域的讀;以及happens-before傳遞性等。
A happens-before B,JMM并不要求A一定要在B之前執(zhí)行。JMM僅僅要求前一個操作(執(zhí)行的結(jié)果)對后一個操作可見,且前一個操作按順序排在第二個操作之前。這里操作A的執(zhí)行結(jié)果不需要對操作B可見;而且重排序操作A和操作B后的執(zhí)行結(jié)果,與操作A和操作B按happens-before順序執(zhí)行的結(jié)果一致。在這種情況下,JMM會認(rèn)為這種重排序并不非法(not illegal),JMM允許這種重排序。做到:在不改變程序執(zhí)行結(jié)果的前提下,盡可能提供并行度。
as-if-serial語義是指,不管怎么重排序(編譯器和處理器為了提供并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系(寫后讀、寫后寫、讀后寫)的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果,但是如果不存在數(shù)據(jù)依賴關(guān)系,則可能被重排序。
在多線程中,對存在控制依賴的操作重排序,不會改變執(zhí)行結(jié)果。在多線程程序中,對存在控制依賴的操作做重排序,可能會改變程序的執(zhí)行結(jié)果。
順序一致性內(nèi)存模型,規(guī)定一個線程中的所有操作必須按照程序的順序來執(zhí)行;不管程序是否同步,所有線程都只能看到一個單一的操作執(zhí)行順序,在順序一致性模型中,每個操作都必須原子執(zhí)行且立刻對所有線程可見。
關(guān)于重排序的案例可以參考:芋道源碼-【死磕Java并發(fā)】—–Java內(nèi)存模型之重排序
關(guān)于JMM,可以參考Hollis-再有人問你Java內(nèi)存模型是什么,就把這篇文章發(fā)給他、Hollis-JVM內(nèi)存結(jié)構(gòu)VS Java內(nèi)存模型 VS Java對象模型
芋道源碼-Java各種鎖的小結(jié)
匠心零度-面試官問:Java中的鎖有哪些?我跪了
Lock接口實現(xiàn)鎖功能,與synchronize類似,并且支持鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等synchronize關(guān)鍵字鎖不具備的同步特性。
圖-Lock接口主要特性
圖-Lock API
隊列同步器AQS,用來構(gòu)建鎖或者其它同步組件的基礎(chǔ)框架 ,它使用了一個int成員變量表示同步狀態(tài),通過內(nèi)置的FIFO隊列來完成資源獲取線程的排隊工作。同步器是實現(xiàn)鎖(任意同步組件)的關(guān)鍵,鎖是面向使用者的,它定義了使用者與鎖交互的接口,隱藏了實現(xiàn)細(xì)節(jié);同步器面向的是鎖的實現(xiàn)者,它簡化了鎖的實現(xiàn)方式,屏蔽了同步狀態(tài)管理、線程的排隊、等待與喚醒等底層操作。
具體實現(xiàn)原理可以參考:大白話聊聊Java并發(fā)面試問題之談?wù)勀銓QS的理解?【石杉的架構(gòu)筆記】
表示能夠支持一個線程對資源的重復(fù)加鎖。synchronized關(guān)鍵字隱式的支持重入鎖,比如一個synchronized關(guān)鍵字修飾的遞歸方法,在方法執(zhí)行時,執(zhí)行線程在獲取了鎖之后仍能連續(xù)多次的獲取該鎖。ReentrantLock顧名思義也是支持可重復(fù)的。重進入是指任意線程在獲取到鎖之后能夠再次獲取該鎖而不會被阻塞,需要考慮以下兩個問題:
線程再次獲得鎖,鎖需要識別獲取鎖的線程是否為當(dāng)前占據(jù)鎖的線程,如果是,則再次獲取成功;
鎖的最終釋放,線程重復(fù)n次獲取鎖之后,隨后在第n次釋放該鎖后,其它線程能夠獲得到該鎖,鎖的最終釋放要求鎖對于獲取進行計數(shù)自增,計數(shù)表示當(dāng)前鎖被重復(fù)獲取的次數(shù),而鎖被釋放時,計數(shù)自減,當(dāng)計數(shù)等于0時表示鎖已經(jīng)成功釋放。
公平與非公平(默認(rèn)實現(xiàn))獲取鎖的區(qū)別在于,是否需要等待比當(dāng)前線程更早地請求獲取鎖的線程釋放鎖,非公平可能會出現(xiàn)“饑餓”問題,公平鎖的實現(xiàn)代價是按照FIFO的原則進行大量的線程切換,需要針對公平性和吞吐量進行權(quán)衡。
分離讀鎖與寫鎖,使得并發(fā)性相對一般的排它鎖有很大的提升,特別適合讀多寫少的場景(掘金-大白話聊聊Java并發(fā)面試問題之微服務(wù)注冊中心的讀寫鎖優(yōu)化【石杉的架構(gòu)筆記】)
public class Cache { // 線程非安全的map,通過讀寫鎖保證線程安全 static Map<String, Object> map = new HashMap<>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); static Lock w = rwl.writeLock(); public static final Object get(String key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } public static final Object put(String key, Object value) { w.lock(); try { return map.put(key, value); } finally { w.unlock(); } } }
關(guān)于讀寫鎖的內(nèi)部原理可以參考:Java并發(fā)編程之鎖機制之ReentrantReadWriteLock(讀寫鎖)
構(gòu)建同步組件的基礎(chǔ)工具,提供最基本的線程阻塞和喚醒功能。
圖-LockSupport提供的阻塞和喚醒方法
Condition是一種廣義上的條件隊列,為線程提供了一種更為靈活的等待/通知模式,線程在調(diào)用await方法后執(zhí)行掛起操作,直到線程等待的某個條件為真時才會被喚醒。Condition必須要配合鎖一起使用,因為對共享狀態(tài)變量的訪問發(fā)生在多線程環(huán)境下。一個Condition的實例必須與一個Lock綁定,因此Condition一般都是作為Lock的內(nèi)部實現(xiàn)。最典型的應(yīng)用場景有生產(chǎn)者/消費者模式、ArrayBlockQueue等。
具體實現(xiàn)原理可以參考:匠心零度-死磕Java并發(fā)】—–J.U.C之Condition
對于同步方法,JVM采用ACC_SYNCHRONIZED標(biāo)記符來實現(xiàn)同步;對于同步代碼塊,JVM采用monitorenter、monitorexit兩個指令來實現(xiàn)同步。synchronized可以保證原子性、可見性與有序性。
synchronized是重量級鎖,Jdk1.6對鎖的實現(xiàn)引入了大量的優(yōu)化,如自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術(shù)來減少鎖操作的開銷。
深入分析可以參考:
深入理解多線程(一)——Synchronized的實現(xiàn)原理
深入理解多線程(二)—— Java的對象模型
深入理解多線程(三)—— Java的對象頭
深入理解多線程(四)—— Moniter的實現(xiàn)原理
深入理解多線程(五)—— Java虛擬機的鎖優(yōu)化技術(shù)、InfoQ-聊聊并發(fā)(二)—Java SE1.6 中的 Synchronized(鎖升級優(yōu)化)
再有人問你synchronized是什么,就把這篇文章發(fā)給他。
輕量級synchronized,在多處理器開發(fā)中保證了共享變量的可見性,可見性指一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值,JMM確保所有線程看到這個變量的值是一致的。
深入分析可以參考:
深入理解Java中的volatile關(guān)鍵字
再有人問你volatile是什么,把這篇文章也發(fā)給他。
Java經(jīng)典面試題:為什么 ConcurrentHashMap的讀操作不需要加鎖?
純潔的微笑:面試必問之 ConcurrentHashMap 線程安全的具體實現(xiàn)方式
Hollis:詳解ConcurrentHashMap及JDK8的優(yōu)化
芋道源碼:不止 JDK7 的 HashMap ,JDK8 的 ConcurrentHashMap 也會造成 CPU 100%?原因與解決~
程序猿DD: 解讀Java 8 中為并發(fā)而生的 ConcurrentHashMap
CSDN-ConcurrentHashMap(JDK1.8)為什么要放棄Segment
基于鏈接節(jié)點的無界線程安全隊列,它采用先進先出的規(guī)則對節(jié)點進行排序,采用CAS算法實現(xiàn)。那么它是如何實現(xiàn)線程安全的,入隊出隊函數(shù)都是操作volatile變量:head、tail,所以要保證隊列線程安全只需要保證對這兩個Node操作的可見性和原子性,由于volatile本身保證可見性,所以只需要看下多線程下如果保證對著兩個變量操作的原子性。對于offer操作是在tail后面添加元素,也就是調(diào)用tail.casNext方法,而這個方法是使用的CAS操作,只有一個線程會成功,然后失敗的線程會循環(huán)一下,重新獲取tail,然后執(zhí)行casNext方法,對于poll也是這樣的。
深入分析可以參考:
并發(fā)編程網(wǎng)-并發(fā)隊列-無界非阻塞隊列ConcurrentLinkedQueue原理探究
CSDN-Java并發(fā)編程之ConcurrentLinkedQueue詳解
阻塞隊列是支持阻塞插入和移除元素的隊列,常用于生產(chǎn)者和消費者的場景。
JDK提供了7個阻塞隊列:
ArrayBlockingQueue :數(shù)組、有界、FIFO、默認(rèn)非公平 LinkedBlockingQueue :鏈表、有界、默認(rèn)和最大長度Integer.MAX_VALUE PriorityBlockingQueue :支持優(yōu)先級(排序規(guī)則)、無界 DelayQueue:支持延時、無界 SynchronousQueue:不存儲元素、傳遞性場景,吞吐量高 LinkedTransferQueue:鏈表、無界、預(yù)占模式、ConcurrentLinkedQueue、SynchronousQueue (公平模式下)、無界的LinkedBlockingQueues等的超集 LinkedBlockingDeque:鏈表、雙向、容量可選
深入使用與分析可以參考:
程序猿DD-死磕Java并發(fā):J.U.C之阻塞隊列:LinkedTransferQueue
程序猿DD-死磕Java并發(fā):J.U.C之阻塞隊列:LinkedBlockingDeque
InfoQ-聊聊并發(fā)(七)——Java中的阻塞隊列
阻塞隊列的實現(xiàn)原理,使用通知模式實現(xiàn),ArrayBlockingQueue借助notEmpty、notFull兩個Condition來實現(xiàn)。當(dāng)線程被阻塞隊列阻塞時,線程會進入WAITING(parking)狀態(tài)。
Fork/Join是切分并合并子任務(wù)的框架,主要步驟分為:分割出足夠小的任務(wù);執(zhí)行任務(wù)并合并結(jié)果,子任務(wù)放在雙端隊列(工作竊取)里邊,然后啟動線程分別從雙端隊列里獲取任務(wù)執(zhí)行,子任務(wù)結(jié)果統(tǒng)一放在一個隊列里,啟動一個線程從隊列里拿數(shù)據(jù),然后合并這些數(shù)據(jù)。
深入分析和使用參考:
InfoQ-聊聊并發(fā)(八)—— Fork/Join框架介紹
包括原子更新基本類型AtomicBoolean、AtomicInteger、AtomicLong;原子更新數(shù)組類型AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray;原子更新引用類型AtomicReference、AtomicReferenceFieldUpdater、AtomicMarkableReference;原子更新字段類型AtomicIntegeFieldUpdater、AtomicLongFieldUpdater、AtomicStampedReference(解決CAS的ABA問題)。
原子操作類對比直接加鎖提供了一種更輕量級的原子性實現(xiàn)方案,采取樂觀鎖的思想,即沖突檢測+數(shù)據(jù)更新,基于CAS實現(xiàn)(樂觀鎖是一種思想,CAS是這種思想的一種實現(xiàn)方式),CAS的做法很簡單:即比較并替換,三個參數(shù),一個當(dāng)前內(nèi)存值V、舊的預(yù)期值A(chǔ)、即將更新的值B,當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時,將內(nèi)存值修改為B并返回true,否則什么都不做,并返回false。底層實現(xiàn),Unsafe是CAS的核心類,Java無法直接訪問底層操作系統(tǒng),而是通過本地(native)方法來訪問,不過盡管如此,JVM還是開了一個后門:Unsafe,它提供了硬件級別的原子操作。
CAS相對于其它鎖,不會進行內(nèi)核態(tài)操作,有著一些性能的提升。但同時引入自旋,當(dāng)鎖競爭較大的時候,自旋次數(shù)會增多,循環(huán)時間太長,cpu資源會消耗很高,換句話說CAS+自旋適合使用在競爭不激烈的低并發(fā)應(yīng)用場景。在Java 8中引入了4個新的計數(shù)器類型,LongAdder、LongAccumulator、DoubleAdder、DoubleAccumulator,主要思想是當(dāng)競爭不激烈的時候所有線程都是通過CAS對同一個變量(Base)進行修改,當(dāng)競爭激烈的時候會將根據(jù)當(dāng)前線程哈希到對應(yīng)Cell上進行修改(多段鎖),主要原理是通過CAS樂觀鎖保證原子性,通過自旋保證當(dāng)次修改的最終修改成功,通過降低鎖粒度(多段鎖)增加并發(fā)性能。
同時CAS只能保證一個共享變量原子操作,如果是多個共享變量就只能使用鎖了,當(dāng)然如果你有辦法把多個變量整成一個變量,利用CAS也不錯。例如讀寫鎖中state的高地位。
CAS只比對值,在一般場景下不會引起邏輯錯誤(例如余額),但是在特殊情況下,值雖然相同,但是可能已經(jīng)是此A非彼A了(例如并發(fā)情況下的堆棧),因此CAS不能只比對值,還必須保證是原來的數(shù)據(jù)才能修改成功,一種做法是將值比對升級為版本號的比對,一個數(shù)據(jù)一個版本,版本變化,即使值相同,也不應(yīng)該修改成功。(參考架構(gòu)師之路-并發(fā)扣款一致性優(yōu)化,CAS下ABA問題,這個話題還沒聊完)。Java提供了AtomicStampedReference來解決,AtomicStampedReference通過包裝[E,Integer]的元組來對對象標(biāo)記版本戳stamp,從而避免ABA問題。
Java并發(fā)容器、框架、工具類
“Java并發(fā)編程的原理和應(yīng)用”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。