前一篇文章《Synchronized用法原理和鎖優化升級過程》從面試角度詳細分析了synchronized關鍵字原理,本篇文章主要圍繞volatile關鍵字用代碼分析下可見性,原子性,有序性,synchronized也輔助證實一下,來加深對鎖的理解。程序員
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線程使用的值和主線程使用值不一致,致使程序混亂,這就是線程之間的不可見性,這麼說你應該能明白了。線程間的不可見性是該程序死循環的根本緣由。spring
上述案例中,咱們用代碼證實了線程間的共享變量是不可見的,其實你能夠從上圖得出結論:只要線程A的工做內存可以感知主內存中共享變量flag的值發生變化就行了,這樣就能把最新的值更新到A線程的工做內存了,你只要能想到這裏,問題就已經結束了,沒錯,volatile關鍵字就實現了這個功能,線程A能感知到主內存共享變量flag發生了變化,因而強制從主內存讀取到flag最新值設置到本身工做內存,因此想要VolatileTest代碼程序正常結束,用volatile關鍵字修飾共享變量flag,private volatile static boolean flag = true;就大功告成。volatile底層實現的硬件基礎是基於硬件架構和緩存一致性協議。若是想深刻下,能夠翻看上一篇文章《可見性是什麼?(通俗易懂)》。必定要試試纔會有收穫哦!緩存
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有相同的可見性語義。springboot
原子性是指一個操做要麼成功,要麼失敗,是一個不可分割的總體。多線程
/** * @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++時,就出現了線程安全問題。框架
咱們用synchronized關鍵字來改造上面的代碼。ide
/** * @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,控制檯輸出結果以下圖所示。
什麼是有序性?咱們寫的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編譯器優化代碼重排序後的結果。
被volatile修飾的共享變量至關於屏障,屏障的做用是不容許指令隨意重排的,有序性主要表如今下面三個方面。
/** * @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;兩行代碼也作了重排序。證實了屏障上面的指令是能夠重排序的。
如上圖所示將c,d屏障放到普通變量上面,再次執行代碼,依然會有x,y同時等於0的狀況,證實了屏障下面的指令是能夠重排的。
/** * @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,該程序會一直陷入死循環,結束不了,證實了屏障上下的代碼不能夠重排。
/** * @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的狀況。
你們能夠看到我最近幾篇文章分析多線程花了很多精力都在談論可見性,原子性等問題,由於這些特性是理解多線程的基礎,在我看來基礎又特別重要,因此怎麼反覆寫我認爲都不過度,在這以前有不少新手或者有2到3年工做經驗的童鞋常常會問我關於Java的學習方法,我給他們的建議就是要紮實基礎,別上來就學高級的知識點或者框架,好比ReentrantLock源碼,springboot框架,就像你玩遊戲,一開始你就玩難度級別比較高的,一旦坡度比較高你就會比較難受吃力更別說對着書本了,這就是真正的從入門到放棄的過程。同時在學習的時候別光思考,以爲這個知識點本身會了就過了,這是不夠的須要多寫代碼,多實踐,你在這個過程當中再去加深本身對知識的理解與記憶,其實有不少知識你看起來是理解了,可是你沒有動手去實踐,你也沒有真正理解,這樣只看不作的方法我是不推薦的,本人本科畢業後工做7年,一直從事Java一線的研發工做,中間也帶過團隊,由於本身曾經也走過不少彎路踏着坑走過來的,對學習程序仍是有必定的心得體會,我會在從此的日子裏持續整理把一些經驗和知識方面的經歷分享給你們,但願你們喜歡關注我。我是叫練,叫個口號就開始練!
總結下來就是兩句話:多動手,紮實基礎。
今天給和你們聊了多線程的3個重要的特性,用代碼實現的方式詳細闡述了這些名詞的含義,若是認真執行了一遍代碼應該能看明白,喜歡的請點贊加關注哦。我是叫練【公衆號】,邊叫邊練。