因爲計算機的處理器運算速度與它的存儲和通訊子系統速度的差距太大了,大量的時間都花費在磁盤 I/O、網絡通訊或者數據庫訪問上,致使處理器在大部分時間裏都處於等待其餘資源的狀態。所以,爲了充分利用計算機的處理器運算能力,現代計算機操做系統採用了多任務處理的方式,即讓計算機併發處理多個任務。java
對於計算量相同的任務,程序線程併發協調得越有條不紊,效率天然就會越高;反之,線程之間頻繁阻塞甚至死鎖,將會大大下降程序的併發能力。數據庫
因爲計算機的存儲設備與處理器的運算速度有幾個數量級的差距,因此現代計算機系統加入了一層讀寫速度儘量接近處理器運算速度的高速緩存來做爲內存與處理器之間的緩衝:將運算須要使用的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存中,這樣處理器就無須等待緩慢的內存讀寫了。數組
基於高速緩存的存儲交互解決了處理器與內存的速度矛盾,但也引入了一個新的問題:緩存一致性。緩存
在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存。當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致。爲了解決一致性的問題,須要各個處理器訪問緩存時遵循一些協議,在讀寫時根據協議來進行操做,好比 MSI、MESI 等協議。安全
處理器、高速緩存、主內存間的交互關係:網絡
除了增長高速緩存以外,爲了使處理器內部的運算單元能儘可能被充分利用,處理器可能會對輸入代碼進行亂序執行優化。處理器會在計算以後將亂序執行的結果重組,保證該結果與順序執行的結果一致。多線程
Java 內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量包括實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,由於後者是線程私有的,不會被共享,不存在競爭問題。併發
Java 內存模型規定了全部的變量都存儲在主內存中。每條線程還有本身的工做內存,線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝。函數
線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成。優化
從定義上來看,主內存主要對應於 Java 堆中的對象實例數據部分,而工做內存則對應於虛擬機棧中的部分區域。
從更低層次上說,主內存直接對應於物理硬件的內存,而爲了獲取更好的運行速度,虛擬機(甚至是硬件系統自己的優化措施)可能會讓工做內存優先存儲於寄存器和高速緩存中,由於程序運行時主要訪問讀寫的是工做內存。
關於主內存與工做內存之間的交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存之類的實現細節,Java 內存模型中定義了 8 種操做來完成,虛擬機必須保證每一種操做都是原子的、不可再分的。
volatile 的做用:
volatile 變量只能保證可見性,不能保證原子性。在如下運算場景中,仍然要經過加鎖(使用 synchronized 或 java.util.concurrent 中的原子類)來保證原子性:
對 volatile 變量的特殊規則:
Java 內存模型容許虛擬機將沒有被 volatile 修飾的 64 位數據(long 和 double)的讀寫操做劃分爲兩次 32 位的操做來進行,即容許虛擬機實現能夠不保證 64 位數據類型的 load、store、read 和 write 這 4 個操做的原子性,這就是所謂的 long 和 double 的非原子性協定。
目前各平臺下的商用虛擬機幾乎都把 64 位數據的讀寫操做實現爲具備原子性的操做,所以在編寫代碼時通常不須要把 long 和 double 變量專門聲明爲 volatile。
Java 內存模型是圍繞着在併發過程當中如何處理原子性、可見性和有序性這 3 個特徵來創建的。
可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。
Java 內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現可見性的。
volatile、synchronized、final 關鍵字都能實現可見性。
Java 程序中自然的有序性能夠總結爲一句話:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句是指「線程內表現爲串行的語義」,後半句是指「指令重排序」現象和「工做內存與主內存同步延遲」現象。
Java 語言提供了 volatile、synchronized 關鍵字來保證線程之間操做的有序性。
先行發生是 Java 內存模型中定義的兩項操做之間的偏序關係,若是說操做 A 先行發生於操做 B,其實就是說在發生操做 B 以前,操做 A 產生的影響能被操做 B 觀察到。「影響」包括修改了內存中共享變量的值、發送了消息、調用了方法等。它是判斷數據是否存在競爭、線程是否安全的主要依據。
Java內存模型的先行發生關係:
若是兩個操做之間的關係不知足以上規則,而且沒法從以上規則推導出來,那麼它們就沒有順序性保障,虛擬機能夠對它們隨意地進行重排序。
主流的操做系統都提供了線程實現,Java 語言則提供了在不一樣硬件和操做系統平臺下對線程操做的統一處理,每一個已經執行 start() 且還未結束的 java.lang.Thread 類的實例就表明了一個線程。
內核線程(Kernel-Level Thread,KLT)就是直接由操做系統內核(Kernel)支持的線程,這種線程由內核來完成線程切換,內核經過操縱調度器對線程進行調度,並負責將線程的任務映射到各個處理器上。
每一個內核線程能夠視爲內核的一個分身,這樣操做系統就有能力同時處理多件事情,支持多線程的內核就叫作多線程內核。
程序通常不會直接使用內核線程,而是使用內核線程的一種高級接口——輕量級進程(Light Weight Process,LWP),輕量級進程就是一般意義上所講的線程,每一個輕量級進程都由一個內核線程支持。這種輕量級進程與內核線程之間 1:1 的關係稱爲一對一的線程模型。
輕量級進程的侷限性:
用戶線程(User Thread,UT)徹底創建在用戶空間的線程庫上,系統內核不能感知線程的存在。用戶線程的創建、同步、銷燬和調度徹底在用戶態中完成,不須要內核的幫助。所以操做快速且低消耗,也能夠支持規模更大的線程數量。這種進程與用戶線程之間 1:N 的關係稱爲一對多的線程模型。
使用用戶線程的優點在於不須要系統內核支援,劣勢也在於沒有系統內核的支援,全部的線程操做都須要用戶程序本身處理。所以使用用戶線程實現的程序通常都比較複雜。
混合實現時,用戶線程仍是徹底創建在用戶空間中,而操做系統提供支持的輕量級進程則做爲用戶線程和內核線程之間的橋樑。在這種混合模式中,用戶線程與輕量級進程的數量比是不定的,即爲 N:M 的關係,這種就是多對多的線程模型。
混合實現的好處:
線程調度是指系統爲線程分配處理器使用權的過程,主要調度方式有兩種:協同式線程調度和搶佔式線程調度。
線程的執行時間由線程自己來控制,線程執行完以後,主動通知系統切換到另一個線程上。
協同式線程調度最大的好處是實現簡單,並且切換線程的操做對線程本身是可知的,因此沒有什麼線程同步的問題。它的壞處就是線程執行時間不可控,若是一個線程編寫有問題,一直不告知系統進行線程切換,那麼程序就會一直阻塞在那裏。
每一個線程由系統來分配執行時間,線程的切換不禁線程自己來決定。
使用搶佔式線程調度時,線程的執行時間是系統可控的,不會有一個線程致使整個進程阻塞的問題。
Java 使用的線程調度方式就是搶佔式調度。