天天看點

Java多線程(三):volatile

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高。