本文已經收錄到個人 Github 我的博客,歡迎大佬們光臨寒舍:git
個人 GIthub 博客程序員
併發處理的普遍應用是
Amdah1
定律代替摩爾定律成爲計算機性能發展源動力的根本緣由,也是人類壓制計算機運算能力的最有力武器github
線程通訊是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通訊機制有兩種:共享內存和消息傳遞。數據庫
線程同步是指程序用於控制不一樣線程之間操做發生相對順序的機制。編程
Java
的併發採用的是共享內存模型,Java
線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明。若是你想設計表現良好的併發程序,理解 Java
內存模型是很是重要的。Java
內存模型規定了如何和什麼時候能夠看到由其餘線程修改事後的共享變量的值,以及在必須時如何同步的訪問共享變量。數組
Q1:多任務處理的必要性緩存
I/O
、網絡通訊或數據庫訪問時老是處於等待其餘資源的狀態經過指標
TPS
(Transactions Per Second
)可衡量一個服務性能的高低好壞,它表示每秒服務端平均能響應的請求總數,進而體現出程序的併發能力安全
Q2:硬件的效率與一致性網絡
爲了更好的理解
Java
內存模型,先理解物理計算機中的併發問題,二者有很高的可比性多線程
爲了平衡內存交互速度與處理器的運算速度之間幾個數量級的差距,引入一層高速緩存(Cache
)來做爲內存與處理器之間的緩衝:
- 出現問題:引入高速緩存雖解決了處理器與內存速度之間的矛盾,可是其引入了一個新的問題——緩存一致性
- 解決辦法:須要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操做
內存模型能夠理解爲:在特定的操做協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象
Java
內存模型以前筆者在 進階之路 | 奇妙的Thread之旅中簡要介紹過
Java
內存模型,相信看過的讀者都有一些印象
屏蔽掉各類硬件和操做系統的內存訪問差別,實現 Java
程序在各類平臺下都能達到一致的內存訪問效果
經過定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節
注意:這裏的變量與
Java
中說的變量不一樣,而指的是實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數(其存放於局部變量表中,而局部變量表在JVM
棧中),由於後者是線程私有的,不會被共享,天然就不會存在競爭問題。
注意:這裏的主內存、工做內存與 一文洞悉JVM內存管理機制 說的
Java
內存區域中的Java
堆、棧、方法區等並非同一個層次的內存劃分
注意:
- 線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存中的變量
- 不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞必須經過主內存來完成
交互協議:用於規定一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存之類的實現細節。
共有 8
種操做:
- 鎖定
lock
:把變量標識爲一條線程獨佔的狀態- 解鎖
unlock
:把處於鎖定狀態的變量釋放出來- 寫入
write
:把store
操做從工做內存中獲得的變量的值放入主內存的變量中- 讀取
read
:把變量的值從主內存傳輸到線程的工做內存中,以便隨後的load
動做使用
2.用於工做內存變量:
- 賦值
assign
:把從執行引擎接收到的值賦給工做內存的變量- 使用
use
:把工做內存中一個變量的值傳遞給執行引擎- 存儲
store
:把工做內存中變量的值傳送到主內存中,以便隨後的write
操做使用- 寫入
write
:把store
操做從工做內存中獲得的變量的值放入主內存的變量中
結論:注意是順序非連續
read
和 load
store
和 write
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
可直接保證的原子性變量操做有:
read
、load
、assign
、use
、store
和write
,所以可認爲基本數據類型的訪問讀寫具有原子性的特徵若須要保證更大範圍的原子性,可經過更高層次的字節碼指令
monitorenter
和monitorexit
來隱式地使用lock
和unlock
這兩個操做,反映到Java
代碼中就是同步代碼塊synchronized
關鍵字
- 經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現
- 提供三個關鍵字保證可見性:
volatile
能保證新值能當即同步到主內存,且每次使用前當即從主內存刷新synchronized
對一個變量執行unlock
操做以前能夠先把此變量同步回主內存中- 被
final
修飾的字段在構造器中一旦初始化完成且構造器沒有把this
的引用傳遞出去,就能夠在其餘線程中就能看見final
字段的值
- 若是在本線程內觀察,全部的操做都是有序的,指 「線程內表現爲串行的語義」;
- 若是在一個線程中觀察另外一個線程,全部的操做都是無序的,指 「指令重排序」 現象和 「工做內存與主內存同步延遲」 現象
- 提供兩個關鍵字保證有序性:
volatile
自己就包含了禁止指令重排序的語義synchronized
保證一個變量在同一個時刻只容許一條線程對其進行lock
操做,使得持有同一個鎖的兩個同步塊只能串行地進入
想詳細瞭解 volatile
的讀者,能夠看下筆者以前寫的文章:進階之路 | 奇妙的 Thread 之旅
Java
與線程英文:
Kernel-Level Thread
,簡稱:KLT
Scheduler
)對線程進行調度,並負責將線程的任務映射到各個處理器上。每一個內核線程能夠視爲內核的一個分身, 這樣操做系統就有能力同時處理多件事情Light Weight Process
,簡稱:LWP
):內核線程的一種高級接口
- 優勢:每一個輕量級進程都由一個內核線程支持,所以每一個都成爲一個獨立的調度單元,即便有一個輕量級進程在系統調用中阻塞,也不會影響整個進程繼續工做
- 缺點:
- 因爲基於內核線程實現,因此各類線程操做(建立、析構及同步)都須要進行系統調用,代價相對較高,須要在用戶態和內核態中來回切換
- 一個系統支持輕量級進程的數量是有限的
- 一對一線程模型:輕量級進程與內核線程之間
1:1
的關係,如圖所示
英文:
User Thread
,簡稱:UT
- 廣義上認爲一個線程不是內核線程就是用戶線程
- 狹義上認爲用戶線程指的是徹底創建在用戶空間的線程庫上,而系統內核不能感知線程存在的實現
1:N
的關係,如圖所示定義:既存在用戶線程,也存在輕量級進程
優勢:
- 用戶線程徹底創建在用戶空間中,所以用戶線程的建立、切換、析構等操做依然廉價,而且能夠支持大規模的用戶線程併發
- 操做系統提供支持的輕量級進程做爲用戶線程和內核線程之間的橋樑,可使用內核提供的線程調度功能及處理器映射,且用戶線程的系統調用要經過輕量級線程來完成,大大下降了整個進程被徹底阻塞的風險
N:M
的關係,如圖所示Q:
Java
線程的實現是選擇哪種呢?A:答案是不肯定的。操做系統支持怎樣的線程模型,在很大程度上決定了
JVM
的線程是怎樣映射的。線程模型只對線程的併發規模和操做成本產生影響,而對Java
程序的編碼和運行過程來講,這些差別都是透明的。
線程調度:指系統爲線程分配處理器使用權的過程
- 實現簡單
- 切換操做本身可知,不存在線程同步的問題
可是線程優先級並非太靠譜,一方面由於
Java
的線程是經過映射到系統的原生線程上來實現的,因此線程調度最終仍是取決於操做系統,在一些平臺上不一樣的優先級實際會變得相同;另外一方面優先級可能會被系統自行改變。
在任意一個時間點,一個線程只能有且只有其中的一種狀態:
新建 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
若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力
本文參考連接: