1. 程式人生 > 實用技巧 >volatile,synchronized可見性,有序性,原子性程式碼證明(基礎硬核)

volatile,synchronized可見性,有序性,原子性程式碼證明(基礎硬核)

0.簡介


前一篇文章《Synchronized用法原理和鎖優化升級過程》從面試角度詳細分析了synchronized關鍵字原理,本篇文章主要圍繞volatile關鍵字用程式碼分析下可見性,原子性,有序性,synchronized也輔助證明一下,來加深對鎖的理解。

**

1.可見性


1.1 不可見性

A執行緒操作共享變數後,該共享變數對執行緒B是不可見的。我們來看下面的程式碼。

package com.duyang.thread.basic.volatiletest;
/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 10:10
 * @description:不可見性測試
 * @modified By:
 * 公眾號:叫練
 */
public class VolatileTest {

    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            while (flag){
                //注意在這裡不能有輸出
            };
            System.out.println("threadA over");
        });
        threadA.start();
        //休眠100毫秒,讓執行緒A先執行
        Thread.sleep(100);
        //主執行緒設定共享變數flag等於false
        flag = false;
    }
}

上述程式碼中,在主執行緒中啟動了執行緒A,主執行緒休眠100毫秒,目的是讓執行緒A先執行,主執行緒最後設定共享變數flag等於false,控制檯沒有輸出結果,程式死迴圈沒有結束不了。如下圖所示主執行緒執行完後flag = false後Java記憶體模型(JMM),主執行緒把自己工作記憶體的flag值設定成false後同步到主記憶體,此時主記憶體flag=false,執行緒A並沒有讀取到主記憶體最新的flag值(false),主執行緒執行完畢,執行緒A工作記憶體一直佔著cpu時間片不會從主記憶體更新最新的flag值,執行緒A看不到主記憶體最新值,A執行緒使用的值和主執行緒使用值不一致,導致程式混亂,這就是執行緒之間的不可見性,這麼說你應該能明白了。

執行緒間的不可見性是該程式死迴圈的根本原因。

1.2 volatile可見性

上述案例中,我們用程式碼證明了執行緒間的共享變數是不可見的,其實你可以從上圖得出結論:只要執行緒A的工作記憶體能夠感知主記憶體中共享變數flag的值發生變化就好了,這樣就能把最新的值更新到A執行緒的工作記憶體了,你只要能想到這裡,問題就已經結束了,沒錯,volatile關鍵字就實現了這個功能,執行緒A能感知到主記憶體共享變數flag發生了變化,於是強制從主記憶體讀取到flag最新值設定到自己工作記憶體,所以想要VolatileTest程式碼程式正常結束,用volatile關鍵字修飾共享變數flag,private volatile static boolean flag = true

;就大功告成。volatile底層實現的硬體基礎是基於硬體架構和快取一致性協議。如果想深入下,可以翻看上一篇文章可見性是什麼?(通俗易懂)》。一定要試試才會有收穫哦!

1.3 synchronized可見性

synchronized是能保證共享變數可見的。每次獲取鎖都重新從主記憶體讀取最新的共享變數。

package com.duyang.thread.basic.volatiletest;
/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 10:10
 * @description:不可見性測試
 * @modified By:
 * 公眾號:叫練
 */
public class VolatileTest {

    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            while (flag){
                synchronized (VolatileTest.class){
                    
                }
            };
            System.out.println("threadA over");
        });
        threadA.start();
        //休眠100毫秒,讓執行緒A先執行
        Thread.sleep(100);
        //主執行緒設定共享變數flag等於false
        flag = false;
    }
}

上述程式碼中,我線上程A的while迴圈中加了一個同步程式碼塊,synchronized (VolatileTest.class)鎖的是VolatileTest類的class。最終程式輸出"threadA over",程式結束。可以得出結論:執行緒A每次加鎖前會去讀取主記憶體共享變數flag=false這條最新的資料。由此證明synchronized關鍵字和volatile有相同的可見性語義。

2.原子性


2.1 原子性

原子性是指一個操作要麼成功,要麼失敗,是一個不可分割的整體。

2.2 volatile 非原子性

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 11:22
 * @description:Volatile關鍵字原子性測試
 * @modified By:
 * 公眾號:叫練
 */
public class VolatileAtomicTest {

    private volatile static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread threadA = new Thread(task);
        Thread threadB = new Thread(task);
        threadA.start();
        threadB.start();
        //主執行緒等待AB執行完畢!
        threadA.join();
        threadB.join();
        System.out.println("累加count="+count);
    }

    private static class Task implements Runnable {
        @Override
        public void run() {
            for(int i=0; i<10000; i++) {
                count++;
            }
        }
    }

}

上述程式碼中,在主執行緒中啟動了執行緒A,B,每個執行緒將共享變數count值加10000次,執行緒AB執行完成之後輸出count累加值;下圖是控制檯輸出結果,答案不等於20000,證明了volatile修飾的共享變數並不保證原子性。出現這個問題的根本原因的count++,這個操作不是原子操作,在JVM中將count++分成3步操作執行。

  • 讀取count值。
  • 將count加1。
  • 寫入count值到主記憶體。

當多執行緒操作count++時,就出現了執行緒安全問題。

2.3 synchronized 原子性

我們用synchronized關鍵字來改造上面的程式碼。

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 11:22
 * @description:Volatile關鍵字原子性測試
 * @modified By:
 * 公眾號:叫練
 */
public class VolatileAtomicTest {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread threadA = new Thread(task);
        Thread threadB = new Thread(task);
        threadA.start();
        threadB.start();
        //主執行緒等待AB執行完畢!
        threadA.join();
        threadB.join();
        System.out.println("累加count="+count);
    }

    private static class Task implements Runnable {
        @Override
        public void run() {
            //this鎖住的是Task物件例項,也就是task
            synchronized (this) {
                for(int i=0; i<10000; i++) {
                    count++;
                }
            }
        }
    }
}

上述程式碼中,線上程自增的方法中加了synchronized(this)同步程式碼塊,this鎖住的是Task物件例項,也就是task物件;執行緒A,B執行順序是同步的,所以最終AB執行緒執行的結果是20000,控制檯輸出結果如下圖所示。

3.有序性


3.1 有序性

什麼是有序性?我們寫的Java程式程式碼不總是按順序執行的,都有可能出現程式重排序(指令重排)的情況,這麼做的好處就是為了讓執行塊的程式程式碼先執行,執行慢的程式放到後面去,提高整體執行效率。畫個簡單圖後舉個實際運用案例程式碼,大家就學到了。

如上圖所示,任務1耗時長,任務2耗時短,JIT編譯程式後,任務2先執行,再執行任務1,對程式最終執行結果沒有影響,但是提高了效率啊(任務2先執行完對結果沒有影響,但提高了響應速度)!

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 15:09
 * @description:指令重排測試
 * @modified By:
 * 公眾號:叫練
 */
public class CodeOrderTest {
    private static int x,y,a,b=0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //初始化4個變數
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 3;
                    x = b;
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 3;
                    y = a;
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("執行次數:"+count);
                break;
            } else {
                System.out.println("執行次數:"+count+","+"x:"+x +" y:"+y);
            }
        }

    }
}

上述程式碼中,迴圈啟動執行緒A,B,如果說x,y都等於0時,程式退出。count是程式次數計數器。下圖是控制檯程式列印部分結果。從圖上可以分析出x,y都等於0時,執行緒A的a = 3;x = b;兩行程式碼做了重排序,執行緒B中b = 3;y = a;兩行程式碼也做了重排序。這就是JIT編譯器優化程式碼重排序後的結果。

3.2 volatile有序性

被volatile修飾的共享變數相當於屏障,屏障的作用是不允許指令隨意重排的,有序性主要表現在下面三個方面。

3.2.1 屏障上面的指令可以重排序。

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 15:09
 * @description:指令重排測試
 * @modified By:
 * 公眾號:叫練
 */
public class VolatileCodeOrderTest {
    private static int x,y,a,b=0;
    private static volatile int c = 0;
    private static volatile int d = 0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //初始化4個變數
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 3;
                    x = b;
                    c = 4;
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 3;
                    y = a;
                    d = 4;
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("執行次數:"+count);
                break;
            } else {
                System.out.println("執行次數:"+count+","+"x:"+x +" y:"+y);
            }
        }

    }
}

上述程式碼中,迴圈啟動執行緒A,B,如果說x,y都等於0時,程式退出。共享變數c,d是volatile修飾,相當於記憶體屏障,count是程式次數計數器。下圖是控制檯程式列印部分結果。從圖上可以分析出x,y都等於0時,執行緒A的a = 3;x = b;兩行程式碼做了重排序,執行緒B中b = 3;y = a;兩行程式碼也做了重排序。證明了屏障上面的指令是可以重排序的。

3.2.2 屏障下面的指令可以重排序。


如上圖所示將c,d屏障放到普通變數上面,再次執行程式碼,依然會有x,y同時等於0的情況,證明了屏障下面的指令是可以重排的。

3.2.3 屏障上下的指令不可以重排序。

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 15:09
 * @description:指令重排測試
 * @modified By:
 * 公眾號:叫練
 */
public class VolatileCodeOrderTest {
    private static int x,y,a,b=0;
    private static volatile int c = 0;
    private static volatile int d = 0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //初始化4個變數
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            c = 0;
            d = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 3;
                    //禁止上下重排
                    c = 4;
                    x = b;
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 3;
                    //禁止上下重排
                    d = 4;
                    y = a;
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("執行次數:"+count);
                break;
            } else {
                System.out.println("執行次數:"+count+","+"x:"+x +" y:"+y);
            }
        }

    }
}

如上述程式碼,將屏障放在中間,會禁止上下指令重排,x,y變數不可能同時為0,該程式會一直陷入死迴圈,結束不了,證明了屏障上下的程式碼不可以重排。

3.3 synchronized有序性

/**
 * @author :jiaolian
 * @date :Created in 2020-12-22 15:09
 * @description:指令重排測試
 * @modified By:
 * 公眾號:叫練
 */
public class VolatileCodeOrderTest {
    private static int x,y,a,b=0;
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {

        while (true) {
            //初始化4個變數
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (VolatileCodeOrderTest.class) {
                        a = 3;
                        x = b;
                    }
                }
            });
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (VolatileCodeOrderTest.class) {
                        b = 3;
                        y = a;
                    }
                }
            });
            threadA.start();
            threadB.start();
            threadA.join();
            threadB.join();
            count++;
            if (x == 0 && y==0) {
                System.out.println("執行次數:"+count);
                break;
            } else {
                System.out.println("執行次數:"+count+","+"x:"+x +" y:"+y);
            }
        }
    }
}

上述程式碼中,x,y也不可能同時等於0,synchronized鎖的VolatileCodeOrderTest的class物件,執行緒A,B是同一把鎖,程式碼是同步執行的,是有先後順序的,所以synchronized也能保證有序性。值得注意的一點是上述程式碼synchronized不能用synchronized(this),this表示當前執行緒也就是threadA或threadB,就不是同一把鎖了,如果用this測試會出現x,y同時等於0的情況。

4.程式設計師學習方法心得


大家可以看到我最近幾篇文章分析多執行緒花了不少精力都在談論可見性,原子性等問題,因為這些特性是理解多執行緒的基礎,在我看來基礎又特別重要,所以怎麼反覆寫我認為都不過分,在這之前有很多新手或者有2到3年工作經驗的童鞋經常會問我關於Java的學習方法,我給他們的建議就是要紮實基礎,別上來就學高階的知識點或者框架,比如ReentrantLock原始碼,springboot框架,就像你玩遊戲,一開始你就玩難度級別比較高的,一旦坡度比較高你就會比較難受吃力更別說對著書本了,這就是真正的從入門到放棄的過程。同時在學習的時候別光思考,覺得這個知識點自己會了就過了,這是不夠的需要多寫程式碼,多實踐,你在這個過程中再去加深自己對知識的理解與記憶,其實有很多知識你看起來是理解了,但是你沒有動手去實踐,你****也沒有真正理解,這樣只看不做的方法我是不推薦的,本人本科畢業後工作7年,一直從事Java一線的研發工作,中間也帶過團隊,因為自己曾經也走過很多彎路踏著坑走過來的,對學習程式還是有一定的心得體會,我會在今後的日子裡持續整理把一些經驗和知識方面的經歷分享給大家,希望大家喜歡關注我。我是叫練,叫個口號就開始練!
總結下來就是兩句話:多動手,紮實基礎

5.總結


今天給和大家聊了多執行緒的3個重要的特性,用程式碼實現的方式詳細闡述了這些名詞的含義,如果認真執行了一遍程式碼應該能看明白,喜歡的請點贊加關注哦。我是叫練【公眾號】,邊叫邊練。