Java SE 多線程-內存模型

java內存模型,java memory model(JMM)

線程間的通訊機制,包含兩種方式:共享內存和消息傳遞java

共享內存:線程之間經過共享程序/進程內存中的公共狀態,從而進行通訊.例如共享對象.程序員

消息傳遞:經過明確的發送消息來進行顯示的通訊.例如java中的wait()和notify()緩存

*安全

線程間的同步


同步是指程序用於控制不一樣線程之間操做發生相對順序的機制.多線程

共享內存時,同步必須顯示進行.即程序員必須指定某段代碼在進程間互斥執行.架構

消息傳遞時,消息的發送必然在接收以前,所以同步是隱式的.併發

Java的併發採用的是共享內存模型ide


java內存模型


從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係;線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本.本地內存是JMM的一個抽象概念,並不真實存在.線程

線程A與線程B之間如要通訊的話,必需要經歷下面2個步驟:code

1.首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。

2.而後,線程B到主內存中去讀取線程A以前已更新過的共享變量。

*

JVM對內存模型的實現


JVM在內存中建立兩個區域,線程棧區和堆區

每一個線程擁有獨立的線程棧(也稱爲Stack,虛擬機棧,棧),其中存放着棧幀(也成爲Stack Frame,方法棧).線程每調用一個方法就對應一個棧幀入棧,方法執行完畢或者異常終止就意味着該棧幀出棧(銷燬).棧幀中存放當前方法的局部變量表(用於存放局布變量)和操做數棧(用於算數運算中操做數的入棧和出棧).

棧幀的內存大小在編譯時就已經肯定,不隨代碼執行而改變.可是線程棧能夠動態擴展,若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常(即內存溢出);若是虛擬機棧能夠動態擴展,若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常(即內存溢出)

只有位於棧頂的棧幀纔是當前執行棧幀,成爲當前棧幀,對應的方法成爲當前方法.

線程棧中的局部變量(具體來講是棧幀中的局部變量),對於其餘線程不可見.在傳遞時也只是傳遞對應的副本值.若是局部變量時基本類型,那麼直接存儲其值,若是是引用類型,那麼在棧(棧幀)中存儲其堆地址,其具體內容存放在堆中.

堆中的對象能夠被多線程所共享,只要對應線程有其地址便可.

*

硬件內存架構


CPU從寄存器中讀取操做數,寄存器從CPU緩存(可能有多級)中讀取數據,CPU緩存從內存/主存中讀取數據.

當CPU須要訪問內存時,實際過程是:內存將數據傳遞給CPU緩存,CPU緩存將數據傳遞給寄存器.當CPU須要寫入數據到內存時,也是按照這個逆向流程,可是,CPU緩存只會在特定的時間節點上將數據刷新到內存中.

因此會出現下面代碼中的問題:

public static void main(String\[\] args) throws InterruptedException { MyRunnable r = new MyRunnable(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
    new Thread(r).start(); 
} 
static class MyRunnable implements Runnable { 
    static int count = 0;
    
    @Override public void run() { 
        while (true) if (count < 100) { 
            System.out.println(Thread.currentThread() 
                    + ":" + count++); 
            } else{     
                System.out.println(Thread.currentThread() 
                    + " break :" + count); 
                break; 
            } 
        } 
}

部分執行結果:

Thread[Thread-3,5,main]:84
    Thread[Thread-10,5,main]:99
    Thread[Thread-5,5,main] break :100
    Thread[Thread-0,5,main] break :100
    Thread[Thread-1,5,main]:98
    Thread[Thread-1,5,main] break :100
    Thread[Thread-4,5,main]:97
    Thread[Thread-4,5,main] break :100
    Thread[Thread-2,5,main]:96
    Thread[Thread-9,5,main]:95
    Thread[Thread-6,5,main]:94
    Thread[Thread-9,5,main] break :100
    Thread[Thread-2,5,main] break :100

能夠看到,部分線程已經將count值加到100,後續仍有線程輸出的值不足100,緣由在於這部分線程在計算時,cpu是從cpu緩存中讀取的count備份,並且此備份並不是最新值.

執行時刻靠後的線程讀取到"舊值"的線程稱爲髒讀.

使用volatile關鍵字能夠保證沒有髒讀,即保證變量的可見性.每一個線程修改的結果能夠第一時間被其餘線程看到.由於cpu再也不從cpu緩存中讀取數據而是直接從主存中讀取.

可是仍然會存在線程不安全的問題,緣由在volatile僅保證數據可見性,可是不保證操做原子性(即一個操做或者多個操做要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行).

上述程序的第25行,實際執行過程當中須要讀取count值,控制檯輸出,count值加1,返回.volatile僅保證後執行的線程讀取到的值不會比先執行的線程讀的值更"舊".可是可能存在一種狀況:線程A讀取count值(假如是50)後,時間片丟失,線程B拿到時間片,讀取count值(也爲50)並完整執行該方法(count值爲51),而後線程A恢復執行,因爲已經讀取過count值因此再也不執行,執行結果與線程B相同(也爲51),從而仍有線程不安全.

爲了保證線程安全,仍須要使用鎖或者同步代碼塊,由於在解鎖以前,必然將相應的變量值刷新到內存中.

至於volatile,其更大的做用是防止指令重排序.

相關文章
相關標籤/搜索