Java 并发编程艺术 读书笔记
第 1 章 并發編程的挑戰
1.1.3 如何減少上下文切換
減少上下文切換的方法有無鎖并發編程、CAS 算法、使用最少線程和使用協程。
- 無鎖并發編程。多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的 ID 按照 Hash 算法取模分段,不同的線程處理不同段的數據。
- CAS 算法。Java 的 Atomic 包使用 CAS 算法來更新數據,而不需要加鎖。
- 使用最少線程。避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處于等待狀態。
- 協程:在單線程里實現多任務的調度,并在單線程里維持多個任務間的切換。
1.1.4 減少上下文切換實戰
如何通過 jstack 命令分析線程死鎖:
https://www.jianshu.com/p/bb4ba6612392
1.2 避免死鎖的幾個常見方法
- 避免一個線程同時獲取多個鎖。
- 避免一個線程在鎖內同時占用多個資源,盡量保證每個鎖只占用一個資源。
- 嘗試使用定時鎖,使用 lock.tryLock(timeout)來替代使用內部鎖機制。
- 對于數據庫鎖,加鎖和解鎖必須在一個數據庫連接里,否則會出現解鎖失敗的情況。
1.3 資源限制的挑戰
什么是資源限制
在進行并發編程時,要考慮資源的限制。硬件資源限制有帶寬的上傳/下載速度、硬盤讀寫速度和 CPU 的處理速度。軟件資源限制有數據庫的連接數和 socket 連接數等。
資源限制引發的問題
并發執行,因為受限于資源,仍然在串行執行,這時候程序不僅不會加快執行,反而會更慢,因為增加了上下文切換和資源調度的時間。
如何解決資源限制的問題
對于硬件資源限制,可以考慮使用集群并行執行程序。既然單機的資源有限制,那
么就讓程序在多機上運行。比如使用 ODPS、Hadoop 或者自己搭建服務器集群,不同的
機器處理不同的數據??梢酝ㄟ^“數據 ID%機器數”,計算得到一個機器編號,然后由對
應編號的機器處理這筆數據。
對于軟件資源限制,可以考慮使用資源池將資源復用。比如使用連接池將數據庫和
Socket 連接復用,或者在調用對方 webservice 接口獲取數據時,只建立一個連接。
第 2 章 Java 并發機制的底層實現原理
2.1 volatile 的應用
可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。如果 volatile 變量修飾符使用恰當的話,它比 synchronized 的使用和執行成本更低,因為它不會引起線程上下文的切換和調度。
2.1.1.volatile 的定義與實現原理
Lock 前綴指令會引起處理器緩存回寫到內存。
一個處理器的緩存回寫到內存會導致其他處理器的緩存無效。
2.1.2 volatile 的使用優化
LinkedTransferQueue, 這個內部類 PaddedAtomicReference 相對于父類 AtomicReference 只做了一件事情,就是將共享變量追加到 64 字節。
在緩存一致性機制的作用下,會導致其他處理器不能訪問自己高速緩存中的尾節點,而隊列的入隊和出隊操作則需要不停修改頭節點和尾節點,所以在多處理器的情況下將會嚴重影響到隊列的入隊和出隊效率。Doug lea 使用追加到 64 字節的方式來填滿高速緩沖區的緩存行,避免頭節點和尾節點加載到同一個緩存行,使頭、尾節點在修改時不會互相鎖定。
2.2 synchronized 的實現原理與應用
先來看下利用 synchronized 實現同步的基礎:Java 中的每一個對象都可以作為鎖。
具體表現為以下 3 種形式。
- 對于普通同步方法,鎖是當前實例對象。
- 對于靜態同步方法,鎖是當前類的 Class 對象。
- 對于同步方法塊,鎖是 Synchonized 括號里配置的對象。
Synchonized 在 JVM 里的實現原理:
代碼塊同步是使用 monitorenter 和 monitorexit 指令實現的,而方法同步是使用另外一種方式實現的,細
節在 JVM 規范里并沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。
monitorenter 指令是在編譯后插入到同步代碼塊的開始位置,而 monitorexit 是插入到方法結束處 #FF9800和異常處,JVM 要保證每個 monitorenter 必須有對應的 monitorexit 與之配對。任何對象都有一個 monitor 與之關聯,當且一個 monitor 被持有后,它將處于鎖定狀態。線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 monitor 的所有權,即嘗試獲得對象的鎖。
2.2.1 Java 對象頭
java 的對象頭里面有些什么東西?
synchronized 用的鎖是存在 Java 對象頭里的 , 如果對象是數組類型,則虛擬機用 3 個字寬(Word)存儲對象頭,如果對象是非數組類型,則用 2 字寬存儲對象頭。
Mark work 的作用?
Mark Word 里默認存儲對象的 HashCode、分代年齡和鎖標記位
他是實現偏向鎖的關鍵
2.2.2 鎖的升級與對比
Java SE 1.6 鎖的四種狀態:
--------------- 無鎖狀態 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖
鎖標志位: 01 -> 01 -> 00 -> 10
偏向鎖位: 0 -> 1
詳細介紹鎖升級過程:
多線程高并發:synchronized鎖升級過程及其實現原理
1.偏向鎖
什么是偏向鎖?
所謂偏向鎖,就是偏向某一個線程的意思,它的目的就是消除數據在無爭用的情況下的同步操作,進一步提高運行性能
偏向鎖的獲得和撤銷流程是怎么樣的?
2.輕量鎖
輕量鎖的加鎖及解鎖過程
2.3 原子操作的實現原理
2.3.2.處理器如何實現原子操作
處理器提供總線鎖定 #FF9800和緩存鎖定 #FF9800兩個機制來保證復雜內存操作的原子性
總線鎖
所謂總線鎖就是使用處理器提供的一個 LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占共享內存。
緩存鎖
頻繁使用的內存會緩存在處理器的 L1、L2 和 L3 高速緩存里,那么原子操作就可以直接在處理器內部緩存中進行,并不需要聲明總線鎖,在 Pentium 6 和目前的處理器中可以使用 “緩存鎖定” 的方式來實現復雜的原子性。
2.3.3 Java 如何實現原子操作
AtomicInteger.compareAndSet、AtomicBoolean、AtomicInteger、AtomicLong
CAS 實現原子操作的三大問題
ABA 問題
ABA 問題的解決思路是怎么樣的?
使用版本號,每次修改了變量則在變量前追加版本號
- 循環時間長開銷大
- 只能保證一個共享變量的原子操作
第 3 章 Java 內存模型
什么是 JMM?
JMM 就是 Java 內存模型,因為在不同的硬件生產商和操作系統下,內存的訪問有一定的差異,所以會造成相同的代碼運行在不同的系統上會有不同的問題,為此,Java 內存模型屏蔽掉各種硬件和操作系統之間的內存訪問差異,以實現 Java 程序在不同的平臺下都能達到一致的并發效果
Java 內存模型規定所有變量都存儲在主內存中,包括實例變量、靜態變量,但是不包括局部變量和方法參數,后者是線程私有的。每個線程都有自己的工作內存,線程的工作內存保存了該線程要用到的變量和主內存的副本拷貝,線程對變量的操作都在自己的工作內存中進行。線程不能直接讀寫主內存中的變量。
不同的線程也無法訪問對方的工作內存中的變量,線程之間的變量傳遞均需要主內存來完成。
JMM 定義了什么?
整個 Java 內存模型實際上是圍繞著三個特征建立起來的: 原子性、可見性、有序性
原子性
JMM 只能保證基本的原子性,如果要保證一個代碼塊的原子性,提供了 monitorenter 和 moniterexit 兩個字節碼指令,也就是 synchronized 關鍵字。因此在 synchronized 塊之間的操作都是原子性的。
可見性
可見性指當一個線程修改共享變量的值,其他線程能夠立即知道被修改了。Java 是利用 volatile 關鍵字來提供可見性的。 當變量被 volatile 修飾時,這個變量被修改后會立刻刷新到主內存,當其它線程需要讀取該變量時,會去主內存中讀取新值。而普通變量則不能保證這一點。
除了 volatile 關鍵字之外,final和 synchronized 也能實現可見性。
synchronized 的原理是,在執行完,進入 unlock 之前,必須將共享變量同步到主內存中。
final 修飾的字段,一旦初始化完成,如果沒有對象逸出(指對象為初始化完成就可以被別的線程使用),那么對于其他線程都是可見的。
有序性
在 Java 中,可以使用 synchronized 保證多線程之間操作的有序性。實現原理有些區別:
Volatile 關鍵字是通過內存屏障達到靜止指令重排序,以保證有序性。
synchronized 的原理是,一個線程 lock 之后,必須 unLock 后,其他線程才可以重新 lock,使得被 synchronized 包住的代碼在多線程程序中是串行的
https://zhuanlan.zhihu.com/p/258393139
什么是內存屏障?
https://www.jianshu.com/p/2ab5e3d7e510
3.1.1 并發編程模型的兩個關鍵問題
線程之間如何通信及線程之間如何同步?
線程之間的通信機制有兩種:共享內存和消息傳遞。在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞。
Java 的并發采用的是共享內存模型,Java 線程之間的通信總是隱式進行,整個通信過程對程序員完全透明。
3.1.2 Java 內存模型的抽象結構
在 Java 中,所有實例域、靜態域和數組元素都存儲在堆內存 #FF9800中,堆內存在線程之間共享。局部變量,方法定義參數和異常處理器參數不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。
Java 線程之間的通信由 Java 內存模型(本文簡稱為 JMM)控制,JMM 決定一個線程對共享變量的寫入何時對另一個線程可見。
線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是 JMM 的一個抽象概念,并不真實存在。
如果線程 A 與線程 B 之間要通信的話,必須要經歷下面 2 個步驟。
? 線程 A 把本地內存 A 中更新過的共享變量刷新到主內存中去。
? 線程 B 到主內存中去讀取線程 A 之前已更新過的共享變量。
3.1.3 從源代碼到指令序列的重排序
在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分3 種類型。
語句的執行順序。
從 Java 源代碼到最終實際執行的指令序列,會分別經歷下面 3 種重排序,如圖 3-3所示。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-n3nicp08-1616245944158)(./images/1614229234213.png)]
3.1.4 并發編程模型的分類
3.1.5 happens-before 簡介
happens-before 是什么東西?
我們無法枚舉所有的場景來規定某個線程修改的變量何時對另一個線程可見。但可以制定一些通用的規則,這就是 happens-before
與程序員密切相關的 happens-before 規則如下。
? 程序順序規則:一個線程中的每個操作,happens-before 于該線程中的任意后續操作。
? 監視器鎖規則:對一個鎖的解鎖,happens-before 于隨后對這個鎖的加鎖。
? volatile 變量規則:對一個 volatile 域的寫,happens-before 于任意后續對這個volatile 域的讀。
? 傳遞性:如果 A happens-before B,且 B happens-before C,那么 A happens-before
C。
ps: 兩個操作之間具有 happens-before 關系,并不意味著前一個操作必須要在后一個操
作之前執行!
3.2 重排序
什么是重排序?
重排序是指編譯器和處理器為了優化程序性能而對指令序列進行重新排序的一種手段。
3.2.1 數據依賴性
編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。
3.2.2 as-if-serial 語義
什么是 as-if-serial ?
不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執行結果不能被改變。as-if-serial 語義把單線程程序保護了起來,遵守 as-if-serial 語義的編譯器、runtime 和處理器共同為編寫單線程程序的程序員創建了一個幻覺:單線程程序是按程序的順序來執行的。
3.2.3 程序順序規則
在計算機中,軟件技術和硬件技術有一個共同的目標:在不改變程序執行結果的前提下,盡可能提高并行度。編譯器和處理器遵從這一目標,從 happens-before 的定義我們可以看出, JMM 同樣遵從這一目標。
3.2.4 重排序對多線程的影響
在單線程程序中,對存在控制依賴的操作重排序,不會改變執行結果(這也是 as-ifserial 語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執行結果。
3.3 順序一致性
3.3.1 數據競爭與順序一致性
當程序未正確同步時,就可能會存在數據競爭。Java 內存模型規范對數據競爭的定義如下。
? 在一個線程中寫一個變量,
? 在另一個線程讀同一個變量,
? 而且寫和讀沒有通過同步來排序。
3.3.2 順序一致性內存模型
順序一致性內存模型是什么?
順序一致性內存模型是一個理論參考模型,JMM 和處理器內存模型在設計時通常會以順序一致性內存模型為參照。
順序一致性內存模型有兩大特性是什么?
? 一個線程中的所有操作必須按照程序的順序來執行。
? (不管程序是否同步)所有線程都只能看到一個單一的操作執行順序。在順序一
致性內存模型中,每個操作都必須原子執行且立刻對所有線程可見。
3.3.3 同步程序的順序一致性效果
JMM在具體實現上的基本方針為:在不改變(正確同步的)程序執行結果的前提下,盡可能地為編譯器和處理器的優化打開方便之門。
3.3.4 未同步程序的執行特性
為什么JMM不保證對 64 位的 long 型和 double 型變量的寫操作具有原子性,而順序一致性模型保證對所有的內存讀/寫操作都具有原子性 ?
在計算機中,數據通過總線在處理器和內存之間傳遞。每次處理器和內存之間的數據傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為總線事務(Bus Transaction)??偩€事務包括讀事務(ReadTransaction)和寫事務(WriteTransaction)。讀事務從內存傳送數據到處理器,寫事務從處理器傳送數據到內存,每個事務會讀/寫內存中一個或多個物理上連續的字。這里的關鍵是,總線會同步試圖并發使用總線的事務。在一個處理器執行總線事務期間,總線會禁止其他的處理器和 I/O設備執行內存的讀/寫。
從 JDK5 開始),僅僅只允許把一個 64位 long/double型變量的寫操作拆分為兩個 32位的寫操作來執行,任意的讀操作在 JSR-133 中都必須具有原子性(即任意讀操作必須要在單個讀事務中執行)。
3.4 volatile 的內存語義
3.4.1 volatile 的特性
volatile變量自身具有下列特性。
? 可見性。對一個 volatile 變量的讀,總是能看到(任意線程)對這個 volatile變量最后的寫入。
? 原子性:對任意單個 volatile 變量的讀/寫具有原子性,但類似于 volatile++這種復合操作不具有原子性。
3.4.2 volatile 寫-讀建立的 happens-before 關系
3.4.3 volatile 寫-讀的內存語義
下面對 volatile寫和 volatile 讀的內存語義做個總結。
? 線程 A 寫一個 volatile 變量,實質上是線程 A 向接下來將要讀這個 volatile 變量的某個線程發出了(其對共享變量所做修改的)消息。
? 線程 B 讀一個 volatile 變量,實質上是線程 B 接收了之前某個線程發出的(在寫這個 volatile 變量之前對共享變量所做修改的)消息。
? 線程 A 寫一個 volatile 變量,隨后線程 B 讀這個 volatile變量,這個過程實質上是線程 A通過主內存向線程 B發送消息。
3.4.4 volatile內存語義的實現
volatile 內存語義是怎么實現的?
JMM針對編譯器制定了一些 volatile 重排序規則:
?當第二個操作是 volatile寫時,不管第一個操作是什么,都不能重排序。這個規
則確保 volatile寫之前的操作不會被編譯器重排序到 volatile寫之后。
? 當第一個操作是 volatile 讀時,不管第二個操作是什么,都不能重排序。這個規
則確保 volatile讀之后的操作不會被編譯器重排序到 volatile讀之前。
? 當第一個操作是 volatile 寫,第二個操作是 volatile讀時,不能重排序。
3.4.5 JSR-133 為什么要增強 volatile 的內存語義
為了提供一種比鎖更輕量級的線程之間通信的機制,JSR-133 專家組決定增強 volatile 的內存語義:嚴格限制編譯器和處理器對 volatile 變量與普通變量的重排序,確保 volatile 的寫-讀和鎖的釋放-獲取具有相同的內存語義。
3.5 鎖的內存語義
3.5.1 鎖的釋放-獲取建立的 happens-before關系
3.5.2 鎖的釋放和獲取的內存語義
對比鎖釋放-獲取的內存語義與 volatile寫-讀的內存語義可以看出:鎖釋放與 volatile寫有相同的內存語義;鎖獲取與 volatile讀有相同的內存語義。下面對鎖釋放和鎖獲取的內存語義做個總結。
? 線程 A 釋放一個鎖,實質上是線程 A 向接下來將要獲取這個鎖的某個線程發出了(線程 A 對共享變量所做修改的)消息。
? 線程 B 獲取一個鎖,實質上是線程 B 接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改的)消息。
? 線程 A 釋放鎖,隨后線程 B 獲取這個鎖,這個過程實質上是線程 A 通過主內存向線程 B 發送消息。
3.5.3 鎖內存語義的實現
ReentrantLock 的實現依賴于 Java 同步器框架 AbstractQueuedSynchronizer.
由于 AQS 是用一個整型的 volatile 變量來維護同步狀態,所以這個變量實際上 ReentrantLock 通過 volatile 來實現內存語義
ReentrantLock 分為公平鎖和非公平鎖,我們首先分析公平鎖。使用公平鎖時,加鎖方法 lock()調用軌跡如下。
加鎖方法首先讀 volatile變量 state
在使用公平鎖時,解鎖方法 unlock()調用軌跡如下。
根據 volatile的 happens-before 規則,釋放鎖的線程在寫 volatile 變量之前可見的共享變量,在獲取鎖的線程讀取同一個 volatile 變量后將立即變得對獲取鎖的線程可見。
非公平鎖的釋放和公平鎖完全一樣,
所以這里僅僅分析非公平鎖的獲取。使用非公平鎖時,加鎖方法 lock()調用軌跡如下。
CAS 如何同時具有 volatile 讀和 volatile 寫的內存語義?
首先,編譯器不會對 volatile 讀與其后面的任意內存操作重排序。同樣的,編譯器也不會對 volatile 寫與 volatile 寫前面的任意內存操作重排序。組合這兩個條件,意味著為了同時實現 volatile讀和 volatile 寫的內存語義,編譯器不能對 CAS與 CAS 前面和后面的任意內存操作重排序。
在 CAS 的源碼實現中,比如 X86 處理器,程序會根據處理器的類型來決定是否為其添加 cmpxchg(口訣:村民培訓成果) 指令添加 LOCK 前綴,多線程處理器則添加,單處理器則不添加;
這個 LOCK 前綴有什么用處呢?
在奔騰處理器出來之前的處理器中,帶有 lock 前綴的指令在執行期間會鎖住總線,使得其他處理器暫時無法通過總線訪問內存。 但是這會帶來昂貴的開銷,從 Pentium 4、Intel Xeon及 P6 處理器開始,Intel 使用 緩存鎖定(Cache Locking)來保證指令執行的原子性。緩存鎖定將大大降低 lock 前綴指令的執行開銷。
那么什么是緩存鎖定呢?
緩存鎖定是某個 CPU 對緩存數據進行更改時,會通知緩存了該數據的 CPU 拋棄緩存的數據或者從內存重新讀取。
總結 從本文對 ReentrantLock 的分析可以看出,鎖釋放-獲取的內存語義的實現至少有下面兩種方式。
1)利用 volatile 變量的寫-讀所具有的內存語義。
2)利用 CAS 所附帶的 volatile 讀和 volatile 寫的內存語義。
3.5.4 concurrent 包的實現
由于 Java 的 CAS 同時具有 volatile 讀和 volatile 寫的內存語義,因此 Java 線程之間的通信現在有了下面 4 種方式。
concurrent 包的源代碼實現是這樣的:
首先,聲明共享變量為 volatile。
然后,使用 CAS 的原子條件更新來實現線程之間的同步。
同時,配合以 volatile 的讀/寫和 CAS 所具有的 volatile 讀和寫的內存語義來實現線程之間的通信。
3.6 域的內存意義
3.6.1 final 域的重排序規則
總結
以上是生活随笔為你收集整理的Java 并发编程艺术 读书笔记的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 悟空问答 模板 html,WeCente
- 下一篇: ultrascale和arm区别_ZYN