1. 程式人生 > >Java併發程式設計(03):多執行緒併發訪問,同步控制

Java併發程式設計(03):多執行緒併發訪問,同步控制

本文原始碼:[GitHub·點這裡](https://github.com/cicadasmile/java-base-parent) || [GitEE·點這裡](https://gitee.com/cicadasmile/java-base-parent) # 一、併發問題 多執行緒學習的時候,要面對的第一個複雜問題就是,併發模式下變數的訪問,如果不理清楚內在流程和原因,經常會出現這樣一個問題:執行緒處理後的變數值不是自己想要的,可能還會一臉懵的說:這不合邏輯吧? ## 1、成員變數訪問 多個執行緒訪問類的成員變數,可能會帶來各種問題。 ```java public class AccessVar01 { public static void main(String[] args) { Var01Test var01Test = new Var01Test() ; VarThread01A varThread01A = new VarThread01A(var01Test) ; varThread01A.start(); VarThread01B varThread01B = new VarThread01B(var01Test) ; varThread01B.start(); } } class VarThread01A extends Thread { Var01Test var01Test = new Var01Test() ; public VarThread01A (Var01Test var01Test){ this.var01Test = var01Test ; } @Override public void run() { var01Test.addNum(50); } } class VarThread01B extends Thread { Var01Test var01Test = new Var01Test() ; public VarThread01B (Var01Test var01Test){ this.var01Test = var01Test ; } @Override public void run() { var01Test.addNum(10); } } class Var01Test { private Integer num = 0 ; public void addNum (Integer var){ try { if (var == 50){ num = num + 50 ; Thread.sleep(3000); } else { num = num + var ; } System.out.println("var="+var+";num="+num); } catch (Exception e){ e.printStackTrace(); } } } ``` 這裡案例的流程就是併發下運算一個成員變數,程式的本意是:var=50,得到num=50,可輸出的實際結果是: ``` var=10;num=60 var=50;num=60 ``` VarThread01A執行緒處理中進入休眠,休眠時num已經被執行緒VarThread01B進行一次加10的運算,這就是多執行緒併發訪問導致的結果。 ## 2、方法私有變數 修改上述的程式碼邏輯,把num變數置於方法內,作為私有的方法變數。 ```java class Var01Test { // private Integer num = 0 ; public void addNum (Integer var){ Integer num = 0 ; try { if (var == 50){ num = num + 50 ; Thread.sleep(3000); } else { num = num + var ; } System.out.println("var="+var+";num="+num); } catch (Exception e){ e.printStackTrace(); } } } ``` 方法內部的變數是私有的,且和當前執行方法的執行緒繫結,不會存線上程間干擾問題。 # 二、同步控制 ## 1、Synchronized關鍵字 使用方式:修飾方法,或者以控制同步塊的形式,保證多個執行緒併發下,同一時刻只有一個執行緒進入方法中,或者同步程式碼塊中,從而使執行緒安全的訪問和處理變數。如果修飾的是靜態方法,作用的是這個類的所有物件。 獨佔鎖屬於悲觀鎖一類,synchronized就是一種獨佔鎖,假設處於最壞的情況,只有一個執行緒執行,阻塞其他執行緒,如果併發高,處理耗時長,會導致多個執行緒掛起,等待持有鎖的執行緒釋放鎖。 ## 2、修飾方法 這個案例和第一個案例原理上是一樣的,不過這裡雖然在修改值的地方加入的同步控制,但是又挖了一個坑,在讀取的時候沒有限制,這個現象俗稱髒讀。 ```java public class AccessVar02 { public static void main(String[] args) { Var02Test var02Test = new Var02Test (); VarThread02A varThread02A = new VarThread02A(var02Test) ; varThread02A.start(); VarThread02B varThread02B = new VarThread02B(var02Test) ; varThread02B.start(); var02Test.readValue(); } } class VarThread02A extends Thread { Var02Test var02Test = new Var02Test (); public VarThread02A (Var02Test var02Test){ this.var02Test = var02Test ; } @Override public void run() { var02Test.change("my","name"); } } class VarThread02B extends Thread { Var02Test var02Test = new Var02Test (); public VarThread02B (Var02Test var02Test){ this.var02Test = var02Test ; } @Override public void run() { var02Test.change("you","age"); } } class Var02Test { public String key = "cicada" ; public String value = "smile" ; public synchronized void change (String key,String value){ try { this.key = key ; Thread.sleep(2000); this.value = value ; System.out.println("key="+key+";value="+value); } catch (InterruptedException e) { e.printStackTrace(); } } public void readValue (){ System.out.println("讀取:key="+key+";value="+value); } } ``` 線上程中,邏輯上已經修改了,只是沒執行到,但是在main執行緒中讀取的value毫無意義,需要在讀取方法上也加入同步的執行緒控制。 ## 3、同步控制邏輯 同步控制實現是基於Object的監視器。 - 執行緒對Object的訪問,首先要先獲得Object的監視器 ; - 如果獲取成功,則會獨佔該物件 ; - 其他執行緒會掉進同步佇列,執行緒狀態變為阻塞 ; - 等Object的持有執行緒釋放鎖,會喚醒佇列中等待的執行緒,嘗試重啟獲取物件監視器; ## 4、修飾程式碼塊 說明一點,程式碼塊包含方法中的全部邏輯,鎖定的粒度和修飾方法是一樣的,就寫在方法上吧。同步程式碼塊一個很核心的目的,減小鎖定資源的粒度,就如同表鎖和行級鎖。 ```java public class AccessVar03 { public static void main(String[] args) { Var03Test var03Test1 = new Var03Test() ; Thread thread1 = new Thread(var03Test1) ; thread1.start(); Thread thread2 = new Thread(var03Test1) ; thread2.start(); Thread thread3 = new Thread(var03Test1) ; thread3.start(); } } class Var03Test implements Runnable { private Integer count = 0 ; public void countAdd() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(this) { count++ ; System.out.println("count="+count); } } @Override public void run() { countAdd() ; } } ``` 這裡就是鎖定count處理這個動作的核心程式碼邏輯,不允許併發處理。 ## 5、修飾靜態方法 靜態方法屬於類層級的方法,物件是不可以直接呼叫的。但是synchronized修飾的靜態方法鎖定的是這個類的所有物件。 ```java public class AccessVar04 { public static void main(String[] args) { Var04Test var04Test1 = new Var04Test() ; Thread thread1 = new Thread(var04Test1) ; thread1.start(); Var04Test var04Test2 = new Var04Test() ; Thread thread2 = new Thread(var04Test2) ; thread2.start(); } } class Var04Test implements Runnable { private static Integer count ; public Var04Test (){ count = 0 ; } public synchronized static void countAdd() { System.out.println(Thread.currentThread().getName()+";count="+(count++)); } @Override public void run() { countAdd() ; } } ``` 如果不是使用同步控制,從邏輯和感覺上,輸出的結果應該如下: ``` Thread-0;count=0 Thread-1;count=0 ``` 加入同步控制之後,實際測試輸出結果: ``` Thread-0;count=0 Thread-1;count=1 ``` ## 6、注意事項 - 繼承中子類覆蓋父類方法,synchronized關鍵字特性不能繼承傳遞,必須顯式宣告; - 構造方法上不能使用synchronized關鍵字,構造方法中支援同步程式碼塊; - 介面中方法,抽象方法也不支援synchronized關鍵字 ; # 三、Volatile關鍵字 ## 1、基本描述 Java記憶體模型中,為了提升效能,執行緒會在自己的工作記憶體中拷貝要訪問的變數的副本。這樣就會出現同一個變數在某個時刻,在一個執行緒的環境中的值可能與另外一個執行緒環境中的值,出現不一致的情況。 使用volatile修飾成員變數,不能修飾方法,即標識該執行緒在訪問這個變數時需要從共享記憶體中獲取,對該變數的修改,也需要同步重新整理到共享記憶體中,保證了變數對所有執行緒的可見性。 ## 2、使用案例 ```java class Var05Test { private volatile boolean myFlag = true ; public void setFlag (boolean myFlag){ this.myFlag = myFlag ; } public void method() { while (myFlag){ try { System.out.println(Thread.currentThread().getName()+myFlag); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } ``` ## 3、注意事項 - 可見性只能確保每次讀取的是最新的值,但不支援變數操作的原子性; - volatile並不會阻塞執行緒方法,但是同步控制會阻塞; - Java同步控制的根本:保證併發下資源的原子性和可見性; # 四、原始碼地址 ``` GitHub·地址 https://github.com/cicadasmile/java-base-parent GitEE·地址 https://gitee.com/cicadasmile/java-base-parent ``` ![](https://img2018.cnblogs.com/blog/1691717/201908/1691717-20190823075428183-1996768