volatile與synchronized的區別

AskHarries發表於2018-04-26

1. volatile修飾的變數具有可見性

113

從圖中可以看出:

①每個執行緒都有一個自己的本地記憶體空間–執行緒棧空間???執行緒執行時,先把變數從主記憶體讀取到執行緒自己的本地記憶體空間,然後再對該變數進行操作

②對該變數操作完後,在某個時間再把變數重新整理回主記憶體

public class RunThread extends Thread {

 private boolean isRunning = true;

 public boolean isRunning() {
 return isRunning;
 }

 public void setRunning(boolean isRunning) {
 this.isRunning = isRunning;
 }

 @Override
 public void run() {
 System.out.println("進入到run方法中了");
 while (isRunning == true) {
 }
 System.out.println("執行緒執行完成了");
 }
}

public class Run {
 public static void main(String[] args) {
 try {
 RunThread thread = new RunThread();
 thread.start();
 Thread.sleep(1000);
 thread.setRunning(false);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
}複製程式碼

Run.java 第28行,main執行緒 將啟動的執行緒RunThread中的共享變數設定為false,從而想讓RunThread.java 第14行中的while迴圈結束。

如果,我們使用JVM -server引數執行該程式時,RunThread執行緒並不會終止!從而出現了死迴圈!!

原因分析:

現在有兩個執行緒,一個是main執行緒,另一個是RunThread。它們都試圖修改 第三行的 isRunning變數。按照JVM記憶體模型,main執行緒將isRunning讀取到本地執行緒記憶體空間,修改後,再重新整理回主記憶體。

而在JVM 設定成 -server模式執行程式時,執行緒會一直在私有堆疊中讀取isRunning變數。因此,RunThread執行緒無法讀到main執行緒改變的isRunning變數

從而出現了死迴圈,導致RunThread無法終止。這種情形,在《Effective JAVA》中,將之稱為“活性失敗”

解決方法,在第三行程式碼處用 volatile 關鍵字修飾即可。這裡,它強制執行緒從主記憶體中取 volatile修飾的變數。

2. volatile禁止指令重排

所謂原子性,就是某系列的操作步驟要麼全部執行,要麼都不執行。

比如,變數的自增操作 i++,分三個步驟:

①從記憶體中讀取出變數 i 的值

②將 i 的值加1

③將 加1 後的值寫回記憶體

這說明 i++ 並不是一個原子操作。因為,它分成了三步,有可能當某個執行緒執行到了第②時被中斷了,那麼就意味著只執行了其中的兩個步驟,沒有全部執行。

關於volatile的非原子性,看個示例:

public class MyThread extends Thread {
 public volatile static int count;

 private static void addCount() {
 for (int i = 0; i < 100; i++) {
 count++;
 }
 System.out.println("count=" + count);
 }

 @Override
 public void run() {
 addCount();
 }
}

public class Run {
 public static void main(String[] args) {
 MyThread[] mythreadArray = new MyThread[100];
 for (int i = 0; i < 100; i++) {
 mythreadArray[i] = new MyThread();
 }

 for (int i = 0; i < 100; i++) {
 mythreadArray[i].start();
 }
 }
}複製程式碼

MyThread類第2行,count變數使用volatile修飾

Run.java 第20行 for迴圈中建立了100個執行緒,第25行將這100個執行緒啟動去執行 addCount(),每個執行緒執行100次加1

期望的正確的結果應該是 100*100=10000,但是,實際上count並沒有達到10000

原因是:volatile修飾的變數並不保證對它的操作(自增)具有原子性。(對於自增操作,可以使用JAVA的原子類AutoicInteger類保證原子自增)

比如,假設 i 自增到 5,執行緒A從主記憶體中讀取i,值為5,將它儲存到自己的執行緒空間中,執行加1操作,值為6。此時,CPU切換到執行緒B執行,從主從記憶體中讀取變數i的值。由於執行緒A還沒有來得及將加1後的結果寫回到主記憶體,執行緒B就已經從主記憶體中讀取了i,因此,執行緒B讀到的變數 i 值還是5

相當於執行緒B讀取的是已經過時的資料了,從而導致執行緒不安全性。這種情形在《Effective JAVA》中稱之為“安全性失敗”

綜上,僅靠volatile不能保證執行緒的安全性。(原子性)

3. synchronized

synchronized可作用於一段程式碼或方法,既可以保證可見性,又能夠保證原子性。

可見性體現在:通過synchronized或者Lock能保證同一時刻只有一個執行緒獲取鎖然後執行同步程式碼,並且在釋放鎖之前會將對變數的修改重新整理到主存中。

原子性表現在:要麼不執行,要麼執行到底。

如果對上面的執行結果還有疑問,也先不用急,我們先來了解Synchronized的原理,再回頭上面的問題就一目瞭然了。我們先通過反編譯下面的程式碼來看看Synchronized是如何實現對程式碼塊進行同步的:

package com.paddx.test.concurrent;

public class SynchronizedDemo {
 public void method() {
 synchronized (this) {
 System.out.println("Method 1 start");
 }
 }
}複製程式碼

反編譯結果:

820406-20160414215316020-1963237484

關於這兩條指令的作用,我們直接參考JVM規範中描述:

monitorenter :

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

這段話的大概意思為:

每個物件有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,執行緒執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:

1、如果monitor的進入數為0,則該執行緒進入monitor,然後將進入數設定為1,該執行緒即為monitor的所有者。

2、如果執行緒已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.

3.如果其他執行緒已經佔用了monitor,則該執行緒進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。

monitorexit:

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

這段話的大概意思為:

執行monitorexit的執行緒必須是objectref所對應的monitor的所有者。

指令執行時,monitor的進入數減1,如果減1後進入數為0,那執行緒退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的執行緒可以嘗試去獲取這個 monitor 的所有權。

通過這兩段描述,我們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是通過一個monitor的物件來完成,其實wait/notify等方法也依賴於monitor物件,這就是為什麼只有在同步的塊或者方法中才能呼叫wait/notify等方法,否則會丟擲java.lang.IllegalMonitorStateException的異常的原因。

Synchronized是Java併發程式設計中最常用的用於保證執行緒安全的方式,其使用相對也比較簡單。但是如果能夠深入瞭解其原理,對監視器鎖等底層知識有所瞭解,一方面可以幫助我們正確的使用Synchronized關鍵字,另一方面也能夠幫助我們更好的理解併發程式設計機制,有助我們在不同的情況下選擇更優的併發策略來完成任務。對平時遇到的各種併發問題,也能夠從容的應對。

總結

1.volatile僅能使用在變數級別; synchronized則可以使用在變數、方法、和類級別的

2.volatile僅能實現變數的修改可見性,並不能保證原子性;synchronized則可以保證變數的修改可見性和原子性

3.volatile不會造成執行緒的阻塞; synchronized可能會造成執行緒的阻塞。

4.volatile標記的變數不會被編譯器優化;synchronized標記的變數可以被編譯器優化

volatile與synchronized的區別


相關文章