1. 程式人生 > 其它 >多執行緒的原子性,可見性,有序性

多執行緒的原子性,可見性,有序性

java記憶體模型定義了主存,工作記憶體等這些抽象概念,底層對應著cpu暫存器,快取,cpu指令優化等。

由此引出了 原子性,可見性,有序性

一、原子性

保證指令不會受到上下文切換的影響而產生指令交錯,鎖就是用來解決這個問題的

二、可見性

為了保證指令不會受cpu快取的影響

2.1 現象描述和解釋

先看一個例子

private final static Logger LOGGER = LoggerFactory.getLogger(Test8.class);

    static boolean flag=true;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (flag){
                }
            }
        });

        t1.start();
        Thread.sleep(1000);
        LOGGER.info("改變標記");
        flag=false;

    }

上邊的程式碼t1執行緒當flag=true時會一直迴圈,主執行緒1s後改變flag,按預想的t1應該會結束,實際上t1執行緒不會結束,

這就是可見性問題。

java記憶體模型中有主記憶體,每個執行緒都有自己的工作記憶體,當一個變數被頻繁讀取時,jit編譯器會將flag的值快取到工作記憶體中的快取記憶體中,後邊從快取中讀取。這樣當主執行緒修改了flag並更新到主記憶體後t1執行緒還是從快取記憶體獲取flag,所以一直不能停止

總結下來就是一個執行緒對主記憶體的資料進行了修改,但對另外一個執行緒不可見

2.2 解決辦法

(1) 給共享的變數加一個關鍵字volatile,表示容易變化的,這樣對這個執行緒的讀取就不會走快取,一直從工作記憶體獲取。

static volatile boolean flag=true;

(2) synchronized也可以解決可見性問題

獲取共享變數值的時候先加鎖,這樣也能保證可見性

public class Test8 {

    private final static Logger LOGGER = LoggerFactory.getLogger(Test8.class);

    final static Object lock = new Object();

    static boolean flag=true;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    synchronized (lock){
                        if(!flag){
                            break;
                        }
                    }
                }
            }
        });

        t1.start();

        Thread.sleep(1000);
        LOGGER.info("改變標記");
        synchronized (lock){
            flag=false;
        }
    }
}

這種解決方式下需要注意對 flag變數的所有操作都要放在synchronized塊中

2.3 簡單應用

兩階段終止模式可以使用執行緒的interrupt方法和打斷標記來實現,這種方式需要特殊處理InterruptedException

異常,在異常處理中重新設定打斷標記,否則就不能正常停止。

也可以使用volatile關鍵字來實現,這種方式就不需要特殊處理InterruptedException異常了

public class Test2 {

    public static void main(String[] args) throws InterruptedException {
        Monitor monitor = new Monitor();
        monitor.start();
        // 2秒後主執行緒中執行停止
        Thread.sleep(3000);
        monitor.stop();
    }

}
//建設器類,有一個執行緒一直在監控
class Monitor{
    static Logger logger = LoggerFactory.getLogger(Monitor.class);
    Thread t;
    //控制是否停止的標記
    private volatile boolean isStop=false;
    //開始監控的方法
    public void start(){
        t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    Thread current =Thread.currentThread();
					// 因為isStop被volatile修飾了,所以其他執行緒的修改可以感知到,
                      // 這樣就可以用來控制執行緒是否結束
                    if(isStop){
                        //被打斷了
                        logger.debug("要結束了,執行結束前的操作");
                        break;
                    }
                    try {
                        //每隔一秒執行一次監控邏輯
                        Thread.sleep(1000);
                        logger.debug("執行監控邏輯");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        //這種方式不需要在異常中進行特殊處理
                    }
                }
            }
        },"t1");
        t.start();
    }

    //停止監視器的方法
    public void stop(){
        isStop=true;
    }
}

2.4 volatile解決可見性問題的原理

volatile的原理是基於記憶體屏障,

在讀取被volatile修飾的變數時會在讀取指令之前加入讀屏障,

在寫入被volatile修飾的變數時會在寫指令之後加入寫屏障,

讀屏障會保證屏障之後對volatile變數的讀取都從主記憶體中讀,寫屏障會保證屏障之前對volatile變數的修改都會重新整理到主記憶體中。

三、有序性

保證指令不會受cpu指令並行優化(指令重排)的影響

2.1 問題描述

jit編譯器會在不影響最終結果的前提下調整指令的執行順序,在多執行緒環境下可能就會出現一些問題。

例如建立物件時,在java層面看到的是一句程式碼

User user = new User()

在位元組碼指令層面會對應著幾個步驟

(1)建立例項,(2)執行構造方法(3)暴露引用

而 2和3的順序是有可能被調整的,這樣在多執行緒環境下如果這個user是個共享變數,當前執行緒有可能先執行了3那麼其他執行緒就有可能拿到一個不完整的物件。

2.2 解決辦法

變數用volatile關鍵字修飾

2.3 volatile解析有序性問題原理

在讀取被volatile修飾的變數時會在讀取指令之前加入讀屏障,

在寫入被volatile修飾的變數時會在寫指令之後加入寫屏障,寫屏障會保證之前的指令不進行指令重排。

2.4 有序性應用 多執行緒單例模式

public class App {
    private App(){}
    //volatile關鍵字禁止指令重排
    private volatile static App app;

    public static App getInstance(){
        if(app == null) {
            //只有第一次建立物件時才會進入同步塊並加鎖
            synchronized (App.class){
                //防止多個執行緒同時進入了第一個if
                if(app == null){
                    app = new App();
                }
            }
        }
        return app;
    }
}