1. 程式人生 > >線程安全—可見性和有序性

線程安全—可見性和有序性

[] AS eight ota pan 是個 http 並發執行 ice

什麽是java的內存模型?

共享變量:一個變量可以被多個線程使用,那麽這個變量就是這幾個線程的共享變量。 Java Memory Model (JAVA 內存模型)描述線程之間如何通過內存(memory)來進行交互,描述了java程序中各種變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取出變量這樣的底層細節。 具體說來, JVM中存在一個主存區(Main Memory或Java Heap Memory),對於所有線程進行共享,但線程不能直接操作主內存中的變量,每個線程都有自己獨立的工作內存(Working Memory),裏面保存該線程使用到的變量的副本( 主內存中該變量的一份拷貝 )
規定:線程對共享變量的讀寫都必須在自己的工作內存中進行,而不能直接在主內存中讀寫。不同線程不能直接訪問其他線程的工作內存中的變量,線程間變量值的傳遞需要主內存作為橋梁。 技術分享圖片 技術分享圖片 什麽是內存的可見性? 可見性:一個線程對共享變量值得修改,能夠及時的被其他線程看到 線程可見性原理:
線程一對共享變量的改變想要被線程二看見,就必須執行下面兩個步驟: ①將工作內存1中的共享變量的改變更新到主內存中 ②將主內存中最新的共享變量的變化更新到工作內存2中。 技術分享圖片 指令重排序:代碼書寫的順序與實際執行的順序不同,指令重排序是編譯器或處理器為了提高程序性能而做的優化。

1.編譯器優化的重排序(編譯器優化)

2.指令級並行重排序(處理器優化)

3.內存系統的重排序(處理器優化)

是不是所有的語句的執行順序都可以重排呢?

答案是否定的。為了講清楚這個問題,先講解另一個概念:數據依賴性

什麽是數據依賴性?

如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴。數據依賴分下列三種類型:

名稱 代碼示例 說明
寫後讀 a = 1;b = a; 寫一個變量之後,再讀這個位置。
寫後寫 a = 1;a = 2; 寫一個變量之後,再寫這個變量。
讀後寫 a = b;b = 1;
讀一個變量之後,再寫這個變量。

上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果將會被改變。所以,編譯器和處理器在重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序。也就是說:在單線程環境下,指令執行的最終效果應當與其在順序執行下的效果一致,否則這種優化便會失去意義。這句話有個專業術語叫做as-if-serial semantics (as-if-serial語義)

int num1=1;//第一行
int num2=2;//第二行
int sum=num1+num;//第三行

  • 單線程:第一行和第二行可以重排序,但第三行不行
  • 重排序不會給單線程帶來內存可見性問題
  • 多線程中程序交錯執行時,重排序可能會照成內存可見性問題。

可見性分析:

導致共享變量在線程間不可見的原因:

  1. 線程的交叉執行
  2. 重排序結合線程交叉執行
  3. 共享變量更新後的值沒有在工作內存與主內存間及時更新

重排序對多線程的影響 class ReorderExample { int a = 0; boolean flag = false; public void writer() { a = 1; // 1 flag = true; // 2 } public void reader() { if (flag) { // 3 int i = a * a; // 4 } } } flag變量是個標記,用來標識變量a是否已被寫入。這裏假設有兩個線程A和B,A首先執行writer()方法,隨後B線程接著執行reader()方法。線程B在執行操作4時,能否看到線程A在操作1對共享變量a的寫入?

答案是:不一定能看到。

由於操作1和操作2沒有數據依賴關系,編譯器和處理器可以對這兩個操作重排序;同樣,操作3和操作4沒有數據依賴關系,編譯器和處理器也可以對這兩個操作重排序。讓我們先來看看,當操作1和操作2重排序時,可能會產生什麽效果?

執行順序是:2 -> 3 -> 4 -> 1 (這是完全存在並且合理的一種順序,如果你不能理解,請先了解CPU是如何對多個線程進行時間分配的)

操作3和操作4重排序後,因為操作3和操作4存在控制依賴關系。當代碼中存在控制依賴性時,會影響指令序列執行的並行度。為此,編譯器和處理器會采用猜測(Speculation)執行來克服控制相關性對並行度的影響。以處理器的猜測執行為例,執行線程B的處理器可以提前讀取並計算a*a,然後把計算結果臨時保存到一個名為重排序緩沖(reorder buffer ROB)的硬件緩存中。當接下來操作3的條件判斷為真時,就把該計算結果寫入變量i中。

我們可以看出,猜測執行實質上對操作3和4做了重排序。重排序在這裏破壞了多線程程序的語義!

同步(synchronization)就是指一個線程訪問數據時,其它線程不得對同一個數據進行訪問,即同一時刻只能有一個線程訪問該數據,當這一線程訪問結束時其它線程才能對這它進行訪問。 技術分享圖片 技術分享圖片
package com.xidian.count;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

import com.xidian.annotations.ThreadSafe;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@ThreadSafe
public class CountExample3 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時並發執行的線程數
    public static int threadTotal = 200;

    public static int count = 0;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private synchronized static void add() {
        count++;
    }
}
View Code

volatile實現可見性

技術分享圖片 技術分享圖片 volatile變量每次被線程訪問時,都強迫從主內存中讀取該變量的值,而當變量發生變化的時候都會強迫線程將最新的值刷新到主內存中。 這樣不同的變量總能看到最新的值。

volatile關鍵字:

  • 能夠保證volatile變量的可見性
  • 不能保證volatile變量的原子性
深入來說:通過加入內存屏障和禁止重排序優化來實現的。
  • 對volatile變量執行寫操作時,會在寫操作後加入一條store屏障指令

    • store指令會在寫操作後把最新的值強制刷新到主內存中。同時還會禁止cpu對代碼進行重排序優化。這樣就保證了值在主內存中是最新的。
  • 對volatile變量執行讀操作時,會在讀操作前加入一條load屏障指令

    • load指令會在讀操作前把內存緩存中的值清空後,再從主內存中讀取最新的值。
技術分享圖片

技術分享圖片

技術分享圖片
package com.xidian.count;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

import com.xidian.annotations.NotThreadSafe;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@NotThreadSafe
public class CountExample4 {

    // 請求總數
    public static int clientTotal = 5000;

    // 同時並發執行的線程數
    public static int threadTotal = 200;

    public static volatile int count = 0;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        count++;
        // 1、count 從主存中取出count的值
        // 2、+1  在工作內存中執行+1操作
        // 3、count 將count的值寫回主存
        //及時將count用vilatile修飾,每次從主存中取到的都是最新的值,可是當多個線程同時取到最新的值,執行+1操作,當刷新到主存中的時候會覆蓋結果,從而丟失一些+1操作
    }
}
View Code 由程序運行結果可知,即使我們使用volatile修飾變量依然無法保證線程安全。 那是為什麽呢?

volatile實現共享變量內存可見性有一個條件,就是對共享變量的操作必須具有原子性。比如 num = 10; 這個操作具有原子性,但是 num++ 或者num--由3步組成,並不具有原子性,所以是不行的。

假如num=5,此時有線程A從主內存中獲取num的值,並執行++,但在還未見修改寫入主內存中,又有線程B取得num的值,對其進行++操作,造成丟失修改,明明執行了2次++,num的值卻只增加了1.

volatile不具有原子性,它不適用於計數的場景,那麽它適用於什麽場景呢? volatile使用條件:
  1. 對變量的寫入操作不依賴其當前值

    • 不滿足:number++、count=count*5
    • 滿足:boolean變量、記錄溫度變化的變量等
  2. 該變量沒有包含在具有其他變量的不變式中

    • 不滿足:不變式 low<up

綜上,volatile特別適合用來做線程標記量,如下圖

技術分享圖片

synchronized和volatile的比較;

  • synchronized鎖住的是變量和變量的操作,而volatile鎖住的只是變量,而且該變量的值不能依賴它本身的值,volatile算是一種輕量級的同步鎖
  • volatile不需要加鎖,比synchronized更加輕量級,不會阻塞線程。
  • 從內存可見性角度講,volatile讀相當於加鎖,volatilexie相當於解鎖。
  • synchronized既能保證可見性,又能保證原子性,而volatile只能保證可見性,無法保證原子性。
註:由於voaltile比synchronized更加輕量級,所以執行的效率肯定是比synchroized更高。在可以保證原子性操作時,可以盡量的選擇使用volatile。在其他不能保證其操作的原子性時,再去考慮使用synchronized。

有序性

技術分享圖片

Happens-before原則,先天有序性,即不需要任何額外的代碼控制即可保證有序性,java內存模型一個列出了八種Happens-before規則,如果兩個操作的次序不能從這八種規則中推倒出來,則不能保證有序性。

※程序次序規則:一個線程內,按照代碼執行,書寫在前面的操作先行發生於書寫在後面的操作。 ※鎖定規則:一個unLock操作先行發生於後面對同一個鎖的lock操作 ※volatile變量規則:對一個變量的寫操作先行發生於後面對這個變量的讀操作 ※傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C ※線程啟動原則:Thread對象的start()方法先行發生於此線程的每一個動作 ※線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生 ※線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()方法返回值手段檢測到線程已經終止執行 ※對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始

第一條規則要註意理解,這裏只是程序的運行結果看起來像是順序執行,雖然結果是一樣的,jvm會對沒有變量值依賴的操作進行重排序,這個規則只能保證單線程下執行的有序性,不能保證多線程下的有序性。

總結

技術分享圖片

線程安全—可見性和有序性