1.說一說自己對于 synchronized 關(guān)鍵字的了解
synchronized
關(guān)鍵字解決的是多個線程之間訪問資源的同步性,synchronized
關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執(zhí)行。
另外,在 Java 早期版本中,synchronized
屬于 重量級鎖,效率低下。
為什么呢?
因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock
來實現(xiàn)的,Java 的線程是映射到操作系統(tǒng)的原生線程之上的。如果要掛起或者喚醒一個線程,都需要操作系統(tǒng)幫忙完成,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高。
慶幸的是在 Java 6 之后 Java 官方對從 JVM 層面對 synchronized
較大優(yōu)化,所以現(xiàn)在的 synchronized
鎖效率也優(yōu)化得很不錯了。JDK1.6 對鎖的實現(xiàn)引入了大量的優(yōu)化,如自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術(shù)來減少鎖操作的開銷。
所以,你會發(fā)現(xiàn)目前的話,不論是各種開源框架還是 JDK 源碼都大量使用了 synchronized
關(guān)鍵字。
2. 說說自己是怎么使用 synchronized 關(guān)鍵字
synchronized 關(guān)鍵字最主要的三種使用方式:
1.修飾實例方法: 作用于當前對象實例加鎖,進入同步代碼前要獲得 當前對象實例的鎖
synchronized void method() {
//業(yè)務(wù)代碼
}
2.修飾靜態(tài)方法: 也就是給當前類加鎖,會作用于類的所有對象實例 ,進入同步代碼前要獲得 當前 class 的鎖。因為靜態(tài)成員不屬于任何一個實例對象,是類成員( static 表明這是該類的一個靜態(tài)資源,不管 new 了多少個對象,只有一份)。所以,如果一個線程 A 調(diào)用一個實例對象的非靜態(tài) synchronized
方法,而線程 B 需要調(diào)用這個實例對象所屬類的靜態(tài) synchronized
方法,是允許的,不會發(fā)生互斥現(xiàn)象,因為訪問靜態(tài) synchronized
方法占用的鎖是當前類的鎖,而訪問非靜態(tài) synchronized
方法占用的鎖是當前實例對象鎖。
synchronized void staic method() {
//業(yè)務(wù)代碼
}
3.修飾代碼塊 :指定加鎖對象,對給定對象/類加鎖。synchronized(this|object)
表示進入同步代碼庫前要獲得給定對象的鎖。synchronized(類.class)
表示進入同步代碼前要獲得 當前 class 的鎖
synchronized(this) {
//業(yè)務(wù)代碼
}
總結(jié):
-
synchronized
關(guān)鍵字加到static
靜態(tài)方法和synchronized(class)
代碼塊上都是是給 Class 類上鎖。 -
synchronized
關(guān)鍵字加到實例方法上是給對象實例上鎖。 - 盡量不要使用
synchronized(String a)
因為 JVM 中,字符串常量池具有緩存功能!
下面我以一個常見的面試題為例講解一下 synchronized
關(guān)鍵字的具體使用。
面試中面試官經(jīng)常會說:“單例模式了解嗎?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實現(xiàn)單例模式的原理唄!”
雙重校驗鎖實現(xiàn)對象單例(線程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判斷對象是否已經(jīng)實例過,沒有實例化過才進入加鎖代碼
if (uniqueInstance == null) {
//類對象加鎖
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
另外,需要注意 uniqueInstance
采用 volatile
關(guān)鍵字修飾也是很有必要。
uniqueInstance
采用 volatile
關(guān)鍵字修飾也是很有必要的, uniqueInstance = new Singleton();
這段代碼其實是分為三步執(zhí)行:
- 為
uniqueInstance
分配內(nèi)存空間 - 初始化
uniqueInstance
- 將
uniqueInstance
指向分配的內(nèi)存地址
但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2。指令重排在單線程環(huán)境下不會出現(xiàn)問題,但是在多線程環(huán)境下會導(dǎo)致一個線程獲得還沒有初始化的實例。例如,線程 T1 執(zhí)行了 1 和 3,此時 T2 調(diào)用 getUniqueInstance
() 后發(fā)現(xiàn) uniqueInstance
不為空,因此返回 uniqueInstance
,但此時 uniqueInstance
還未被初始化。
使用 volatile
可以禁止 JVM 的指令重排,保證在多線程環(huán)境下也能正常運行。
3. 構(gòu)造方法可以使用 synchronized 關(guān)鍵字修飾么?
先說結(jié)論:構(gòu)造方法不能使用 synchronized 關(guān)鍵字修飾。
構(gòu)造方法本身就屬于線程安全的,不存在同步的構(gòu)造方法一說。
4. 講一下 synchronized 關(guān)鍵字的底層原理
synchronized 關(guān)鍵字底層原理屬于 JVM 層面。
4.1. synchronized 同步語句塊的情況
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代碼塊");
}
}
}
通過 JDK 自帶的 javap
命令查看 SynchronizedDemo
類的相關(guān)字節(jié)碼信息:首先切換到類的對應(yīng)目錄執(zhí)行 javac SynchronizedDemo.java
命令生成編譯后的 .class 文件,然后執(zhí)行javap -c -s -v -l SynchronizedDemo.class
。
從上面我們可以看出:
synchronized
同步語句塊的實現(xiàn)使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代碼塊的開始位置,monitorexit
指令則指明同步代碼塊的結(jié)束位置。
當執(zhí)行 monitorenter
指令時,線程試圖獲取鎖也就是獲取 對象監(jiān)視器 monitor
的持有權(quán)。
在 Java 虛擬機(HotSpot)中,Monitor 是基于 C++實現(xiàn)的,由ObjectMonitor實現(xiàn)的。每個對象中都內(nèi)置了一個
ObjectMonitor
對象。另外,
wait/notify
等方法也依賴于monitor
對象,這就是為什么只有在同步的塊或者方法中才能調(diào)用wait/notify
等方法,否則會拋出java.lang.IllegalMonitorStateException
的異常的原因。
在執(zhí)行monitorenter
時,會嘗試獲取對象的鎖,如果鎖的計數(shù)器為 0 則表示鎖可以被獲取,獲取后將鎖計數(shù)器設(shè)為 1 也就是加 1。
在執(zhí)行 monitorexit
指令后,將鎖計數(shù)器設(shè)為 0,表明鎖被釋放。如果獲取對象鎖失敗,那當前線程就要阻塞等待,直到鎖被另外一個線程釋放為止。
4.2. synchronized 修飾方法的的情況
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized
修飾的方法并沒有 monitorenter
指令和 monitorexit
指令,取得代之的確實是 ACC_SYNCHRONIZED
標識,該標識指明了該方法是一個同步方法。JVM 通過該 ACC_SYNCHRONIZED
訪問標志來辨別一個方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。
4.3.總結(jié)
synchronized
同步語句塊的實現(xiàn)使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代碼塊的開始位置,monitorexit
指令則指明同步代碼塊的結(jié)束位置。
synchronized
修飾的方法并沒有 monitorenter
指令和 monitorexit
指令,取得代之的確實是 ACC_SYNCHRONIZED
標識,該標識指明了該方法是一個同步方法。
不過兩者的本質(zhì)都是對對象監(jiān)視器 monitor 的獲取。
5. 說說 JDK1.6 之后的 synchronized 關(guān)鍵字底層做了哪些優(yōu)化,可以詳細介紹一下這些優(yōu)化嗎
JDK1.6 對鎖的實現(xiàn)引入了大量的優(yōu)化,如偏向鎖、輕量級鎖、自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化等技術(shù)來減少鎖操作的開銷。
鎖主要存在四種狀態(tài),依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀態(tài)、重量級鎖狀態(tài),他們會隨著競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。
關(guān)于這幾種優(yōu)化的詳細信息可以查看下面這篇文章:Java6 及以上版本對 synchronized 的優(yōu)化
6. 談?wù)?synchronized 和 ReentrantLock 的區(qū)別
6.1. 兩者都是可重入鎖
“可重入鎖” 指的是自己可以再次獲取自己的內(nèi)部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次獲取鎖,鎖的計數(shù)器都自增 1,所以要等到鎖的計數(shù)器下降為 0 時才能釋放鎖。
6.2.synchronized 依賴于 JVM 而 ReentrantLock 依賴于 API
synchronized
是依賴于 JVM 實現(xiàn)的,前面我們也講到了 虛擬機團隊在 JDK1.6 為 synchronized
關(guān)鍵字進行了很多優(yōu)化,但是這些優(yōu)化都是在虛擬機層面實現(xiàn)的,并沒有直接暴露給我們。ReentrantLock
是 JDK 層面實現(xiàn)的(也就是 API 層面,需要 lock() 和 unlock() 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實現(xiàn)的。
6.3.ReentrantLock 比 synchronized 增加了一些高級功能
相比synchronized
,ReentrantLock
增加了一些高級功能。主要來說主要有三點:
-
等待可中斷 :
ReentrantLock
提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()
來實現(xiàn)這個機制。也就是說正在等待的線程可以選擇放棄等待,改為處理其他事情。 -
可實現(xiàn)公平鎖 :
ReentrantLock
可以指定是公平鎖還是非公平鎖。而synchronized
只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。ReentrantLock
默認情況是非公平的,可以通過ReentrantLock
類的ReentrantLock(boolean fair)
構(gòu)造方法來制定是否是公平的。 -
可實現(xiàn)選擇性通知(鎖可以綁定多個條件):
synchronized
關(guān)鍵字與wait()
和notify()
/notifyAll()
方法相結(jié)合可以實現(xiàn)等待/通知機制。ReentrantLock
類當然也可以實現(xiàn),但是需要借助于Condition
接口與newCondition()
方法。
Condition
是 JDK1.5 之后才有的,它具有很好的靈活性,比如可以實現(xiàn)多路通知功能也就是在一個Lock
對象中可以創(chuàng)建多個Condition
實例(即對象監(jiān)視器),線程對象可以注冊在指定的Condition
中,從而可以有選擇性的進行線程通知,在調(diào)度線程上更加靈活。 在使用notify()/notifyAll()
方法進行通知時,被通知的線程是由 JVM 選擇的,用ReentrantLock
類結(jié)合Condition
實例可以實現(xiàn)“選擇性通知” ,這個功能非常重要,而且是 Condition 接口默認提供的。而synchronized
關(guān)鍵字就相當于整個 Lock 對象中只有一個Condition
實例,所有的線程都注冊在它一個身上。如果執(zhí)行notifyAll()
方法的話就會通知所有處于等待狀態(tài)的線程這樣會造成很大的效率問題,而Condition
實例的signalAll()
方法 只會喚醒注冊在該Condition
實例中的所有等待線程。文章來源:http://www.zghlxwxcb.cn/news/detail-635682.html
如果你想使用上述功能,那么選擇 ReentrantLock 是一個不錯的選擇。性能已不是選擇標準文章來源地址http://www.zghlxwxcb.cn/news/detail-635682.html
到了這里,關(guān)于并發(fā)——synchronized 關(guān)鍵字的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!