java-多線程編程2-併發性問題-底層實現

硬件效率和一致性問題

因爲計算機的存儲設備與處理器的運算速度有幾個數量級的差距,因此現代計算機系統都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache)來做爲內存與處理器之間的緩衝:將運算須要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中,這樣處理器就無須等待緩慢的內存讀寫了。java

基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,可是也爲計算機系統帶來更高的複雜度,由於它引入了一個新的問題:緩存一致性(Cache Coherence)。在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存(MainMemory)緩存

1.jpg

主內存與工做內存

Java內存模型規定了全部的變量都存儲在主內存(Main Memory)中(此處的主內存與介紹物理硬件時的主內存名字同樣,二者也能夠互相類比,但此處僅是虛擬機內存的一部分)。每條線程還有本身的工做內存(Working Memory,可與前面講的處理器高速緩存類比),線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝 ,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量 。ide

2.png

主內存與工做內存之間具體的交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存之類的實現細節,Java內存模型中定義瞭如下8種操做來完成,虛擬機實現時必須保證下面說起的每一種操做都是原子的、不可再分的oop

  1. lock(鎖定):做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。
  2. unlock(解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
  3. read(讀取):做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用。
  4. load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
  5. use(使用):做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值的字節碼指令時將會執行這個操做。
  6. assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
  7. store(存儲):做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用。
  8. write(寫入):做用於主內存的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中。

volatile型變量的特殊規則

當一個變量定義爲volatile以後,它將具有兩種特性,第一是保證此變量對全部線程的可見性,這裏的「可見性」是指當一條線程修改了這個變量的值,新值對於其餘線程來講是能夠當即得知的。而普通變量不能作到這一點,普通變量的值在線程間傳遞均須要經過主內存來完成,例如,線程A修改一個普通變量的值,而後向主內存進行回寫,另一條線程B在線程A回寫完成了以後再從主內存進行讀取操做,新變量值纔會對線程B可見。測試

注意這個方法並不能保證原子性,只能保證可見性spa

package test;
/**
 * 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(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        //預期20×10000
        // 等待全部累加線程都結束
        while (Thread.activeCount() > 1)
            Thread.yield();
        System.out.println(race);
    }
}

預期輸出爲200000實際並非,由於java中運算操做並非原子操做,會有字節碼重排序的狀況發生操作系統

volatile對 long和double型變量的特殊規則線程

Java內存模型要求lock、unlock、read、load、assign、use、store、write這8個操做都具備原子性,可是對於64位的數據類型(long和double),在模型中特別定義了一條相對寬鬆的規定:容許虛擬機將沒有被volatile修飾的64位數據的讀寫操做劃分爲兩次32位的操做來進行,即容許虛擬機實現選擇能夠不保證64位數據類型的load、store、read和write這4個操做的原子性
若是有多個線程共享一個並未聲明爲volatile的long或double類型的變量,而且同時對它們進行讀取和修改操做,那麼某些線程可能會讀取到一個既非原值,也不是其餘線程修改值的表明了「半個變量」的數值3d

注意:目前各類平臺下的商用虛擬機幾乎都選擇把64位數據的讀寫操做做爲原子操做來對待,所以咱們在編寫代碼時通常不須要把用到的long和double變量專門聲明爲volatile

原子性、可見性與有序性

  • 原子性(Atomicity):由Java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store和write,咱們大體能夠認爲基本數據類型的訪問讀寫是具有原子性的(例外就是long和double的非原子性協定,讀者只要知道這件事情就能夠了,無須太過在乎這些幾乎不會發生的例外狀況)。
  • 若是應用場景須要一個更大範圍的原子性保證(常常會遇到),Java內存模型還提供了lock和unlock操做來知足這種需求,儘管虛擬機未把lock和unlock操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式地使用這兩個操做,這兩個字節碼指令反映到Java代碼中就是同步塊——synchronized關鍵字,所以在synchronized塊之間的操做也具有原子性。
  • 可見性(Visibility):可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。除了volatile以外,Java還有兩個關鍵字能實現可見性,即synchronized和final。
  • 有序性(Ordering):Java內存模型的有序性在前面講解volatile時也詳細地討論過了,Java程序中自然的有序性能夠總結爲一句話:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句是指「線程內表現爲串行的語義」(Within-Thread As-If-Serial Semantics),後半句是指「指令重排序」現象和「工做內存與主內存同步延遲」現象
結論:一個操做「時間上的先發生」不表明這個操做會是「先行發生「

Java與線程

對於Sun JDK來講,它的Windows版與Linux版都是使用一對一的線程模型實現的,一條Java線程就映射到一條輕量級進程之中,由於Windows和Linux系統提供的線程模型就是一對一的code

3.png

Java線程調度

線程調度是指系統爲線程分配處理器使用權的過程,主要調度方式有兩種,分別是協同式線程調度(CooperativeThreads-Scheduling)和搶佔式線程調度(PreemptiveThreads-Scheduling)

狀態轉換

4.png

  1. 新建(New):建立後還沒有啓動的線程處於這種狀態。
  2. 運行(Runable):Runable包括了操做系統線程狀態中的Running和Ready,也就是處於此狀態的線程有可能正在執行,也有可能正在等待着CPU爲它分配執行時間。
  3. 無限期等待(Waiting):處於這種狀態的線程不會被分配CPU執行時間,它們要等待被其餘線程顯式地喚醒。如下方法會讓線程陷入無限期的等待狀態:●沒有設置Timeout參數的Object.wait()方法。●沒有設置Timeout參數的Thread.join()方法。●LockSupport.park()方法。
  4. 限期等待(Timed Waiting):處於這種狀態的線程也不會被分配CPU執行時間,不過無須等待被其餘線程顯式地喚醒,在必定時間以後它們會由系統自動喚醒。如下方法會讓線程進入限期等待狀態:●Thread.sleep()方法。●設置了Timeout參數的Object.wait()方法。●設置了Timeout參數的Thread.join()方法。●LockSupport.parkNanos()方法。●LockSupport.parkUntil()方法。
  5. 阻塞(Blocked):線程被阻塞了,「阻塞狀態」與「等待狀態」的區別是:「阻塞狀態」在等待着獲取到一個排他鎖,這個事件將在另一個線程放棄這個鎖的時候發生;而「等待狀態」則是在等待一段時間,或者喚醒動做的發生。在程序等待進入同步區域的時候,線程將進入這種狀態。
  6. 結束(Terminated):已終止線程的線程狀態,線程已經結束執行。
相關文章
相關標籤/搜索