Java多執行緒(四):volatile

Rest探路者發表於2019-07-04

volatile

volatile是一種輕量同步機制。請看例子
MyThread25類

public class MyThread25 extends Thread{
    private boolean isRunning = true;

    public boolean isRunning()
    {
        return isRunning;
    }

    public void setRunning(boolean isRunning)
    {
        this.isRunning = isRunning;
    }

    public void run()
    {
        System.out.println("進入run了");
        while (isRunning == true){}
        System.out.println("執行緒被停止了");
    }

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

        MyThread25 mt = new MyThread25();
        mt.start();
        Thread.sleep(1000);
        mt.setRunning(false);
        System.out.println("已設定為false");

    }
}

輸出結果如下

進入run了
已設定為false

為什麼程式始終不結束?說明mt.setRunning(false);沒有起作用。
這裡我們說下Java記憶體模型(JMM)
java虛擬機器有自己的記憶體模型(Java Memory Model,JMM),JMM可以遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓java程式在各種平臺下都能達到一致的記憶體訪問效果。
JMM定義了執行緒和主記憶體之間的抽象關係:共享變數儲存在主記憶體(Main Memory)中,每個執行緒都有一個私有的本地記憶體(Local Memory),本地記憶體儲存了被該執行緒使用到的主記憶體的副本,執行緒對變數的所有操作都必須在本地記憶體中進行,而不能直接讀寫主記憶體中的變數。這三者之間的互動關係如下
Java多執行緒(四):volatile

出現上述執行結果的原因是,主記憶體isRunning = true, mt.setRunning(false)設定主記憶體isRunning = false,本地記憶體中isRunning仍然是true,執行緒用的是本地記憶體,所以進入了死迴圈。

在isRunning前加上volatile
private volatile boolean isRunning = true;
輸出結果如下

進入run了
已設定為false
執行緒被停止了

volatile不能保證原子類執行緒安全

先看例子
MyThread26_0類,用volatile修飾num

public class MyThread26_0 extends Thread {
    public static volatile int num = 0;
    //使用CountDownLatch來等待計算執行緒執行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);

    @Override
    public void run() {
        for(int j=0;j<1000;j++){
            num++;//自加操作
        }
        countDownLatch.countDown();
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread26_0[] mt = new MyThread26_0[30];
        //開啟30個執行緒進行累加操作
        for(int i=0;i<mt.length;i++){
            mt[i] = new MyThread26_0();
        }
        for(int i=0;i<mt.length;i++){
            mt[i].start();
        }
        //等待計算執行緒執行完
        countDownLatch.await();
        System.out.println(num);
    }
}

輸出結果如下

25886

理論上,應該輸出30000。原子操作表示一段操作是不可分割的,因為num++不是原子操作,這樣會出現執行緒對過期的num進行自增,此時其他執行緒已經對num進行了自增。
num++分三步:讀取、加一、賦值。
結論:
volatile只會對單個的的變數讀寫具有原子性,像num++這種複合操作volatile是無法保證其原子性的
解決方法:
用原子類AtomicInteger的incrementAndGet方法自增

public class MyThread26_1 extends Thread {
    //使用原子操作類
    public static AtomicInteger num = new AtomicInteger(0);
    //使用CountDownLatch來等待計算執行緒執行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);

    @Override
    public void run() {
        for(int j=0;j<1000;j++){
            num.incrementAndGet();//原子性的num++,通過迴圈CAS方式
        }
        countDownLatch.countDown();
    }

    public static void main(String []args) throws InterruptedException {
        MyThread26_1[] mt = new MyThread26_1[30];
        //開啟30個執行緒進行累加操作
        for(int i=0;i<mt.length;i++){
            mt[i] = new MyThread26_1();
        }
        for(int i=0;i<mt.length;i++){
            mt[i].start();
        }
        //等待計算執行緒執行完
        countDownLatch.await();
        System.out.println(num);
    }
}

輸出結果如下

30000

原子類方法組合使用執行緒不安全

例子如下
ThreadDomain27類

public class ThreadDomain27 {
    public static AtomicInteger aiRef = new AtomicInteger();

    public void addNum()
    {
        System.out.println(Thread.currentThread().getName() + "加了100之後的結果:" + aiRef.addAndGet(100));
        aiRef.getAndAdd(1);
    }
}

MyThread27類

public class MyThread27 extends Thread{
    private ThreadDomain27 td;

    public MyThread27(ThreadDomain27 td)
    {
        this.td = td;
    }

    public void run()
    {
        td.addNum();
    }

    public static void main(String[] args)
    {
        try
        {
            ThreadDomain27 td = new ThreadDomain27();
            MyThread27[] mt = new MyThread27[5];
            for (int i = 0; i < mt.length; i++)
            {
                mt[i] = new MyThread27(td);
            }
            for (int i = 0; i < mt.length; i++)
            {
                mt[i].start();
            }
            Thread.sleep(1000);
            System.out.println(ThreadDomain27.aiRef.get());
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
}

輸出結果如下

Thread-2加了100之後的結果:100
Thread-3加了100之後的結果:200
Thread-0加了100之後的結果:302
Thread-1加了100之後的結果:403
Thread-4加了100之後的結果:504
505

理想的輸出結果是100,201,302...,因為addAndGet方法和getAndAdd方法構成的addNum不是原子操作。
解決該問題只需要在addNum加上synchronized關鍵字。
輸出結果如下

Thread-1加了100之後的結果:100
Thread-0加了100之後的結果:201
Thread-2加了100之後的結果:302
Thread-3加了100之後的結果:403
Thread-4加了100之後的結果:504
505

結論:
volatile解決的是變數在多個執行緒之間的可見性,但是無法保證原子性。
synchronized不僅保障了原子性外,也保障了可見性。

volatile和synchronized比較

先看例項,使用volatile是什麼效果
CountDownLatch保證10個執行緒都能執行完成,當然你也可以在System.out.println(test.inc);之前使用Thread.sleep(xxx)

public class MyThread28 {
    //使用CountDownLatch來等待計算執行緒執行完
    static CountDownLatch countDownLatch = new CountDownLatch(10);
    public volatile int inc = 0;
    public void increase() {
        inc++;
    }

    public static synchronized void main(String[] args) throws InterruptedException {
        final MyThread28 test = new MyThread28();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                    countDownLatch.countDown();

                }
            }.start();
        }
        countDownLatch.await();
        System.out.println(test.inc);
    }

}

執行結果如下

9677

每次執行結果都不一致。剛才我已經解釋過,這裡我再解釋一遍。
使用volatile修飾int型變數i,多個執行緒同時進行i++操作。比如有兩個執行緒A和B對volatile修飾的i進行i++操作,i的初始值是0,A執行緒執行i++時從本地記憶體剛讀取了i的值0(i++不是原子操作),就切換到B執行緒了,B執行緒從本地記憶體中讀取i的值也為0,然後就切換到A執行緒繼續執行i++操作,完成後i就為1了,接著切換到B執行緒,因為之前已經讀取過了,所以繼續執行i++操作,最後的結果i就為1了。同理可以解釋為什麼每次執行結果都是小於10000的數字。

解決方法:
使用synchronized關鍵字

public class MyThread28 {
    //使用CountDownLatch來等待計算執行緒執行完
    static CountDownLatch countDownLatch = new CountDownLatch(10);
    public int inc = 0;
    public synchronized void increase() {
        inc++;
    }

    public static synchronized void main(String[] args) throws InterruptedException {
        final MyThread28 test = new MyThread28();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                    countDownLatch.countDown();

                }
            }.start();
        }
        countDownLatch.await();
        System.out.println(test.inc);
    }

}

輸出結果如下

10000

synchronized不管是否是原子操作,它能保證同一時刻只有一個執行緒獲取鎖執行同步程式碼,會阻塞其他執行緒。
結論:
volatile只能用在變數,synchronized可以在變數、方法上使用。
volatile不會造成執行緒阻塞,synchronized會造成執行緒阻塞。
volatile效率比synchronized高。

相關文章