1. 程式人生 > >JUC之volatile關鍵字詳解

JUC之volatile關鍵字詳解

一、JUC簡介
  •     在Java5.0提供了java.util.concurrent(簡稱JUC包),在此包中增加了在併發程式設計中很常用的工具類,在用於定義類似於執行緒的自定義子系統,包括執行緒池,非同步IO和輕量級任務框架;還提供了設計用於多執行緒上下文中的Collection實現等。
二、volatile關鍵字
  • volatile關鍵字:當多個執行緒進行共享資料時,可以保證記憶體中的資料時可見的;相比較於syschronized是一種較為輕量級的同步策略。
  • volatile不具備“互斥性”。
  • volatile不能保證變數的“原子性”。
    下面舉一個列子:
package com.itszt;

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

        MyThread myThread = new MyThread();
        System.out.println("開啟子執行緒");
        myThread.start();
        System.out.println("子執行緒開啟完畢");
        Thread.sleep(2000);
        System.out.println("在主執行緒中終止子執行緒");
        myThread.setCanRun(false);
        System.out.println("main裡面的----"+myThread.canRun);

    }

    public static class MyThread extends Thread{

        private  boolean canRun = true;

        public boolean isCanRun() {
            return canRun;
        }

        public void setCanRun(boolean canRun) {
            this.canRun = canRun;
        }

        @Override
        public void run() {
            super.run();
            while (canRun) {


            }
            System.out.println("子執行緒裡面的:"+canRun);
            System.out.println("子執行緒終止。。。。");

        }
    }
}
執行結果:

上面例子中,我們的main方法的主執行緒與子執行緒MyThread同時對變數canRun變數進行操作,canRun就是多執行緒的共享資料,通過執行結果可以看出,主執行緒更改了canRun的值為false,按理說根據迴圈條件,子執行緒應該會終止,但是結果並沒有終止,而是仍然處在死迴圈中,可見子執行緒中的canRun並沒有被更改,仍然是true。

為什麼會出現這樣的問題呢?我們明明已經更改了變數的值啊?下面,我們來看一下這個變數的值是如何的一個存在,和兩個執行緒是如何取值的。


可以看出,多個執行緒在共享這個資料時,每個執行緒會自己備份這個資料,並單獨進行對資料的操作,這樣,執行緒之間對應這個共享資料時不可見的。


  •     解決辦法一
        當我們用關鍵字volatile修飾canRun後
package com.itszt;
public class Test {
    public static void main(String[] args) throws InterruptedException {

        MyThread myThread = new MyThread();
        System.out.println("開啟子執行緒");
        myThread.start();
        System.out.println("子執行緒開啟完畢");
        Thread.sleep(2000);
        System.out.println("在主執行緒中終止子執行緒");
        myThread.setCanRun(false);
        System.out.println("main裡面的----"+myThread.canRun);

    }

    public static class MyThread extends Thread{

        private volatile boolean canRun = true;

        public boolean isCanRun() {
            return canRun;
        }

        public void setCanRun(boolean canRun) {
            this.canRun = canRun;
        }

        @Override
        public void run() {
            super.run();
            while (canRun) {


            }
            System.out.println("子執行緒裡面的:"+canRun);
            System.out.println("子執行緒終止。。。。");

        }
    }
}
    執行結果:


  •         解決辦法二
                   使用同步鎖sychronized
package com.itszt;

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

        MyThread myThread = new MyThread();
        System.out.println("開啟子執行緒");
        myThread.start();
        System.out.println("子執行緒開啟完畢");
        Thread.sleep(2000);
        System.out.println("在主執行緒中終止子執行緒");

        myThread.setCanRun(false);
        System.out.println("main裡面的----"+myThread.canRun);

    }

    public static class MyThread extends Thread{

        private  boolean canRun = true;

        public boolean isCanRun() {
            return canRun;
        }

        public void setCanRun(boolean canRun) {
            this.canRun = canRun;
        }


        @Override
        public void run() {
            super.run();
            while (true){
                synchronized (Test.class){
                    if (!canRun){
                        break;
                    }
                }
            }
            System.out.println("子執行緒裡面的:"+canRun);
            System.out.println("子執行緒終止。。。。");

        }
    }
}
  執行結果:


三、i++原子性問題
  1.     i++實際分為三步:讀取--->修改--->寫入
  2.     原子性:“讀-改-寫”三步為一體的,是不能被拆分的
  3.     原子變數:JDK5以後,java.util.concurrent.atomic包下,提供了常用的原子變數
  •         原子變數的值用volatile修飾,確保變數記憶體可見性
  •         CAS(Compare-And-Swap)演算法保證資料的原子性
下面的例子來看volatile關鍵字不能確保操作的原子性問題:


package com.itszt;

/**
* 
*
*
* 關鍵字:volatile:保證該資料是被所有子執行緒共享的
*
* 與static的區別:所有的物件共享同一個資料(volatile是作用於多執行緒的,static是作用於多物件的)
*
*
*/
public class Test1 {

    private static volatile int i=0;
    private static long timeBegin;

    public static void main(String[] args){
        timeBegin = System.currentTimeMillis();
        for (int a = 0; a < 50; a++) {
            new MyThread().start();
        }
    }
    private static class MyThread extends Thread{
        @Override
        public void run() {
            super.run();
            for (int b = 0; b < 10000; b++) {
                i++;
            }
            System.out.println("最後的值: "+i);
            if (i==500000) {
                System.out.println("結束的時間為:"+(System.currentTimeMillis()-timeBegin));
            }
        }
    }
}
執行結果:


可以看出,使用volatile關鍵字修飾的i,經過50個執行緒處理,並沒有自加到500000,出現這種情況的原因正是因為volatile不能保證操作的原子性,當一個執行緒讀取到當前的i值,在進行讀-改-寫操作的過程中,已有其他執行緒更改過了i的值,這樣導致更改資料的不一致性。當我們用原子變數修飾共享資料,就能很好的保證原子性。
package com.itszt;

import java.util.concurrent.atomic.AtomicInteger;

/**
* 
*
* i++  的原子性   讀-改-寫
*
* 鎖:
*
*      悲觀鎖:
*
*          我們使用的時候別人是不能用的,只有當我們使用完釋放了資源,別人才能用
*          synchronized、lock:可以解決原子性問題,但是執行效率都不高
*
*      樂觀鎖:
*
*          多個執行緒可以對同一個資料同時操作,或者同一段程式碼進行操作,只不過操作結果是否生效就不一定了。
*
*  JUC是如何幫我們解決這個問題的呢?
*
*      JUC基於CAS演算法來做
*
*      多個執行緒對同一個資料進行操作時會留一個版本號,每個執行緒每操作一次,就會更新版本號,將版本號自增1
*      比如:A操作之前的版本號為0,那麼當A操作完之後,A將更改這個版本號為1
*      加入A操作完成之後,在A需要更新版本之前,會檢查當前的版本號,發現版本號發生變化不在是0的時候,
*      或者已經為1,那麼肯定有另外的執行緒,比如B對該資料進行了更改,並且生成了新的版本號,此時A就需要重新執行操作了(讀改寫)
*
*
*
*/
public class Test2 {

    //使用原子變數進行改進
    private static volatile AtomicInteger i = new AtomicInteger(0);
    //宣告一個時間戳
    private static long timeBegin;

    public static void main(String[] args){

        timeBegin = System.currentTimeMillis();
        //模擬多執行緒去操作共享資料i
        for (int b = 0; b < 50; b++) {
            new MyThread().start();
        }
    }

    //定義個執行緒
    public static class MyThread extends Thread{
        @Override
        public void run() {
            super.run();
            //迴圈執行i++
            for (int a = 0; a < 10000; a++) {
                i.incrementAndGet();//等同於i++
            }
            System.out.println("輸出i的值:"+i);
            //計算執行完畢的時間
            if (i.get()==500000) {
                System.out.println("完成時間: "+(System.currentTimeMillis()-timeBegin));
            }
        }
    }
}
執行結果:


解決volatile關鍵字存在不能保證原子性的不足,另一種解決方案是使用同步鎖synchronized,但是執行效率遠遠不及樂觀鎖的執行效率

package com.itszt;

/**
* 
*
*
* 關鍵字:volatile:保證該資料是被所有子執行緒共享的
*
* 與static的區別:所有的物件共享同一個資料(volatile是作用於多執行緒的,static是作用於多物件的)
*
*
*/
public class Test1 {

    private static volatile int i=0;

    private static long timeBegin;

    public static void main(String[] args){
        timeBegin = System.currentTimeMillis();
        for (int a = 0; a < 50; a++) {
            new MyThread().start();
        }
    }
    private static class MyThread extends Thread{
        @Override
        public void run() {
            super.run();
            for (int b = 0; b < 10000; b++) {
                //使用同步鎖
                synchronized (Test1.class){
                    i++;
                }
            }
            System.out.println("最後的值: "+i);
            if (i==500000) {
                System.out.println("結束的時間為:"+(System.currentTimeMillis()-timeBegin));
            }
        }
    }
}
執行結果:四、CAS演算法
  •     CAS(Compare-And-Swap)演算法是硬體對於併發的支援,針對多處理器操作而設計的處理器中的一種特殊指令,用於管理對共享資料的併發訪問;
  •     CAS是一種無鎖的非阻塞演算法實現
  •     CAS包含三個運算元:
    • 需要讀寫的記憶體值:V
    • 進行比較的預估值:A
    • 擬寫入的更新值:B
    • 當且僅當V==A時,V=B,否則,將重新操作。