volatile與synchronized的區別

1. volatile修飾的變量具備可見性

113

從圖中能夠看出:java

①每一個線程都有一個本身的本地內存空間–線程棧空間???線程執行時,先把變量從主內存讀取到線程本身的本地內存空間,而後再對該變量進行操做編程

②對該變量操做完後,在某個時間再把變量刷新回主內存安全

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循環結束。bash

若是,咱們使用JVM -server參數執行該程序時,RunThread線程並不會終止!從而出現了死循環!!併發

緣由分析:ide

如今有兩個線程,一個是main線程,另外一個是RunThread。它們都試圖修改 第三行的 isRunning變量。按照JVM內存模型,main線程將isRunning讀取到本地線程內存空間,修改後,再刷新回主內存。優化

而在JVM 設置成 -server模式運行程序時,線程會一直在私有堆棧中讀取isRunning變量。所以,RunThread線程沒法讀到main線程改變的isRunning變量ui

從而出現了死循環,致使RunThread沒法終止。這種情形,在《Effective JAVA》中,將之稱爲「活性失敗」this

解決方法,在第三行代碼處用 volatile 關鍵字修飾便可。這裏,它強制線程從主內存中取 volatile修飾的變量。spa

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的全部權,過程以下:

一、若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者。

二、若是線程已經佔有該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標記的變量能夠被編譯器優化

相關文章
相關標籤/搜索