1. 程式人生 > >Java 內存模型與線程

Java 內存模型與線程

使用 操作數 fin 模型 接收 cnblogs eas 分支 交互

when ? why ? how ? what ?

計算機的運行速度和它的存儲和通信子系統速度的差距太大,大量的時間都花費在磁盤I/O 、網絡通信或者數據庫訪問上。如何把處理器的運算能力“壓榨”出來?

技術分享圖片

如何充分利用計算機處理器? 因為絕大多數的運算任務都不可能只靠處理器“計算”就能完成,處理器至少要與內存交互,如讀取運算數據、存儲運算結果這個 I/O 操作是很難消除的。又因為存儲設備和處理器運算速度有幾個數量級差距,所以在內存和處理器之間加了個高速緩存(這樣處理器就無須等待緩慢的內存讀寫)。

每個處理器都有自己的高速緩存如何做到緩存的一致性?

如果處理器 A ,和處理器 B 都通過私有的高速緩存將處理後的數據存儲到主內存中,如果在主內存中這需要存儲的數據是同一個變量那應該以哪個數據為準呢? 所以出現了緩存一致協議。


JMM

Java內存模型(Java Memory Model,JMM)定義程序中各個變量的訪問規則即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。變量指的是實例字段、靜態字段和構成數組對象的元素(不包括局部變量與方法參數,後者是線程私有的)。

由於JVM運行程序的實體是線程,而每個線程創建時JVM都會為其創建一個工作內存,用戶存儲線程私有的數據。

Java 內存模型中規定所有變量都存儲在主內存中,主內存是共享內存區域,所有線程可以訪問,線程的工作內存保存了被改線程使用到變量的主內存副本拷貝,線程對變量的操作(讀取賦值等)必須在工作內存中進行,不能直接讀寫主內存中的變量。

技術分享圖片

需要註意的是,JMM與Java內存區域的劃分是不同的概念層次,更恰當說JMM描述的是一組規則,通過這組規則控制程序中各個變量在共享數據區域和私有數據區域的訪問方式,JMM是圍繞原子性,有序性、可見性展開。

8種操作來完成內存間交互操作

lock(鎖定):作用於主內存變量,把一個變量標示為一條線程獨占的狀態

unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定

read(讀取):作用於主內存的變量,把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用

load(載入):作用於工作內存的變量,把read操作從主存中得到的變量值放入工作內存的變量副本中

use(使用):作用於工作內存的變量,把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作

assign(賦值):作用於工作內存的變量,把一個從執行引擎接收到的值賦給工作內存中的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作

store(存儲):作用於工作內存的變量,把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用

write(寫入):作用於主內存的變量,把store操作從工作內存中得到的變量的值放入主內存的變量中

Java內存模型還規定了執行上述8種基本操作時必須滿足如下規則:

1、不允許read和load、store和write操作之一單獨出現,以上兩個操作必須按順序執行,但沒有保證必須連續執行,也就是說,read與load之間、store與write之間是可插入其他指令的。

2、不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存。

3、不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。

4、一個新的變量只能從主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。

5、一個變量在同一個時刻只允許一條線程對其執行lock操作,但lock操作可以被同一個條線程重復執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量才會被解鎖。

6、如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。

7、如果一個變量實現沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。

8、對一個變量執行unlock操作之前,必須先把此變量同步回主內存(執行store和write操作)。


原子性

在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷的,要麽執行,要麽不執行。

分析以下哪些操作是原子性操作:

x = 10;         //語句1
y = x;         //語句2
x++;           //語句3
x = x + 1;     //語句4

只有語句 1 是原子性操作。

語句 1 線程執行這個語句會直接將數據 10 寫入工作內存中。

語句 2 先要度讀取 x 的值,然後將 x 寫入工作內存,雖然這 2 個操作都是原子性操作,但是合起來就不是。

語句 3 和語句 4 讀取 x 的值,進行加 1 操作,寫入新的值。

只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。

不過這裏有一點需要註意:在32位平臺下,對64位數據的讀取和賦值是需要通過兩個操作來完成的,不能保證其原子性。但是好像在最新的JDK中,JVM已經保證對64位數據的讀取和賦值也是原子性操作了。

從上面可以看出,Java內存模型只保證了基本讀取和賦值是原子性操作,如果要實現更大範圍操作的原子性,可以通過synchronized和Lock來實現。由於synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那麽自然就不存在原子性問題了,從而保證了原子性。

可見性

可見性指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。Java提供了volatile關鍵字來保證可見性。volatile 保證了新值能立即同步到主內存,以及每次用前立即從主內存中刷新。 synchronized 和 final也能實現可見性。

有序性

在 Java 內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程並發執行的正確性。

Java 程序中天然的有序性可以總結為一句話:如果在本線程內觀察,所有的操作都是有序的,如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句是指“線程內表現為串行的語義”,後半句是指“指令重排序”現象和“工作內存與主內存同步延遲”現象。

在Java裏面,可以通過volatile關鍵字來保證一定的“有序性”(具體原理在下一節講述)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性。

Java內存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推導出來,那麽它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。

  1. 程序次序規則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生與書寫在後面的操作。準確地說,應該是控制流順序而不是程序代碼順序,因為要考慮分支、循環等結構。
  2. 管程鎖定規則(Monitor Lock Rule):一個 unlock操作發生於後面對同一個鎖的 lock 操作。這裏必須強調的是同一個鎖,而“後面”同樣是指時間上的先後順序。
  3. volatile 變量規則(Volatile Variable Rule):對一個 volatile 變量的寫操作先行發生於後面對這個變量的讀操作,這裏的“後面”同樣是指時間上的先後順序。
  4. 線程啟動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每個一個動作
  5. 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
  6. 線程終結規則(Thread Termination Rule):線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
  7. 對象終結規則(Finalizer Rule):一個對象的初始化完成先行發生於他的finalize()方法的開始
  8. 傳遞規則(Transitivity):如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C

volatile

  1. 保證可見性
  2. 不保證原子性
  3. 保證有序性(禁止指令重排優化)

volatile具有可見性

public static void main(String[] args) throws InterruptedException {
    ThreadOne threadOne=new ThreadOne();
    threadOne.start();
    Thread.sleep(1000);
    ThreadOne.stop=true;

}
}

class ThreadOne extends Thread{
public static volatile boolean stop=false;
@Override
public  void run(){
    while (stop==false){
        System.out.println(Thread.currentThread().getName()+" is running");
    }
}
}

結果:

技術分享圖片

如果將 volatile 關鍵字去掉會不會變成 死循環呢?

答案不會,因為鎖粗化

原則上我們在編寫代碼的時候,總是推薦同步塊的作用範圍限制得盡量小————只在共享數據的實際作域中才進行同步,這樣是為了使用需要同步的操作數量盡可能變小,如果存在鎖競爭,那等待鎖的線程也能盡快拿到鎖。

大部分情況下,上面的原則都是正確的,但是如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中,那即使沒有線程競爭,頻繁的進行互斥同步操作也會導致不必要的性能損耗。如果虛擬機探測到有這樣一串零碎的操作對同一個對象鎖,將會把鎖同步範圍擴展(粗化)到整個操作序列的外部。

–深入理解JVM,13章線程安全與鎖優化

下面是println方法的源碼,使用了synchronized塊
/**
 * Prints a String and then terminate the line.  This method behaves as
 * though it invokes <code>{@link #print(String)}</code> and then
 * <code>{@link #println()}</code>.
 *
 * @param x  The <code>String</code> to be printed.
 */
public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

會變成

synchronized(this){
while (stop==false){
}
}

synchronized規定,線程在加鎖時,先清空工作內存→在主內存中拷貝最新變量的副本到工作內存→執行完代碼→將更改後的共享變量的值刷新到主內存中→釋放互斥鎖。

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();
    }
    while (Thread.activeCount() > 2) {
        Thread.yield();
    }
    System.out.println(race);
}
}

書本上 Thread.activeCount() > 1 是有問題的,我這篇博客有解釋

運行結果:不是200000

volatile能保證有序性

volatile關鍵字能禁止指令重排序,所以volatile能在一定程度上保證有序性。

volatile關鍵字禁止指令重排序有兩層意思:

  1. 當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;

  2. 在進行指令優化時,不能將在對volatile變量訪問的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。

舉個例子:

//x、y為非volatile變量
//flag為volatile變量
 
x = 2;        //語句1
y = 0;        //語句2
flag = true;  //語句3
x = 4;         //語句4
y = -1;       //語句5

由於flag變量為volatile變量,那麽在進行指令重排序的過程的時候,不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5後面。但是要註意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。

並且volatile關鍵字能保證,執行到語句3時,語句1和語句2必定是執行完畢了的,且語句1和語句2的執行結果對語句3、語句4、語句5是可見的。


volatile使用場景

在某些情況下,volatile 的同步機制的性能確實要優於鎖。

  1. 運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值
  2. 變量不需要與其他狀態變量共同參與不變約束

使用場景1:

volatile boolean shutdownRequested;
public void shutdown(){
    shutdownRequested = true;
}
public void doWork(){
    while(!shutdownRequested){
        //do stuff
    }
}

使用場景2:指令重排序

Map configOptions;
char[] configText;
volatile boolean initialized = false;
// 線程1
//模擬讀取配置信息,當讀完後將 initialized 設置為 true 以後通知其它線程配置可用
configOptions = new HashMap();
configText = readConfigFile(filename);
processConfigOptions(configText,configOptions);
initialized = true;
// 線程2
while(!initialized){
   sleep();
}
//使用線程 1 中初始化好的配置信息
doSomethingWithConfig();

使用場景3:可以使用 volatile 關鍵字來保證多線程下的單例

public class Singleton{
private volatile static Singleton instance = null;
 
private Singleton() {
     
}
 
public static Singleton getInstance() {
    if(instance==null) {
        synchronized (Singleton.class) {
            if(instance==null)
                instance = new Singleton();
        }
    }
    return instance;
}
}

總結

參考

深入理解JVM

https://www.cnblogs.com/dolphin0520/p/3920373.html

主要講了Java內存模型,已經並發的原子性、可見性、有序性,以及 volatile關鍵字。

有什麽錯誤歡迎指出,十分感謝!

Java 內存模型與線程