文章目錄
synchronized關鍵字
synchronized可以把任意一個非NULL的對象當做鎖,HotSpotVM中,這個鎖稱爲 對象監視器(Object Monitor):
- 作用於靜態方法時,鎖住的是Class實例,相當於一個全局鎖,對所有調用這個靜態方法的線程有效。
- 作用於非靜態方法時,鎖住的是對象的實例(this)。
- 作用於一個代碼塊時,可以通過自定義任意一個對象obj當做鎖,鎖住的是所有以該對象爲鎖的代碼塊。
保證併發三大特性
synchronized能保證可見性、原子性和有序性
synchronized保證可見性原理
執行到synchronized代碼塊時,JVM會執行lock這個原子操作,將這個線程的工作內存中共享變量的副本值清空,之後這個線程再次需要用這些變量的時候,發現自己工作內存中的值已經失效了,就會重新從主存中獲取最新值。當執行完同步代碼塊時,JVM會執行unlock這個原子操作,工作內存的變量副本會立刻同步到主存。
synchronized保證原子性原理
只有獲取到鎖的線程才能去執行synchronized中的代碼塊,即使中途發生線程切換,這個線程持有的鎖不會釋放,所以這期間其他線程也無法獲取到鎖去執行這個代碼塊。但是發生異常的話,Synchronized鎖會自動釋放。
synchronized保證有序性原理
加上了synchronized關鍵詞的代碼塊,編譯器還是會進行代碼重排序的優化,只是synchronized保證了代碼塊只能同步訪問,下一個線程獲取鎖之後,上一個線程對共享變量做的改變對它是可見的,就是通過lock刷新工作內存的機制,這樣,編譯器優化的問題就不會出現。
synchronized的兩個特性
可重入特性
指的是同一線程的外層函數獲得鎖之後,內層函數還可以再次獲取該鎖,也就是說一個線程可以多次獲取同一把鎖。
好處:
- 避免死鎖(對於不可重入鎖,如果A方法中調用B方法,A和B方法都要獲取同一把鎖,A方法先獲取了鎖,去調用B方法的時候,B方法此時要等A釋放鎖才能執行,但是A方法要等B方法執行完了才能釋放鎖,這就造成了死鎖)
- 可以讓我們更好的來封裝代碼,而不是將所有的邏輯都寫在一個同步方法中
示例:
public class Test {
public static void main(String[] args) {
new MyThread().start();
new MyThread().start();
}
}
// 自定義一個線程類
class MyThread extends Thread {
@Override
public void run() {
synchronized (MyThread.class) {
System.out.println(getName() + "進入了同步代碼塊1");
synchronized (MyThread.class) {
System.out.println(getName() + "進入了同步代碼塊2");
}
// 這個代碼塊可以是調用本類或者其他類方法中的同步代碼塊
// test0();
}
}
// public void test0() {
// synchronized (MyThread.class) {
// System.out.println(getName() + "進入了同步代碼塊2");
// }
// }
}
結果:
Thread-0進入了同步代碼塊1
Thread-0進入了同步代碼塊2
Thread-1進入了同步代碼塊1
Thread-1進入了同步代碼塊2
實現原理:
同一個線程能多次獲取同一把鎖進入synchronized代碼塊中執行,實現原理是利用了一個變量記錄(假設微lockedThread)當前獲取到鎖的線程,還有一個變量(假設微lockedCount)記錄當前鎖被獲取的次數,當一個線程嘗試獲取鎖時,如果這個鎖已經被獲取過了,會去判斷獲取到鎖的線程是不是就是當前這個線程,如果是,則對lockedCount變量進行加1,獲取鎖成功。釋放鎖的時候,先對lockedCount變量進行減1,只有當lockedCount減爲0的時候,纔會真正釋放鎖。下面給出一個簡易模擬代碼:
public class Lock {
boolean isLocked = false; // 標識鎖是否被線程獲得
Thread lockedBy = null; // 記錄獲得鎖的線程
int lockedCount = 0; // 記錄一個線程中,鎖被獲取的次數
public synchronized void lock() throws InterruptedException {
Thread thread = Thread.currentThread(); // 當前嘗試獲取鎖的線程
// 鎖已經被線程獲得,並且獲取鎖的線程不是當前線程,則當前線程等待
while(isLocked && lockedBy != thread) {
wait();
}
isLocked = true;
lockedBy = thread;
lockedCount++;
}
public synchronized void unlock() {
lockedCount--;
// 該線程中,獲取鎖的程序都執行了釋放鎖操作,線程才真正釋放鎖
if(0 == lockedCount) {
isLocked = false;
notify();
}
}
}
不可中斷特性
一個線程獲得鎖後,其他嘗試獲取這個鎖的線程只能等待或者阻塞,不能被中斷,只能一直等着擁有鎖的線程釋放鎖。
public class Test {
private static Object obj = new Object();
public static void main(String[] args) throws InterruptedException{
Runnable runnable = () -> {
synchronized (obj) {
String name = Thread.currentThread().getName();
System.out.println(name + "進入同步代碼塊");
// 保證不退出同步代碼塊
try {
Thread.sleep(88888);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(name + "執行結束");
}
}
};
Thread t1 = new Thread(runnable);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(runnable);
t2.start();
System.out.println("停止第二個線程之前");
t2.interrupt();
System.out.println("停止第二個線程之後");
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
Thread-0進入同步代碼塊
停止第二個線程之前
停止第二個線程之後
TIMED_WAITING #第一個線程進入等待狀態
BLOCKED #第二個線程進入阻塞狀態
Thread-0執行結束
Thread-1進入同步代碼塊
Thread-1執行結束
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.exapmle.service.test9.Test.lambda$main$0(Test.java:14)
at java.lang.Thread.run(Thread.java:748)
Lock鎖可以設置爲可中斷的,也可以設置爲不可中斷的:
public class LockTest {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
test02();
}
// 不可中斷鎖 lock()
public static void test01() throws InterruptedException {
Runnable runnable = () -> {
String name = Thread.currentThread().getName();
try{
lock.lock();
System.out.println(name+"獲取鎖");
// 讓獲取鎖的線程暫時不退出
Thread.sleep(88888);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println(name+"釋放鎖");
}
};
Thread t1 = new Thread(runnable);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(runnable);
t2.start();
System.out.println("停止第二個線程之前");
t2.interrupt();
System.out.println("停止第二個線程之後");
Thread.sleep(1000);
System.out.println(t1.getState());
System.out.println(t2.getState());
}
// 可中斷鎖 tryLock()
public static void test02() throws InterruptedException {
Runnable runnable = () -> {
String name = Thread.currentThread().getName();
boolean b = false;
try{
// 3s之內沒有獲取到鎖,就停止阻塞,去執行其他任務
b = lock.tryLock(3, TimeUnit.SECONDS);
if(b) {
System.out.println(name+"獲取鎖");
// 讓獲取鎖的線程暫時不退出
Thread.sleep(88888);
}else{
System.out.println(name+"沒有獲取到鎖");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(b) {
lock.unlock();
System.out.println("name"+"釋放鎖");
}
}
};
Thread t1 = new Thread(runnable);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(runnable);
t2.start();
}
}
test01執行結果:
Thread-0獲取鎖
停止第二個線程之前
停止第二個線程之後
TIMED_WAITING
WAITING # 雖然執行了t2.interrupt(); #但是線程2並沒有被中斷,而是進入等待狀態
Thread-0釋放鎖
Thread-1獲取鎖 # 等線程1釋放鎖之後,第二個線程獲取鎖
Thread-1釋放鎖
java.lang.InterruptedException: sleep interrupted # 中斷異常,因爲不可中斷
at java.lang.Thread.sleep(Native Method)
at com.exapmle.service.test9.LockTest.lambda$test01$0(LockTest.java:22)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
test02執行結果:
Thread-0獲取鎖
Thread-1沒有獲取到鎖 #3s之內沒有獲取到鎖,就停止阻塞,去執行其他任務
name釋放鎖
Java對象
在JVM中,對象在內存中的佈局分爲三塊區域:
- 對象頭:Java對象頭在32位虛擬機上佔64bit、在64位虛擬機上佔96bit
- 實例數據:存放類的屬性數據信息,包括父類的屬性信息;
- 對齊填充:由於虛擬機要求 對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊;
對象頭:
Hotspot虛擬機的對象頭包括兩部分,Mark Word(標記字段,64位虛擬機上佔64bit)、Klass Pointer(類型指針,Hotspot虛擬機默認開啓指針壓縮,所以64位虛擬機上佔32bit。沒有開啓指針壓縮時一個指針佔8字節。開啓指針壓縮是因爲對象太大,會減小緩存命中率,GC開銷增大。使用參數-XX:-UseCompressedOops
可以關閉指針壓縮):
- Klass Pointer是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個Class
- Mark Word用於存儲對象自身的運行時數據,如:HashCode、GC分代年齡、Synchronized鎖狀態標誌
是實現Synchronized鎖的關鍵,Synchronized加鎖實際是對對象頭狀態的改變
Synchronized鎖狀態:無鎖不可偏向、無鎖可偏向(但還沒偏向)、偏向鎖、輕量級鎖、重量級鎖
Mark Word:
Lock Record:
Lock Record是線程私有的數據結構,每一個被鎖住的對象Mark Word都會和一個Lock Record關聯,用於存儲鎖對象的Mark Word的拷貝。
synchronized原理
public class Test {
private static Object obj = new Object();
// synchronized作用在代碼塊上
public static void main(String[] args) {
synchronized (obj) {
System.out.println("1");
}
}
// synchronized作用在方法上
public synchronized void fun() {
System.out.println("2");
}
}
現在通過反彙編字節碼文件看看synchronized彙編源碼:
javap -p -v 字節碼文件xx.class #-p是顯示所有方法 -v是顯示所有細節
synchronized是通過monitor監視器鎖來實現,monitor對象中有兩個主要屬性:owner記錄當前擁有鎖的線程,recursion記錄當前鎖被獲取的次數。對於synchronized修飾的代碼塊,在源碼編譯成字節碼的時候,會在同步代碼塊的入口和出口分別插入monitorenter和monitorexit這兩個字節碼指令。對於synchronized修飾的方法,會在該方法上添加ACC_SYNCHRONIZED的標識,表示它是一個同步方法。
monitorenter字節碼指令:
當程序執行到monitorenter指令時會嘗試去獲取當前鎖對象對應的monitor權限:
- 如果monitor的recursion爲0,則該線程進入monitor,然後將recursion設置爲1、owner設置爲當前線程,該線程成爲monitor的所有者;
- 如果線程已經佔用了該monitor(即判斷到recursion不爲0,然後看owner是否爲當前線程),說明這時候是持有鎖的線程再次獲取這個鎖,則可以獲取成功,將recursion加1
- 如果其他線程佔用了monitor,則該線程通過自旋操作再次嘗試幾次去獲取鎖,如果還沒有獲取到就被阻塞
monitorexit字節碼指令:
當執行到monitorexit指令時會將recursion的值減1,當這個值減到0的時候,當前線程就不再擁有這個monitor對象的所有權,就會釋放鎖,然後其他被這個鎖阻塞的線程就可以嘗試去獲取這個monitor對象。
從上面反彙編結果圖中看到還存在一個monitorexit指令,下面有一個Exception table,記錄的是有可能出現異常的指令。這就說明,同步代碼塊中如果出現了異常,那麼會執行到monitorexit指令將recursion減1。所以,synchronized出現異常時會釋放鎖。
當synchronized作用於方法上時,會給這個方法設置一個叫做ACC_SYNCHRONIZED
的標識。當一個方法被調用時,會檢測方法是否設置了ACC_SYNCHRONIZED
標識,如果設置了,線程會去獲取monitor,獲取成功之後才能執行同步方法體,執行完後釋放monitor所有權。
兩種同步方式本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過字節碼來完成。
monitor監視器鎖
monitor監視器鎖也就是通常說的synchronized對象鎖。任何一個Java對象都有一個Monitor與之關聯,Monitor對象存在於每個Java對象的對象頭Mark Word中(存儲的是Monitor對象的指針),這就是爲什麼Java中任意對象可以作爲鎖的原因。
一個Monitor被持有後,它將處於鎖定狀態。Monitor是由ObjectMonitor實現的,位於HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現的。
//ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,
//用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象 ),
//_owner指向持有ObjectMonitor對象的線程
ObjectMonitor() {
_header = NULL;
_count = 0; // 重入次數
_waiters = 0, // 等待線程數
_recursions = 0;
_object = NULL;
_owner = NULL; // 當前持有鎖的線程
_WaitSet = NULL; // 處於wait狀態的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 處於等待鎖block狀態的線程,會被加入到該列表,有資格成爲候選資源的線程
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
monitor競爭
當有多個線程競爭鎖時,流程如下:
monitor等待
當一個線程嘗試獲取鎖,如果鎖已經被其他線程獲取到了,它會再次自旋嘗試獲取鎖,如果還是獲取不到,則進行下面的流程:
第2步要通過CAS把node節點push到_cxq列表中,因爲一次push操作可能失敗
monitor釋放
- 執行完同步代碼快時會讓_recursions減1,當_recursions減爲0時,釋放該鎖
- 根據不同的策略喚醒等待該鎖的其他線程
synchronized是重量級鎖
Synchronized實現線程互斥會導致用戶態和內核態的切換,這種切換會消耗大量的系統資源,因爲用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態需要傳遞給許多變量、參數給內核,內核也需要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工作。所以說synchronized是重量級鎖,JDK6開始對Synchronized進行了鎖升級優化。
CAS
CAS(Compare And Swap)比較相同再交換,是現代CPU廣泛支持的一種對內存中的共享數據進行操作的一種特殊指令。
CAS的作用:CAS可以將比較和交換轉換爲原子操作,這個原子操作直接由CPU保證。CAS可以保證共享變量賦值時的原子操作。
在concurrent併發包下提供的AtomicInteger類就使用了CAS保證併發操作下對共享變量自增操作的正確性:
import java.util.concurrent.atomic.AtomicInteger;
public class Test {
private static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for(int i = 0; i < 10; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for(int j = 0; j < 1000; j++) {
// 自增操作
atomicInteger.incrementAndGet();
}
}
});
threads[i].start();
}
// join()的意思是等待所有的子線程都執行完了,主線程才繼續往後執行
for(Thread t : threads) {
t.join();
}
System.out.println(atomicInteger.get());//10000
}
}
CAS原理
CAS操作依賴3個值:內存中的值V,舊的估計值X,要修改的新值B,如果舊的預估值X等於內存中的值V,就將新的值B保存到內存中。
AtomicInteger和Unsafe部分源碼:
package java.util.concurrent.atomic;
public class AtomicInteger extends Number implements java.io.Serializable {
// ...
private static final long valueOffset; // 根據AtomicInteger對象的內存地址和偏移量valueOffset就能找到value的內存地址
private volatile int value; // 保存實際的值,用volatile修飾,保證可見性
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
// 調用了sun.misc.getAndAddInt()
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// ...
}
package sun.misc;
public final class Unsafe {
// ...
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5; // 舊的預估值,就是此時內存中的值
do {
// var1:AtomicInteger對象
// var2:AtomicInteger對象中的偏移量valueOffset
var5 = this.getIntVolatile(var1, var2);
//CAS操作,比較相同則交換一個int值
// var5+var4:要修改的值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
// ...
}
- 現在線程1和線程2都在對atomicInteger.value執行自增操作,假設線程2線先執行
- 假設現在atomicInteger.value是0,線程2執行到getAndAddInt()時,取到舊的預估值就是0
- 此時CPU切換執行線程1,線程1執行到getAndAddInt()時,取到舊的預估值也是0
- 線程1執行`compareAndSwapInt(var1, var2, var5, var5 + var4),var1和var2結合找到當前內存中的最新值【現在是0】,var5就是舊的預估值(之前取的內存中的值)【現在是0】,var4就是自增操作加1的這個【數值1】,現在比較內存最新值【0】和預估值【0】相等,則將var5+var4的值【1】賦給內存中的這個值。
- 線程1賦值成功,compareAndSwapInt()返回true,線程1結束
- 此時切換回線程2,找到當前內存最新值【1】,線程1舊的預估值還是【0】,比較兩者不相等,compareAndSwapInt()返回false,繼續執行do while循環,此時重新取內存中的值【1】給var5,然後再執行compareAndSwapInt()。此時找到當前內存最新值【1】,就的預估值【2】,兩者相等,則將var5+var4【2】賦給內存中的這個值
- 線程1賦值成功,compareAndSwapInt()返回true,線程1結束
- 最終,內存中的值是2
- 樂觀鎖:認爲讀多寫少,遇到併發寫的可能性很小,因此每次去拿數據的時候都認爲別的線程不會修改,就不會上鎖,但是在更新數據的時候會判斷當前數據是否被修改過了。
Java中的樂觀鎖基本都是通過CAS實現的- 悲觀鎖:認爲寫的情況很多,遇到併發寫的可能性高,因此每次去拿數據的時候都會認爲別的線程會修改這個數據,因此線程一上來執行就加鎖,直到執行完才釋放鎖
synchronized和ReentrantLock屬於悲觀鎖
CAS適用場景
- CAS獲取共享變量時,爲了保證該變量的可見性,需要使用volatile修飾,結合CAS和volatile可以實現無鎖併發,適用於多核CPU下線程間競爭不激烈的場景。
- 因爲沒有使用synchronized,所以線程不會陷入阻塞,這是效率提升的因素之一
- 但如果競爭激烈,那線程在getAndAddInt()中的do while循環處肯定會發生多次重試,反而會影響效率
synchronized鎖升級過程
HotSpot虛擬機中,JDK1.6之前,synchronized是重量級鎖,即使是線程交替執行無競爭併發的情況下,一個線程也要執行Synchronized加鎖,進行用戶態和內核態的切換。
JDK6開始對鎖進行了改進和優化,使得線程之間更高效地操作共享數據,以及解決競爭問題,從而提高程序運行效率。在JDK6中,synchronized鎖粒度是一個升級的過程:無鎖->偏向鎖->輕量級鎖-> 重量級鎖。
鎖存在四種狀態依次是:無鎖狀態(可偏向和不可偏向)、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖。
偏向鎖
HotSpot研究者發現,大多數情況下鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得與釋放,爲了讓線程獲得鎖的代價更低,引入了偏向鎖。偏向鎖就是這個鎖會偏向第一個獲得鎖的線程,在整個運行過程中只有一個線程的時候是這種鎖。
無鎖—>偏向鎖
- A線程訪問同步代碼塊,使用CAS操作將Thread ID放到鎖對象的Mark Word中
- 如果CAS操作成功,此時線程A獲取到鎖
- 如果CAS操作失敗,證明還有別的線程持有鎖,則啓動偏向鎖撤銷
偏向鎖—>撤銷
- 讓A線程在全局安全點阻塞
- 遍歷線程棧,查看是否有被鎖對象的鎖記錄( Lock Record),如果有Lock Record,需要修復鎖記錄和Markword,使其變成無鎖狀態
- 恢復A線程,將是否爲偏向鎖狀態置爲 0 ,開始進行輕量級加鎖流程
優缺點:
- 在單線程重複執行同步代碼塊時提升了性能,因爲如果只有一個線程執行同步代碼塊,就沒必要調用操作系統內核加鎖。
- 如果有很多線程競爭鎖,偏向鎖是無效的,還因爲撤銷偏向鎖的動作必須等待全局安全點纔行,反而降低了性能。
適用場景:
適用於單線程反覆進入同步代碼塊的情況。
JVM開啓/關閉偏向鎖:
- 開啓偏向鎖(JDK1.6之前):-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 開啓偏向鎖(JDK1.6及其之後):-XX:BiasedLockingStartupDelay=0
JDK6之後默認開啓,JDK1.8中默認會延遲4s後纔開啓偏向鎖,這是爲了提高JVM啓動速度,使用參數-XX:BiasedLockingStartupDelay=0
可以關閉延遲。聽說在JDK11中沒有延遲,可以自己驗證下哦! - 關閉偏向鎖:-XX:-UseBiasedLocking
參數-XX:+PrintFlagsInitial
打印出的信息中顯示了偏向鎖默認延遲時間(JDK1.8):
偏向鎖提升單線程反覆執行同步代碼快性能的原理之一:
按照HotSpot的設計,每次加鎖/解鎖都會涉及一些CAS操作,CAS操作會延遲本地調用。偏向鎖的做法是一旦線程獲得了這個鎖,這個線程之後再次執行執行獲取這個鎖是不用走加鎖/解鎖操作的,即只需要判斷當前是偏向鎖並且鎖的擁有者是它自己就行。
CAS爲什麼會延遲本地調用?
多核cpu、併發情況下,用volatile修飾的共享變量要保證可見性,假如此時core1和core2同時把一個共享變量拷貝到了自己的cpu緩存中,當core1修改了這個共享變量的值,通過總線寫回到主存的時候,通過總線嗅探機制,會使core2中對應的這個值失效,也就是將他的緩存清空,當core使用這個變量發現數據失效了,就會重新取主存中的這個變量,這種通過總線監聽來回通信稱爲“Cache 一致性流量”。core1和core2的值再一次相等時,稱爲“Cache一致性”。
而CAS剛好導致了Cache一致性流量情況加重,偏向鎖通過消除不必要的CAS降低了Cache一致性流。
輕量級鎖
在多線程交替執行同步代碼快的情況下(就是線程A執行完了線程B纔來執行,線程B執行完了線程C纔來執行…,多個線程之間不會有鎖競爭的情況),會使用輕量級鎖。如果多個線程在同一時刻進入臨界區,會導致輕量級鎖膨脹升級爲重量級鎖。
輕量級鎖加鎖過程:
- 在A線程棧幀中建立一個鎖記錄(Lock Record),將Mark Word拷貝到自己棧幀中的Lock Record中,這個位置叫 displayced hdr
- 將Lock Record中的owner指向鎖對象
- 使用CAS操作將Lock Record的地址記錄到Mark Word中,如果操作成功則進行下一步,否則進行最後一步
- CAS操作成功,那麼這個線程就獲取到了這個鎖,然後將鎖標誌位設爲輕量級鎖模式(00)
- CAS操作失敗,JVM首先會檢查鎖對象Mark Word中是否已經指向了當前棧幀,如果是則說明是鎖重入;否則說明多個線程競爭鎖,輕量級鎖升級爲重量級鎖,不過在這之前還有自旋操作
這裏爲什麼要使用CAS操作?
假如A、B兩個線程都將MarkWord拷貝到自己棧幀中的LockRecord中,A線程先將MarkWord更新爲指向自己LockRecord的指針,A線程就算獲取鎖成功了;B線程在執行CAS操作將MarkWord更新爲指向自己LockRecord的指針,發現MarkWord變了,CAS操作就會失敗,說明存在鎖競爭,則鎖開始膨脹。
輕量級鎖釋放過程:
- 取出Lock Record中保存的Mark Word信息,用CAS操作將取出的數據重新賦值到Mark Word中,操作成功,則釋放鎖成功
- 否則,說明其他線程嘗試獲取鎖,需要升級爲重量級鎖
輕量級鎖加鎖過程中爲什麼要把對象頭裏的Mark Word複製到線程棧的鎖記錄中?
因爲升級爲輕量級鎖是在多線程的情況下,這些線程可能會競爭鎖,那麼獲取到鎖的線程將自己棧幀中的LockRecord地址記錄到MarkWord中時要進行CAS操作,如果發現MarkWord中的值發生了變化,那CAS操作失敗,說明存在鎖競爭。
優點:
在多線程交替執行同步代碼快的情況下,可以避免重量級鎖引起的性能消耗。
自旋
重量級鎖的開銷很大,要儘量避免輕量級鎖轉爲重量級鎖。因此,當鎖升級爲輕量級鎖之後,如果依然有新線程過來競爭鎖,首先新線程會自旋嘗試獲取鎖,嘗試到一定次數依然沒有拿到,鎖就會升級爲重量級鎖。自旋鎖是JDK4中引入的,在JDK6中才默認開啓。
JVM開發團隊發現在很多應用中,共享數據的鎖定狀態只會持續很短的一段時間,如果爲了這麼短的一段時間使線程阻塞和喚醒導致的開銷不值得。先進行自旋,這個線程就不會放棄處理器執行時間而掛起。
自旋次數默認是10次,可以使用參數-XX:PreBlockSpin
來更改。這個自旋次數不好確定,在JDK6中引入了適應性自旋鎖。
適應性自旋鎖:
自適應意味着自旋次數不再固定,而是由前一個嘗試獲取這個鎖的線程自旋時間來決定。假如前一個線程自旋10次就獲得了鎖,JVM會認爲這個鎖很容易獲取,那麼當前這個線程也可以自旋10次或者再多幾次就能獲取到鎖。假如前面的線程自旋了很多次還沒有獲取到鎖,JVM會認爲這個鎖很難獲取,那以後要獲取這個鎖時就不再進行自旋過程,以免浪費資源。
monitor鎖的競爭過程就用到了自適應自旋鎖。
適用場景:
線程持有鎖的時間短,否則自旋時間長對CPU也會造成壓力。
重量級鎖
synchronized鎖是通過對象關聯的一個叫做監視器鎖(monitor)的對象來實現的,監視器鎖本質又是依賴於底層的操作系統Mutex Lock來實現,而操作系統實現線程之間的切換就需要用戶態和內核態的轉換,這個成本很高,狀態之間的轉換需要相對比較長的時間,這就是爲什麼Synchronized效率低的原因。因此,這種依賴於操作系統Mutex Lock所實現的鎖我們稱之爲 “重量級鎖”。
鎖消除
鎖消除是JDK6對鎖的優化。下面這段代碼,StringBuffer是線程安全的,append方法上加了synchronized關鍵字,但是對於new StringBuffer().append(s1).append(s2).append(s3).toString()
這行代碼,鎖對象是this,也就是StringBuffer的一個實例,每個線程執行到這句代碼時,都會實例化一個StringBuffer對象,它們的鎖和要鎖住的資源是不同的,因此也就沒必要在append方法上加鎖,因此JVM會自動將這個鎖消除。
鎖消除的依據是逃逸分析的數據支持
public class Test {
public static void main(String[] args) {
contactString("aa", "bb", "cc");
}
public static String contactString(String s1, String s2, String s3) {
return new StringBuffer().append(s1).append(s2).append(s3).toString();
}
}
鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的情況下,對鎖進行消除。
鎖粗化
下面這段代碼,for循環中,會進出100次append同步方法,JVM就會將append方法是的鎖消除,將鎖加到for循環上,這要很多鎖就變成了一個鎖。
public class Test {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
for(int i = 0; i < 100; i++) {
sb.append("aa");
}
}
}
鎖粗化是指JVM會探測到一連串細小的操作都使用同一個對象加鎖,將同步代碼塊的範圍放大,放到這串操作的外面,這樣只需加鎖一次即可。
示例
JDK8
參數:-XX:BiasedLockingStartupDelay=0
public class Test {
final static Object LOCK = new Object();
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
Thread t1 = new Thread() {
@Override
public void run() {
getLock();
}
};
t1.setName("t1");
t1.start();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread() {
@Override
public void run() {
getLock();
}
};
t2.setName("t2");
t2.start();
}
public static void getLock() {
synchronized(LOCK) {
System.out.println(Thread.currentThread().getName());
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
}
}
}
將線程1、2中間的sleep()註釋掉,兩線程就會競爭鎖,此時是重量級鎖,並且可以看到MarkWord中有,除了鎖標記(10)外,其餘52個bit也是相等的,其中記錄的就是鎖對象關聯的ObjectMonitor的地址:
public class Test {
final static Object LOCK = new Object();
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
System.out.println(Integer.toHexString(LOCK.hashCode()));
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
getLock();
}
public static void getLock() {
synchronized(LOCK) {
System.out.println(Thread.currentThread().getName());
System.out.println(ClassLayout.parseInstance(LOCK).toPrintable(LOCK));
}
}
}
synchronized小結
synchronized底層使用了monitorenter和monitorexit指令,每個鎖對象都會關聯一個monitor監視器,它有兩個主要屬性:owner記錄當前擁有鎖的線程,recursion記錄當前鎖被獲取的次數。當執行到monitorexit時,recursion會減1,當它的值減到0時,這個線程就會釋放鎖。
synchronized和Lock的區別
synchronized和Lock都可以用來解決多線程安全問題,保證線程同步。區別是:
- synchronized是關鍵字,Lock是接口,必須通過實例化一個實現了Lock鎖的接口才能得到Lock鎖對象,比如ReentrantLock
- synchronized發生異常時,會自動釋放鎖;Lock鎖必須通過調用unLock()去釋放,因此可能造成死鎖現象
- synchronized不能讓等待鎖的線程中斷;而Lock可以讓等待鎖的線程中斷,就是通過調用它的tryLock()方法,如果調用的是lock()方法,則是不可中斷的
- synchronized無法知道線程是否成功獲取鎖,而Lock可以,當設置爲可中斷鎖時,tryLock()方法返回布爾值代表是否獲得鎖
- synchronized能鎖住方法和代碼塊,加鎖和釋放鎖是由JVM自動完成的,Lock只能鎖住代碼塊,加鎖和釋放鎖的時機由程序員自己決定
- Lock可以使用讀鎖提高多線程的讀效率,讀鎖:Lock的一個實現類ReentrantReadWriteLock,允許多個線程讀,但只允許一個線程寫
- synchronized是非公平鎖,ReentrantLock可以控制是否是公平鎖。公平鎖就是喚醒線程的時候,哪個線程先來就先喚醒那個,非公平鎖是隨機喚醒一個線程。可以給ReentrantLock的構造器傳一個布爾值設定是否是公平鎖。
平時寫代碼如何對synchronized優化
- 減小synchronized的範圍,同步代碼塊中代碼執行時間儘量短
- 降低synchronized鎖的粒度
- 讀寫分離,讀取時不加鎖,寫入和刪除時加鎖