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