教學筆記:多線程之volatile與synchronized(二)

JMM中主要是圍繞併發過程中如何處理原子性,可見性和有序性三個特性來建立的。最終可以保證線程安全性,volatile和synchronized兩個關鍵字又是我們最常碰到與最容易提到的關鍵字,這次放在一起來講。

線程安全性:當多個線程訪問某個類的時候,不管運行環境採用何種調度方式或這些線程如何交替執行,並且在主調代碼中不需要額外的同步或協同,這個類都能表現出正確的行為,那麼就稱這個類是線程安全的。

原子性、可見性與有序性

首先來看一下這幾個特性代表的具體含義。

  • 原子性(Atomicity):原子性是指,一個操作是不可中斷的。即使是多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。

    JDK的包中提供了專門的原子包java.util.concurrent.atomic,synchronized關鍵字還有Lock來讓程序在併發環境下具有原子性的特點。

  • 可見性(Visibility):可見性是指當一個線程修改了共享變量的值,其它線程能立即得知這個修改。

    volatile,synchronized和final關鍵字能實現可見性。使用final關鍵字需要注意對象逃逸

  • 有序性:如果再本線程內觀察,所有操作都是有序的,如果再一個線程中觀察另外一個線程,那麼所有操作都是無序的。前半句是指“線程內表現為串行”,後半句是指“指令重排序”現象和“工作內存與主內存同步延遲現象”

    volatile和synchronized關鍵字可以線程之間操作的有序性。

Volatile

一個變量定義為volatile之後,它將具有兩種特性:

  1. 保證次變了對所有線程的可見性,一條線程修改了這個值,新值對其它線程是可以立即得知的。

  2. 禁止指令重排優化。

volatile變量在寫操作時候,會在寫操作後加上store屏障指令,將本地內存刷新到主內存。

volatile變量讀操作的時候,會在讀操作之前加入一條load屏障指令,從主內存中讀取共享變量。

關於JMM的8大操作指令,可以查看我的上篇文章,java內存模型。

volatile變量為什麼在併發下不安全?

volatile變量在各個線程的工作內存中也可以存在不一致的情況,但由於每次使用之前都要刷新,執行引擎看不到不一致的情況,因此可以認為不存在一致性問題,但是Java裡面的運算並非原子操作。

假如說一個寫入值操作不需要依賴依賴這個值的原先值,那麼在進行寫入的時候我們就不需要進行讀取操作。

寫入操作對原本的值的時候沒有要求,那麼所有線程都可以寫入新的值,雖然讀取到的值是相同的,每個線程的操作也是正確的,但是最終結果卻是錯誤的。

教學筆記:多線程之volatile與synchronized(二)

JMM

感興趣的可以運行如下代碼:

public class VolatileTest { public static volatile int count = 0; public static final int THREAD_COUNT = 20;  public static void add(){ count++; } public static void main(String[] args) { Thread[] threads = new Thread[THREAD_COUNT]; for (int i = 0; i < THREAD_COUNT; i++) { threads[i] = new Thread(() -> { for (int j = 0; j < 1000; j++) { add(); } }); threads[i].start(); }  for (int i = 0; i < THREAD_COUNT; i++) { threads[i].join(); }  System.out.println(count); }}// 如果併發正確的話:應該是20000,但是每次運行結果都不到20000

Volatile適合做什麼?

適合做標量,當一個線程對某個變量進行讀寫操作,而其它線程僅僅進行讀操作的時候,是可以保證volatile的正確性的。如下:

volatile bool stopped;public void stop(){ stopped = true}while(!stoppped){ // 執行操作}

Synchronized

Synchronized保證了原子性,可見性與有序性,它的工作時對同步的代碼塊加鎖,使得每次只有一個線程進入代碼塊,從而保證線程安全。synchronized反應到字節碼層面就是monitorenter與monitorexit.

注意*:雖然synchonized關鍵字看起來是萬能的,能保證線程安全性,但是越萬能的控制往往越伴隨著越大的性能影響。

Synchonzied用法

  1. 實例方法上,被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用的對象是調用這個方法的對象;

  2. 靜態方法上,其作用的範圍是整個靜態方法,作用的對象是這個類的所有對象;

  3. 實例方法代碼塊.

  4. 靜態方法代碼塊。

//實例方法public synchronized void add(int value){ this.count += value;} //靜態方法public static synchronized void add(int value){ count += value;}//實例方法代碼塊public void add(int value){ synchronized(this){ this.count += value;  }}//靜態方法代碼塊public class MyClass { public static synchronized void log1(String msg1, String msg2){ log.writeln(msg1); log.writeln(msg2); }  public static void log2(String msg1, String msg2){ synchronized(MyClass.class){ log.writeln(msg1); log.writeln(msg2);  } } } 

Synchonzied案例

public class SynchronziedTest implements Runnable{ static int i = 0; static int j = 0; static SynchronziedTest instance= new SynchronziedTest();  @Override public void run() { for (int j = 0; j < 1000000; j++) { increase(); } } public synchronized void increase(){ i++; } public static void main(String[] args) throws InterruptedException { // 注意新建的線程指向的同一個實例, // 如果指向不同的實例,那麼兩個線程關注的鎖就不是同一把鎖,就會導致線程不安全 Thread t1 = new Thread(instance); Thread t2 = new Thread(instance);  //錯誤的用法 // Thread t3 = new Thread(new SynchronziedTest()); // Thread t4 = new Thread(new SynchronziedTest()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); }}//結果為:200000

注意創建線程的時候指向同一個實例,才會鎖住相同的對象。

最後

這次我們講了線程安全性的基本原則,然後解釋了volatile和synchronized關鍵字,多線程中不得不掌握的關鍵字。

分享來源:簡書,著作權歸作者所有。



教學筆記:多線程之volatile與synchronized(二)


分享到:


相關文章: