前言
本文已經收錄到我的 Github 個人博客,歡迎大佬們光臨寒舍:
學習導圖
一.為什么要學習內存模型與線程?
並發處理的廣泛應用是
Amdah1
定律代替摩爾定律成為計算機性能發展源動力的根本原因,也是人類壓制計算機運算能力的最有力武器
線程通信是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞。
線程同步是指程序用於控制不同線程之間操作發生相對順序的機制。
Java
的並發采用的是共享內存模型,Java
線程之間的通信總是隱式進行,整個通信過程對程序員完全透明。如果你想設計表現良好的並發程序,理解 Java
內存模型是非常重要的。Java
內存模型規定了如何和何時可以看到由其他線程修改過后的共享變量的值,以及在必須時如何同步的訪問共享變量。
二.核心知識點歸納
2.1 概述
Q1:多任務處理的必要性
- 充分利用計算機處理器的能力,避免處理器在磁盤
I/O
、網絡通信或數據庫訪問時總是處於等待其他資源的狀態 - 便於一個服務端同時對多個客戶端提供服務
通過指標
TPS
(Transactions Per Second
)可衡量一個服務性能的高低好壞,它表示每秒服務端平均能響應的請求總數,進而體現出程序的並發能力
Q2:硬件的效率與一致性
為了更好的理解
Java
內存模型,先理解物理計算機中的並發問題,兩者有很高的可比性
為了平衡內存交互速度與處理器的運算速度之間幾個數量級的差距,引入一層高速緩存(Cache
)來作為內存與處理器之間的緩沖:
- 將運算需要使用到的數據復制到緩存中,讓運算能快速進行
- 當運算結束后再從緩存同步回內存之中,而無須讓處理器等待緩慢的內存讀寫
- 出現問題:引入高速緩存雖解決了處理器與內存速度之間的矛盾,但是其引入了一個新的問題——緩存一致性
- 解決辦法:需要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操作
內存模型可以理解為:在特定的操作協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象
2.2 Java
內存模型
之前筆者在 進階之路 | 奇妙的Thread之旅中簡要介紹過
Java
內存模型,相信看過的讀者都有一些印象
2.2.1 設計目的
屏蔽掉各種硬件和操作系統的內存訪問差異,實現 Java
程序在各種平台下都能達到一致的內存訪問效果
2.2.2 設計方法
通過定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節
注意:這里的變量與
Java
中說的變量不同,而指的是實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數(其存放於局部變量表中,而局部變量表在JVM
棧中),因為后者是線程私有的,不會被共享,自然就不會存在競爭問題。
2.2.3 模型結構
- 主內存:所有變量的存儲位置。直接對應於物理硬件的內存
注意:這里的主內存、工作內存與 一文洞悉JVM內存管理機制 說的
Java
內存區域中的Java
堆、棧、方法區等並不是同一個層次的內存划分
- 工作內存:每條線程還有自己的工作內存,用於保存被該線程使用到的變量的主內存副本拷貝。為了獲取更好的運行速度,虛擬機可能會讓工作內存優先存儲於寄存器和高速緩存中
注意:
- 線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量
- 不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞必須通過主內存來完成
-
交互協議:用於規定一個變量如何從主內存拷貝到工作內存、如何從工作內存同步回主內存之類的實現細節。
共有
8
種操作:
- 作用於主內存變量:
- 鎖定
lock
:把變量標識為一條線程獨占的狀態- 解鎖
unlock
:把處於鎖定狀態的變量釋放出來- 寫入
write
:把store
操作從工作內存中得到的變量的值放入主內存的變量中- 讀取
read
:把變量的值從主內存傳輸到線程的工作內存中,以便隨后的load
動作使用
2.用於工作內存變量:
- 賦值
assign
:把從執行引擎接收到的值賦給工作內存的變量- 使用
use
:把工作內存中一個變量的值傳遞給執行引擎- 存儲
store
:把工作內存中變量的值傳送到主內存中,以便隨后的write
操作使用- 寫入
write
:把store
操作從工作內存中得到的變量的值放入主內存的變量中
結論:注意是順序非連續
- 如果要把變量從主內存復制到工作內存,那就要順序地執行
read
和load
- 如果要把變量從工作內存同步回主內存,就要順序地執行
store
和write
2.2.4 確保並發操作安全的原則
A1:執行八種基本操作的時候,必須滿足如下規則:
- 不允許
read
和load
、store
和write
操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現
可以簡單理解為不能拒絕別人給的東西
- 不允許一個線程丟棄它的最近的
assign
操作,即變量在工作內存中改變了之后必須把該變化同步回主內存 - 不允許一個線程無原因地,即沒有發生過任何
assign
操作,就把數據從線程的工作內存同步回主內存中 - 一個新的變量只能在主內存中『誕生』 ,不允許在工作內存中直接使用一個未被初始化(
load
或assign
)的變量,即對一個變量實施use
、store
操作之前必須先執行過了assign
和load
操作 - 如果對一個變量執行
lock
操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load
或assign
操作初始化變量的值
下文的
volatile
底層就是用到了lock
來實現可見性
- 如果一個變量事先沒有被
lock
操作鎖定,那就不允許對它執行unlock
操作,也不允許去unlock
一個被其他線程鎖定住的變量 - 對一個變量執行
unlock
操作之前,必須先把此變量同步回主內存中
可見這么多規則非常繁瑣,實踐也麻煩,下面再介紹一個等效判斷原則 -- 『先行發生原則』
A2:先行發生原則:
是 Java
內存模型中定義的兩項操作之間的偏序關系。
下面例舉一些 『天然的』先行發生關系,無須任何同步器協助就已經存在,可以在編碼中直接使用
- 程序次序規則:在一個線程內,按照控制流順序,書寫在前面的操作先行發生於書寫在后面的操作
- 管程鎖定規則:一個
unlock
操作先行發生於后面對同一個鎖的lock
操作volatile
變量規則:對一個volatile
變量的寫操作先行發生於后面對這個變量的讀操作- 線程啟動規則:
Thread
的start()
先行發生於此線程的每一個動作- 線程終止規則:線程中的所有操作都先行發生於對此線程的終止檢測。可通過
Thread.join()
結束、Thread.isAlive()
的返回值等手段檢測到線程已經終止執行- 線程中斷規則:對線程
interrupt()
的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。可通過Thread.isInterrupted()
檢測到是否有中斷發生- 對象終結規則:一個對象的初始化完成先行發生於它的
finalize()
的開始- 傳遞性:如果操作 A 先行發生於操作 B,操作 B 先行發生於操作 C,那么操作 A 一定先行發生於操作 C
2.2.5 保證原子性、可見性和有序性的措施
- 原子性:一個操作要么都執行要么都不執行
可直接保證的原子性變量操作有:
read
、load
、assign
、use
、store
和write
,因此可認為基本數據類型的訪問讀寫具備原子性的特征若需要保證更大范圍的原子性,可通過更高層次的字節碼指令
monitorenter
和monitorexit
來隱式地使用lock
和unlock
這兩個操作,反映到Java
代碼中就是同步代碼塊synchronized
關鍵字
- 可見性:當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改
- 通過在變量修改后將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存作為傳遞媒介的方式來實現
- 提供三個關鍵字保證可見性:
volatile
能保證新值能立即同步到主內存,且每次使用前立即從主內存刷新synchronized
對一個變量執行unlock
操作之前可以先把此變量同步回主內存中- 被
final
修飾的字段在構造器中一旦初始化完成且構造器沒有把this
的引用傳遞出去,就可以在其他線程中就能看見final
字段的值
- 有序性:程序代碼按照指令順序執行
- 如果在本線程內觀察,所有的操作都是有序的,指 “線程內表現為串行的語義”;
- 如果在一個線程中觀察另一個線程,所有的操作都是無序的,指 “指令重排序” 現象和 “工作內存與主內存同步延遲” 現象
- 提供兩個關鍵字保證有序性:
volatile
本身就包含了禁止指令重排序的語義synchronized
保證一個變量在同一個時刻只允許一條線程對其進行lock
操作,使得持有同一個鎖的兩個同步塊只能串行地進入
想詳細了解 volatile
的讀者,可以看下筆者之前寫的文章:進階之路 | 奇妙的 Thread 之旅
2.3 Java
與線程
2.3.1 線程實現的三種方式
1.使用內核線程
英文:
Kernel-Level Thread
,簡稱:KLT
- 定義:由操作系統內核支持的線程
- 原理:由內核來完成線程切換,內核通過操縱調度器(
Scheduler
)對線程進行調度,並負責將線程的任務映射到各個處理器上。每個內核線程可以視為內核的一個分身, 這樣操作系統就有能力同時處理多件事情 - 多線程內核:支持多線程的內核
- 輕量級進程(
Light Weight Process
,簡稱:LWP
):內核線程的一種高級接口
- 優點:每個輕量級進程都由一個內核線程支持,因此每個都成為一個獨立的調度單元,即使有一個輕量級進程在系統調用中阻塞,也不會影響整個進程繼續工作
- 缺點:
- 由於基於內核線程實現,所以各種線程操作(創建、析構及同步)都需要進行系統調用,代價相對較高,需要在用戶態和內核態中來回切換
- 一個系統支持輕量級進程的數量是有限的
- 一對一線程模型:輕量級進程與內核線程之間
1:1
的關系,如圖所示
2.使用用戶線程
英文:
User Thread
,簡稱:UT
- 定義:
- 廣義上認為一個線程不是內核線程就是用戶線程
- 狹義上認為用戶線程指的是完全建立在用戶空間的線程庫上,而系統內核不能感知線程存在的實現
- 優點:由於用戶線程的建立、同步、銷毀和調度完全在用戶態中完成,不需要內核的幫助,甚至可以不需要切換到內核態,所以操作非常快速且低消耗的,且可以支持規模更大的線程數量
- 缺點:由於沒有系統內核的支援,所有的線程操作都需要用戶程序自己處理,線程的創建、切換和調度都是需要考慮的問題,實現較復雜
- 一對多的線程模型進程:進程與用戶線程之間
1:N
的關系,如圖所示
3.混合
-
定義:既存在用戶線程,也存在輕量級進程
-
優點:
- 用戶線程完全建立在用戶空間中,因此用戶線程的創建、切換、析構等操作依然廉價,並且可以支持大規模的用戶線程並發
- 操作系統提供支持的輕量級進程作為用戶線程和內核線程之間的橋梁,可以使用內核提供的線程調度功能及處理器映射,且用戶線程的系統調用要通過輕量級線程來完成,大大降低了整個進程被完全阻塞的風險
- 多對多的線程模型:用戶線程與輕量級進程的數量比不定,即用戶線程與輕量級進程之間
N:M
的關系,如圖所示
Q:
Java
線程的實現是選擇哪一種呢?A:答案是不確定的。操作系統支持怎樣的線程模型,在很大程度上決定了
JVM
的線程是怎樣映射的。線程模型只對線程的並發規模和操作成本產生影響,而對Java
程序的編碼和運行過程來說,這些差異都是透明的。
2.3.2 線程調度的兩種方式
線程調度:指系統為線程分配處理器使用權的過程
1.協同式線程調度
- 由線程本身來控制線程的執行時間。線程把自己的工作執行完后,要主動通知系統切換到另外一個線程上
- 好處:
- 實現簡單
- 切換操作自己可知,不存在線程同步的問題
- 壞處:線程執行時間不可控,假如一個線程編寫有問題一直不告知系統進行線程切換,那么程序就會一直被阻塞
2.搶占式線程調度
- 由系統來分配每個線程的執行時間
- 好處:線程執行時間是系統可控的,不存在一個線程導致整個進程阻塞的問題
- 可以通過設置線程優先級,優先級越高的線程越容易被系統選擇執行
但是線程優先級並不是太靠譜,一方面因為
Java
的線程是通過映射到系統的原生線程上來實現的,所以線程調度最終還是取決於操作系統,在一些平台上不同的優先級實際會變得相同;另一方面優先級可能會被系統自行改變。
2.3.3 線程的六種狀態
在任意一個時間點,一個線程只能有且只有其中的一種狀態:
-
新建
New
:線程創建后尚未啟動 -
運行
Runable
:包括正在執行(Running
)和等待着 CPU 為它分配執行時間(Ready
)兩種 -
無限期等待
Waiting
:該線程不會被分配CPU
執行時間,要等待被其他線程顯式地喚醒。以下方法會讓線程陷入無限期等待狀態:
沒有設置
Timeout
參數的Object.wait()
沒有設置
Timeout
參數的Thread.join()
LockSupport.park()
(PS:想詳細了解它的可以看下這篇文章:Java 多線程學習(7)聊聊 LockSupport.park () 和 LockSupport.unpark ())
- 限期等待
Timed Waiting
:該線程不會被分配CPU
執行時間,但在一定時間后會被系統自動喚醒。以下方法會讓線程進入限期等待狀態:
Thread.sleep()
- 設置了
Timeout
參數的Object.wait()
- 設置了
Timeout
參數的Thread.join()
LockSupport.parkNanos()
LockSupport.parkUntil()
- 阻塞
Blocked
:線程被阻塞
注意區別阻塞和等待:
- 阻塞狀態:在等待獲取到一個排他鎖,在另外一個線程放棄這個鎖的時候發生;
- 等待狀態:在等待一段時間或者喚醒動作的發生,在程序等待進入同步區域的時候發生。
- 結束
Terminated
:線程已經結束執行
三.碎碎念
恭喜你!已經看完了前面的文章,相信你對
Java
內存模型與線程已經有一定深度的了解!你可以稍微放松獎勵自己一下,可以睡一個美美的覺,明天起來繼續沖沖沖!!!PS:原本《深入理解Java虛擬機》第3版中還提及了協程,但是我還沒學過協程的基本用法,這時候給大家講解感覺有點打腫臉充胖子的感覺 hhh,明天《第一行代碼-第三版》也要到了,待我看完《第一行代碼》再補充協程的內容吧 hhhh
如果文章對您有一點幫助的話,希望您能點一下贊,您的點贊,是我前進的動力
本文參考鏈接: