1. 程式人生 > >《Java併發程式設計的藝術》第二章——Java併發機制的底層實現

《Java併發程式設計的藝術》第二章——Java併發機制的底層實現

Java併發機制的底層實現原理 知識點:
  1. volatile的應用
  2. synchronized的實現原理及應用
  3. 原子操作的實現原理
1.volatile的應用 Java語言規範第3版中對volatile的定義如下:Java程式語言允許執行緒訪問共享變數,為了確保共享變數能夠被準確和一致的更新,執行緒應該確保通過排他鎖單獨獲取這個變數。 也就是說,使用volatile修飾的變數,可以保證其“可見性”。
  • 何為“可見性”?
就是一個執行緒的修改,可以讓另一個執行緒”感知“到。
  • 為什麼需要“可見性”?
所謂“共享變數”就是所有執行緒都可以使用的變數,儲存於記憶體中,每個使用到該變數的執行緒,都要儲存一份變數的副本到自己對應的CPU快取記憶體中,每個CPU快取記憶體都是相互獨立的
,當CPU修改共享變數後(其實只是修改的共享變數的副本),需要寫回到記憶體中,而寫回到記憶體中這個操作,時機是不確定的,所以就可能造成該共享變數已經修改(但並未寫回記憶體),但其他快取記憶體中仍然儲存著舊值的副本的情況。
  • volatile 的作用及原理?
volatile在此刻隆重登場,使用volatile修飾的共享變數,會在編譯時增加一個“Lock”的字首指令,該指令會引發兩件事情: 1)將當前處理器快取行的資料立即寫回系統記憶體。 2)這個寫回記憶體的操作會使其他CPU裡快取了該記憶體地址的資料無效。 為了提高執行速度,CPU是不和記憶體直接通訊的,而是把系統記憶體的資料快取到內部快取再進行操作,操作後,並不確定何時寫回記憶體,而使用volatile修飾的變數會讓CPU將當前快取行立即寫回記憶體。但即使在寫回記憶體後,其他CPU裡快取的資料仍然可能是舊值,所以,在多處理器下,就會實現快取一致性協議來避免這個問題。每個處理器通過嗅探在總線上傳播的資料來檢查自己的快取是否過期,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前快取行設定為無效狀態,當處理器需要對這個資料進行操作時,再重新從記憶體中讀取。
  • volatile的使用優化
追加位元組優化效能:JDK7的併發包裡新增一個佇列集合類LinkedTransferQueue,它在使用volatile變數時,用一種追加位元組的方式來優化隊列出隊和入隊的效能。原理是:若佇列的頭節點和尾節點都不足64位元組時,頭節點和尾節點會被讀取到同一個快取行中,當一個處理器試圖修改頭節點時,需要鎖定整個快取行,那麼在快取一致性協議下,會導致其他處理器不能訪問自己快取中的尾節點(因為他的快取已經無效,需要重新從記憶體中讀取),而出隊入隊操作會不停的修改頭節點和尾節點,會嚴重影響效能。所以採用追加到64位元組來避免該問題。 【備註】雖然追加位元組的方式可以提高效能,但並不是所有場景都適用,如:快取行非64位元組寬的處理器,共享變數不會被頻繁的寫。
2.synchronized的實現原理及應用 synchronized是多執行緒併發程式設計中的元老,亦可稱為”重量級鎖“。 以下例子展示synchronized的用法: 例項一:
package com.lipeng.second;

import java.util.concurrent.TimeUnit;
/**
 * 使用Synchronized修飾方法,同一時間只能有一個執行緒訪問被同步的程式碼
 * SyncDemo1-Thread-1、SyncDemo1-Thread-2訪問同步程式碼
 * SyncDemo1-Thread-3、SyncDemo1-Thread-4訪問非同步程式碼
 * @author promi
 *
 */
public class SyncDemo1 {
	static Sync1 sync=new Sync1();
	public static void main(String[] args) {
		Thread thread1=new Thread(new Runnable() {
			@Override
			public void run() {
				sync.syncAction();
			}
		},"SyncDemo1-Thread-1");
		Thread thread2=new Thread(new Runnable() {
			@Override
			public void run() {
				sync.syncAction();
			}
		},"SyncDemo1-Thread-2");
		Thread thread3=new Thread(new Runnable() {
			@Override
			public void run() {
				sync.noSyncAction();
			}
		},"SyncDemo1-Thread-3");
		Thread thread4=new Thread(new Runnable() {
			@Override
			public void run() {
				sync.noSyncAction();
			}
		},"SyncDemo1-Thread-4");
		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();
	}
}
class Sync1{
	public synchronized void syncAction(){
		System.out.println(Thread.currentThread().getName()+" 執行syncAction方法 ,TimeStrap:"+System.currentTimeMillis());
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	public void noSyncAction(){
		System.out.println(Thread.currentThread().getName()+"執行noSyncAction方法,TimeStrap:"+System.currentTimeMillis());
		try {
			TimeUnit.SECONDS.sleep(3);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}



執行結果:
通過結果可以看到執行緒1和2在執行同步程式碼時,執行緒1先獲取到鎖,直到3秒後釋放鎖,執行緒2才可以獲取到鎖並執行。 例項二:
package com.lipeng.second;

import java.util.concurrent.TimeUnit;
/**
 * 使用synchronized封裝程式碼塊獲取"物件鎖"
 * SyncDemo1-Thread-1、SyncDemo1-Thread-2訪問同步程式碼
 * SyncDemo1-Thread-3、SyncDemo1-Thread-4訪問非同步程式碼
 * @author promi
 *
 */
public class SyncDemo2 {
	static Sync2 sync=new Sync2();
	public static void main(String[] args) {
		Thread thread1=new Thread(new Runnable() {
			@Override
			public void run() {
				sync.syncAction();
			}
		},"SyncDemo2-Thread-1");
		Thread thread2=new Thread(new Runnable() {
			@Override
			public void run() {
				sync.syncAction();
			}
		},"SyncDemo2-Thread-2");
		Thread thread3=new Thread(new Runnable() {
			@Override
			public void run() {
				sync.noSyncAction();
			}
		},"SyncDemo2-Thread-3");
		Thread thread4=new Thread(new Runnable() {
			@Override
			public void run() {
				sync.noSyncAction();
			}
		},"SyncDemo2-Thread-4");
		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();
	}
}
class Sync2{
	public void syncAction(){
		synchronized(this){
			System.out.println(Thread.currentThread().getName()+" 執行syncAction方法 ,TimeStrap:"+System.currentTimeMillis());
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public void noSyncAction(){
		System.out.println(Thread.currentThread().getName()+" 執行noSyncAction方法 ,TimeStrap:"+System.currentTimeMillis());
	}
}
執行結果:

Synchronized(this)表示在執行該程式碼塊之前需要獲取到物件鎖。線上程1獲取到物件鎖後,其他執行緒仍然可訪問其他未同步的程式碼。 例項三:
package com.lipeng.second;

import java.util.concurrent.TimeUnit;
/**
 * 使用synchronized封裝程式碼塊獲取"類鎖"
 * SyncDemo1-Thread-1、SyncDemo1-Thread-2訪問同步程式碼
 * SyncDemo1-Thread-3、SyncDemo1-Thread-4訪問非同步程式碼
 * @author promi
 *
 */
public class SyncDemo3 {
	public static void main(String[] args) {
		Thread thread1=new Thread(new Runnable() {
			@Override
			public void run() {
				Sync3 sync=new Sync3();
				sync.syncAction();
			}
		},"SyncDemo3-Thread-1");
		Thread thread2=new Thread(new Runnable() {
			@Override
			public void run() {
				Sync3 sync=new Sync3();
				sync.syncAction();
			}
		},"SyncDemo3-Thread-2");
		Thread thread3=new Thread(new Runnable() {
			@Override
			public void run() {
				Sync3 sync=new Sync3();
				sync.noSyncAction();
			}
		},"SyncDemo3-Thread-3");
		Thread thread4=new Thread(new Runnable() {
			@Override
			public void run() {
				Sync3 sync=new Sync3();
				sync.noSyncAction();
			}
		},"SyncDemo3-Thread-4");
		thread1.start();
		thread2.start();
		thread3.start();
		thread4.start();
	}
}
class Sync3{
	public void syncAction(){
		synchronized(Sync3.class){
			System.out.println(Thread.currentThread().getName()+" 執行syncAction方法 ,TimeStrap:"+System.currentTimeMillis());
			try {
				TimeUnit.SECONDS.sleep(3);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	public void noSyncAction(){
		System.out.println(Thread.currentThread().getName()+" 執行noSyncAction方法 ,TimeStrap:"+System.currentTimeMillis());
	}
}

執行結果:

例項一和二的鎖,都是物件鎖,即每個執行緒中使用的都是同一個Sync物件,若在每個執行緒中宣告不同的Sync物件,則不會出現執行緒阻塞等待鎖的情況,因為每個執行緒獲取到及需要獲取的物件鎖並不是同一個。但例項三種同步程式碼塊中需要獲取“類鎖”,即使在每個執行緒中宣告不同的Sync物件,也避免不了鎖的等待。因為該鎖是“屬於類的”,是同一個。 總結synchronized使用形式: a),對於普通方法,鎖是當前物件。 b),對於靜態同步方法,鎖是當前類的Class物件。 c),對於同步方法塊,鎖是synchronized括號裡配置的物件。
  • 那麼synchronized在JVM裡是怎麼實現的呢?
每個物件都有一個monitor物件與之關聯,JVM基於進入和退出Monitor物件來實現方法同步和程式碼同步。但兩者實現細節不同。程式碼同步是使用monitorenter,monitorexit(這兩個指令必須成對出現)指令實現。方法同步具體細節在JVM規範裡並未說明,但通過這兩個指令同樣可以實現。
  • synchronized用的鎖是存在哪裡的?
synchronized用到的鎖資訊是存在Java物件頭裡的。
  • 物件頭的結構?
Java物件頭在32位/64位系統中,分別使用8個位元組及16個位元組來表示。其中Mark Word佔用4個位元組(8個位元組),Class Metadata Address佔用4個位元組(8個位元組)。 Mark Word主要儲存物件的hashCode、分代年齡、鎖資訊等。 Class Metadata Address主要儲存指向物件型別資訊的指標。 以32位系統為例,Java物件頭的儲存結構如下:
但對於不同的鎖及其狀態而言,4個位元組不足以表示其資訊,所以按照鎖標誌位的不同,來儲存不同的資訊。
  • 偏向鎖
在Java SE1.6中,為了減少獲取鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6中,所共有4中狀態,從低到高為:無鎖狀態,偏向鎖,輕量級鎖,重量級鎖。 a),CAS:compare and swap: 存在3個運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。 b),偏向鎖加鎖過程: 當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄儲存偏向的執行緒ID,以後該執行緒再進入和退出同步塊時只需要檢測物件頭中Mark Word中是否儲存當前執行緒的ID即可,而不需要進行CAS操作來進行加鎖和解鎖操作。如果測試成功,則獲得鎖,若測試失敗,則檢測Mark Word中鎖標誌位是否設定為01,如果為01,則進行CAS操作將物件頭偏向鎖儲存的執行緒ID指向當前執行緒。若不為01,則使用CAS競爭鎖。 c),偏向鎖解鎖過程: 偏向鎖使用一種等到競爭才會釋放的機制。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有正在執行的位元組碼)。它會首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著,如果執行緒不處於活動狀態,則將物件頭設定成無鎖狀態;如果執行緒仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向物件的鎖記錄,棧中的鎖記錄和物件頭的Mark Word要麼重新偏向於其他執行緒,要麼恢復到無鎖或標記物件不適合作為偏向鎖,最後喚醒暫停的執行緒。 d),關閉偏向鎖 偏向所在Java6和Java7裡是預設開啟的,但是它在應用程式啟動幾秒鐘後才啟用,如果必要刻意使用JVM引數來關閉延遲:-XX:BiasedLockingStartUpDelay=0.或直接關閉偏向鎖:-XX:-UseBiasedLocking=false。 偏向鎖的初始化流程圖如下:   
  • 輕量級鎖

a),輕量級鎖加鎖: 執行緒在執行同步程式碼塊之前,JVM會先在當前執行緒的棧幀中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中(DIsplaced Mark Word)。然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。如果成功,則獲得鎖。如果失敗,則表示有其他執行緒競爭鎖,當前執行緒嘗試使用自旋來獲取鎖。 b),輕量級鎖解鎖: 在解鎖時,會使用原子的CAS操作將DIsplaced Mark Word替換回物件頭,如果成功,則表示沒有競爭發生。如果失敗,則表示當前所存在競爭,鎖會膨脹為重量級鎖。競爭該鎖的執行緒全部會阻塞,知道當前執行緒釋放該鎖。 鎖膨脹為重量級鎖流程圖如下:
c),鎖的優缺點對比

3.原子操作的實現原理 原子本意是“不能被進一步分割的最小粒子”,而原子操作意為“不可被中斷的一個或一個系列操作”。
  • 處理器如何實現原子操作?
a),使用匯流排鎖保證原子性:當一個處理器使用匯流排時,在總線上輸出LOCK#訊號,其他處理器使用匯流排操作該共享記憶體的請求就會被阻塞,那麼該處理器可以獨佔共享記憶體,從而保證操作的原子性。 b),使用快取鎖保證原子性:當一個處理器對快取行中的共享變數進行操作時,通過快取一致性協議,讓其他處理器中快取中的該共享變數無效,從而保證操作的原子性。
  • Java中如何實現原子操作?
a),通過迴圈CAS:迴圈進行CAS操作直到成功為止。<br> b),通過鎖機制保證只有獲得鎖的執行緒才能操作鎖定的記憶體區域。<br> 【備註】:CAS實現原子操作帶來的問題: ABA問題:若共享變數修改過程為A->B->A,進行CAS操作時,雖然值發生過更改,但又變成了預期值。解決方法是在變數前追加版本號。1A->2B->3A。 迴圈時間長開銷大:自旋CAS如果長時間不成功,會給CPU帶來很大的開銷。 只能保證一個共享變數的原子操作:CAS無法保證對多個共享變數的原子操作。 【備註】:本文圖片均摘自《Java併發程式設計的藝術》·方騰飛,若本文有錯或不恰當的描述,請各位不吝斧正。謝謝!