絕大多數的運算任務都不可能只靠處理器「計算」就能完成,處理器至少要與內存交互,如讀取運算數據、存儲運算結果等。因爲計算機的存儲設備與處理器的運算速度有接數量級的差距,因此現代計算機系統都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache)來做爲內存與處理器之間的緩衝:將運算須要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存中,這樣處理器就無需等待緩慢的內存讀寫了。java
基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但它引入了一個新的問題:緩存一致性(Cache Coherence)。在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存(Main Memory),當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致的問題。爲了解決這個問題,須要遵循一些協議,如MSI、MESI、MOSI、Synapse、Firefly及Dragon Protocol等。數據庫
除了增長高速緩存外,爲了使得處理器內部的運算單元能儘可能被充分利用,處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在計算以後將亂序執行的結果重組,保證該結果與順序執行的結果是一致的。與此相似的是Java虛擬機中的指令重排序(Instruction Reorder)編程
Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。數組
Java內存模型的主要目標是定義程序在虛擬機中將各個變量存儲到內存和從內存中取出變量的底層訪問規則。此處的變量(Variables)與Java編程中所說的變量有所區別,它包括了實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,由於後者是線程私有的,不會存在競爭問題。爲了得到較好的執行效能,Java內存模型並無限制執行引擎使用處理器的特定寄存器或緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。緩存
Java內存模型規定了全部的變量都存儲在主內存(Main Memory)中(此處的主內存與介紹物理硬件時的主內存名字同樣,二者也能夠互相類比,但此處僅是虛擬機內存的一部分)。每條線程還有本身的工做內存(Working Memory,可與前面講的處理器高速緩存類比),線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。安全
這裏所講的主內存、工做內存與Java內存區域中的Java堆、棧、方法區等並非同一個層次的內存劃分,這二者基本上是沒有關係的,若是二者必定要勉強對應起來,那從變量、主內存、工做內存的定義來看,主內存主要對應於Java堆中的對象實例數據部分,而工做內存則對應於虛擬機棧中的部分區域。 從更低層次上說,主內存就直接對應於物理硬件的內存,而爲了獲取更好的運行速度,虛擬機(甚至是硬件系統自己的優化措施)可能會讓工做內存優先存儲於寄存器和高速緩存中,由於程序運行時主要訪問讀寫的是工做內存。多線程
Java內存模型中定義瞭如下8種操做來完成主內存與工做內存之間具體的交互協議,虛擬機實現時必須保證下面說起的每一種操做都是原子的、不可再分的(對於double和long類型的變量來講,load、store、read和write操做在某些平臺上容許有例外)併發
若是要把一個變量從主內存複製到工做內存,那就要順序地執行read和load操做,若是要把變量從工做內存同步回主內存,就要順序地執行store和write操做。注意,Java內存模型只要求上述兩個操做必須按順序執行,而沒有保證是連續執行。也就是說,read與load之間、store與write之間是可插入其餘指令的。除此以外,Java內存模型還規定了在執行上述8種基本操做時必須知足以下規則:app
關鍵字volatile能夠說時Java虛擬機提供的最輕量級的同步機制,當一個變量定義爲volatile以後,它將具有兩種特性:函數
1.保證此變量對全部線程的可見性,這裏的「可見性」是指當一條線程修改了這個值,新值對於其餘線程來講是能夠當即得知的
volatile變量在各個線程的工做內存中不存在一致性問題(在各個線程的工做內存中,volatile變量也能夠存在不一致的狀況,可是因爲每次使用以前都會先刷新,執行引擎看不到不一致的狀況,所以能夠認爲不存在一致性問題),可是基於volatile變量的運算在併發下同樣是不安全的。
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
increase();
}
});
threads[i].start();
}
// 等待全部累加線程都結束
while (Thread.activeCount()>1) {
Thread.yield();
}
System.out.println(race);
}
}
複製代碼
運行結果以下:
這段代碼啓動了20個線程,每一個線程進行10000次自增操做,若是正確併發的話應該是輸出200000,可是發現每次運行結果都是小於200000的值,這是爲何呢?
問題就出如今自增運算race++不是原子操做,會產生4條指令getstatic、iconst_一、iadd、putstatic,當getstatic指令把race的值取到操做棧頂時,volatile關鍵字只保證了此時的值是正確的,在執行++操做的時候,其餘線程可能把race的值加大了,而在操做棧頂的值就變成了過時的數據,所以就可能把較小的值同步回主內存中。
因爲volatile變量只能保證可見性,在不符合如下兩條規則的運算場景中,咱們仍然要經過加鎖(使用synchronized或java.util.concurrent中的原子類)來保證原子性:
如如下場景,調用shutdown能當即中止工做。
volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = false
}
public void dowork() {
while(!shutdownRequested){
// do something
}
}
複製代碼
2.使用volatile變量的第二個語義是禁止指令重排序優化,普通的變量僅僅會保證在該方法的執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操做的順序與程序代碼中的執行順序一致。由於在一個線程的方法執行過程當中沒法感知到這點,這也就是Java內存模型中描述的所謂的「線程內表現爲串行的語義」(Within-Thread As-If-Serial Semantics)
Java內存模型中對volatile變量定義的特殊規則。假定T表示一個線程,V和W分別表示兩個volatile型變量,那麼在進行read、load、use、assign、store和write操做時須要知足以下規則:
由Java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store和write,咱們大體能夠認爲基本數據類型的訪問讀寫是具有原子性的。
若是應用場景須要一個更大範圍的原子性保證,Java內存模型還提供了lock和unlock操做來知足這種需求,儘管虛擬機未把lock與unlock操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱匿地使用這兩個操做,這兩個字節碼指令反映到Java代碼中就是同步塊—synchronized關鍵字,所以在synchronized塊之間的操做也具有原子性。
可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現可見性的,不管是普通變量仍是volatile變量都是如此,普通變量與volatile變量的區別是,volatile的特殊規則保證了新值能當即同步到主內存,以及每次使用前當即從主內存刷新。所以,能夠說volatile保證了多線程操做時變量的可見性,而普通變量則不能保證這一點。
除了volatile以外,Java還有兩個關鍵字能實現可見性,即synchronized和final。同步塊的可見性是由「對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write操做)」這條規則得到的,而被final修飾的字段在構造器中一旦初始化完成,而且構造器沒有把this的引用傳遞出去(this引用逃逸是一件很危險的事情,其餘線程有可能經過這個引用訪問到「初始化了一半」的對象),那在其餘線程中就能看見final字段的值。
Java程序中自然的有序性能夠總結爲一句話:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句是指「線程內表現爲串行的語義」(Within-Thread As-If-Serial Semantics),後半句是指「指令重排序」現象和「工做內存與主內存同步延遲」現象。
Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排序的語義,而synchronized則是由「一個變量在同一個時刻只容許一條線程對其進行lock操做」這條規則得到的,這條規則決定了持有同一個鎖的兩個同步塊只能串行地進入。
先行發生(happens-before)的原則是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,咱們能夠經過幾條規則解決併發環境下兩個操做之間是否可能存在衝突的全部問題。
先行發生是Java內存模型中定義的兩項操做之間的偏序關係,若是說操做A先行發生於操做B,其實就是說在發生操做B以前,操做A產生的影響能被操做B觀察到,「影響」包括修改了內存中共享變量的值、發送了消息、調用了方法等。
Java內存模型下一些「自然的」先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,能夠在編碼中直接使用。若是兩個操做之間的關係不在此列,而且沒法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機能夠對它們隨意地進行重排序:
線程是比進程更輕量級的調度執行單位,線程的引入,能夠把一個進程的資源分配和執行調度分開,各個線程既能夠共享進程資源(內存地址、文件I/O等),又能夠獨立調度(線程是CPU調度的基本單位)
系統平臺實現線程主要有3種方式:使用內核線程實現、使用用戶線程實現和使用用戶線程加輕量級進程混合實現。
內核線程(Kernel-Level Thread,KLT)就是直接由操做系統內核支持的線程,這種線程由內核來完成線程切換,內核經過操縱調度器(Scheduler)對線程進行調度,並負責將線程的任務映射到各個處理器上。每一個內核線程能夠視爲內核的一個分身,這樣操做系統就有能力同時處理多件事情,支持多線程的內核就叫作多線程內核(Multi-Threads Kernel)。
程序通常不會直接去使用內核線程,而是去使用內核線程的一種高級接口——輕量級進程(Light Weight Process,LWP),輕量級進程就是咱們一般意義上所講的線程,因爲每一個輕量級進程都由一個內核線程支持,所以只有先支持內核線程,纔能有輕量級進程。這種輕量級進程與內核線程之間1比1的關係稱爲一對一的線程模型,以下圖所示:
因爲內核線程的支持,每一個輕量級進程都成爲一個獨立的調度單元,即便有一個輕量級進程在系統調用中阻塞了,也不會影響整個進程繼續工做,可是輕量級進程具備它的侷限性:首先,因爲是基於內核線程實現的,因此各類線程操做,如建立、析構及同步,都須要進行系統調用。而系統調用的代價相對較高,須要在用戶態(User Mode)和內核態(Kernel Mode)中來回切換。其次,每一個輕量級進程都須要有一個內核線程的支持,所以輕量級進程要消耗必定的內核資源(如內核線程的棧空間),所以一個系統支持輕量級進程的數量是有限的。
從廣義上講,一個線程只要不是內核線程,就能夠認爲是用戶線程(User Thread,UT)。從狹義上的用戶線程指的是徹底創建在用戶空間的線程庫上,系統內核不能感知線程存在的實現。 用戶線程的創建、同步、銷燬和調度徹底在用戶態中完成,不須要內核的幫助。若是程序實現得當,這種線程不須要切換到內核態,所以操做能夠是很是快速且低消耗的,也能夠支持規模更大的線程數量,部分高性能數據庫中的多線程就是由用戶線程實現的。這種進程與用戶線程之間1比N的關係稱爲一對多的線程模型,以下圖所示:
使用用戶線程的優點在於不須要系統內核支援,劣勢也在於沒有系統內核的支援,全部的線程操做都須要用戶程序本身處理。於是使用用戶線程實現的程序通常都比較複雜,如今使用用戶線程的程序愈來愈少了,Java、Ruby等語言都曾經使用過用戶線程,最終又都放棄使用它。
這種混合實現下,既存在用戶進程也存在輕量級進程。用戶線程的建立、切換、析構等操做依然廉價,而且能夠支持大規模的用戶線程併發。而用戶線程的系統調用經過輕量級線程來完成,大大下降了整個進程被徹底阻塞的風險。這種混合模式中,用戶線程與輕量級進程的數量爲N:M的關係
虛擬機規範中並未限定Java線程須要使用哪一種線程模型來實現,線程模型只對線程的併發規模和操做成本產生影響,對Java程序的編碼和運行過程來講,這些差別都是透明的。對於Sun JDK來講,它的Windows版與Linux版都是使用一對一的線程模型實現的,一條Java線程就映射到一條輕量級進程之中,由於Windows和Linux系統提供的線程模型就是一對一的。
線程調度是指系統爲線程分配處理器使用權的過程,主要調度方式有兩種,分別是協同式線程調度(Cooperative Threads-Scheduling)和搶佔式線程調度(Preemptive Threads-Scheduling)。
若是使用協同式調度的多線程系統,線程的執行時間由線程自己來控制,線程把本身的工做執行完了以後,要主動通知系統切換到另一個線程上。協同式多線程的最大好處是實現簡單,並且因爲線程要把本身的事情幹完後纔會進行線程切換,切換操做對線程本身是可知的,因此沒有什麼線程同步的問題。它的壞處也很明顯:線程執行時間不可控制,甚至若是一個線程編寫有問題,一直不告知系統進行線程切換,那麼程序就會一直阻塞在那裏。
若是使用搶佔式調度的多線程系統,那麼每一個線程將由系統來分配執行時間,線程的切換不禁線程自己來決定(在Java中,Thread.yield()可讓出執行時間,可是要獲取執行時間的話,線程自己是沒有什麼辦法的)。在這種實現線程調度的方式下,線程的執行時間是系統可控的,也不會有一個線程致使整個進程阻塞的問題,Java使用的線程調度方式就是搶佔式調度。
雖然Java線程調度是系統自動完成的,可是能夠設置優先級,Java語言一共設置了10個級別的線程優先級(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)。Java的線程是經過映射到系統的原生線程上來實現的,因此線程調度最終仍是取決於操做系統,雖然如今不少操做系統都提供線程優先級的概念,可是並不見得能與Java線程的優先級一一對應。
Java語言定義了5種線程狀態,在任意一個時間點,一個線程只能有且只有其中的一種狀態,這5種狀態分別以下。