前言
????面試題:synchronized是可重入鎖嗎戳玫?
????答案:synchronized是可重入鎖。ReentrantLock也是的未斑。
1咕宿、什么是可重入鎖呢?
????關(guān)于什么是可重入鎖蜡秽,我們先來(lái)看一段維基百科的定義府阀。
若一個(gè)程序或子程序可以“在任意時(shí)刻被中斷然后操作系統(tǒng)調(diào)度執(zhí)行另外一段代碼,這段代碼又調(diào)用了該子程序不會(huì)出錯(cuò)”芽突,則稱(chēng)其為可重入(reentrant或re-entrant)的试浙。即當(dāng)該子程序正在運(yùn)行時(shí),執(zhí)行線程可以再次進(jìn)入并執(zhí)行它寞蚌,仍然獲得符合設(shè)計(jì)時(shí)預(yù)期的結(jié)果田巴。與多線程并發(fā)執(zhí)行的線程安全不同,可重入強(qiáng)調(diào)對(duì)單個(gè)線程執(zhí)行時(shí)重新進(jìn)入同一個(gè)子程序仍然是安全的挟秤。
????通俗來(lái)說(shuō):當(dāng)線程請(qǐng)求一個(gè)由其它線程持有的對(duì)象鎖時(shí)壹哺,該線程會(huì)阻塞,而當(dāng)線程請(qǐng)求由自己持有的對(duì)象鎖時(shí)艘刚,如果該鎖是重入鎖管宵,請(qǐng)求就會(huì)成功,否則阻塞攀甚。
????再換句話說(shuō):可重入就是說(shuō)某個(gè)線程已經(jīng)獲得某個(gè)鎖箩朴,可以再次獲取鎖而不會(huì)出現(xiàn)死鎖。
2秋度、自己寫(xiě)代碼驗(yàn)證下可重入和不可重入
????我們啟動(dòng)一個(gè)線程t1炸庞,調(diào)用addOne()方法來(lái)執(zhí)行加1操作。在addOne方法里面t1會(huì)獲得rtl鎖静陈,然后調(diào)用get()方法燕雁,在get()方法里再次請(qǐng)求獲取trl鎖。
????因?yàn)樽罱K能打印value=1鲸拥,說(shuō)明t1在第二次獲取鎖的時(shí)候并沒(méi)有阻塞。說(shuō)明ReentrantLock是可重入鎖刑赶。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantTest {
private final Lock rtl = new ReentrantLock();
int value = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantTest test = new ReentrantTest();
// 新建一個(gè)線程 進(jìn)行加1操作
Thread t1 = new Thread(() -> test.addOne());
t1.start();
// main線程等待t1線程執(zhí)行完
t1.join();
System.out.println(test.value);
}
public int get() {
// 獲取鎖
rtl.lock();
try {
return value;
} finally {
// 保證鎖能釋放
rtl.unlock();
}
}
public void addOne() {
// 獲取鎖
rtl.lock();
try {
value = 1 + get();
} finally {
// 保證鎖能釋放
rtl.unlock();
}
}
}
????換成synchronized的加鎖方式捏浊,同樣能打印value的值。證明synchronized也是可重入鎖撞叨。
public class ReentrantTest {
private final Object object = new Object();
int value = 0;
public static void main(String[] args) throws InterruptedException {
ReentrantTest test = new ReentrantTest();
// 新建一個(gè)線程 進(jìn)行加1操作
Thread t1 = new Thread(() -> test.addOne());
t1.start();
t1.join();
System.out.println(test.value);
}
public int get() {
// 再此獲取鎖
synchronized (object) {
return value;
}
}
public void addOne() {
// 獲取鎖
synchronized (object) {
value = 1 + get();
}
}
}
3金踪、自己如何實(shí)現(xiàn)一個(gè)可重入和不可重入鎖呢
不可重入:
public class Lock{
private boolean isLocked = false;
public synchronized void lock()
throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
}
public synchronized void unlock(){
isLocked = false;
notify();
}
}
可重入:
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock() throws InterruptedException{
Thread callingThread = Thread.currentThread();
while(isLocked && lockedBy != callingThread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = callingThread;
}
public synchronized void unlock(){
if(Thread.curentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
????從代碼實(shí)現(xiàn)來(lái)看,可重入鎖增加了兩個(gè)狀態(tài)牵敷,鎖的計(jì)數(shù)器和被鎖的線程胡岔,實(shí)現(xiàn)基本上和不可重入的實(shí)現(xiàn)一樣,如果不同的線程進(jìn)來(lái)枷餐,這個(gè)鎖是沒(méi)有問(wèn)題的靶瘸,但是如果進(jìn)行遞歸計(jì)算的時(shí)候,如果加鎖毛肋,不可重入鎖就會(huì)出現(xiàn)死鎖的問(wèn)題怨咪。
4、ReentrantLock如何實(shí)現(xiàn)可重入的
使用ReentrantLock你要知道:
ReentrantLock支持公平和非公平2種創(chuàng)建方式润匙,默認(rèn)創(chuàng)建的是非公平模式的鎖诗眨。
看下它的構(gòu)造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
看下非公平鎖,它是繼承抽象類(lèi)Sync的:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
看下公平鎖孕讳,它也是繼承抽象類(lèi)Sync的:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
????NonfairSync匠楚、FairSync 和抽象類(lèi)Sync 都是ReentrantLock的內(nèi)部類(lèi)。
????Sync的定義厂财,它是繼承AbstractQueuedSynchronizer的油啤,AbstractQueuedSynchronizer既是我們常說(shuō)的AQS(后面我也會(huì)整理一篇)
abstract static class Sync extends AbstractQueuedSynchronizer {
}
????好了,繼承關(guān)系清楚了 蟀苛,現(xiàn)在我們看下ReentrantLock是如何實(shí)現(xiàn)可重入的
????我們?cè)赼ddOne()和get()兩個(gè)方法加鎖的地方都打上斷點(diǎn)益咬。然后開(kāi)始調(diào)式:
-
addOne方法獲取鎖的時(shí)候走到NonfairSync的“compareAndSetState(0, 1)”,通過(guò)CAS設(shè)置state的值為1帜平,調(diào)用成功幽告,并設(shè)置當(dāng)前鎖被持有的線程為當(dāng)前線程t1;
-
繼續(xù)調(diào)試,get方法獲取鎖的時(shí)候走到NonfairSync的“compareAndSetState(0, 1)”裆甩,通過(guò)CAS設(shè)置state的值為1冗锁,調(diào)用失敗(因?yàn)橐呀?jīng)被當(dāng)前線程t1鎖占有),走到else里面嗤栓,繼續(xù)往里看冻河。走到NonfairSync的tryAcquire方法箍邮,再往里走;
會(huì)調(diào)用Sync抽象類(lèi)里面的nonfairTryAcquire方法。源碼解釋我都寫(xiě)在下面了叨叙。
final boolean nonfairTryAcquire(int acquires) {
// 當(dāng)前線程
final Thread current = Thread.currentThread();
// state變量的值
int c = getState();
// 因?yàn)閏當(dāng)前值為1锭弊,所以走else里面
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 判斷當(dāng)前線程 是不是 當(dāng)前鎖被持有的線程 ,判斷為 true
else if (current == getExclusiveOwnerThread()) {
// c + acquires = 1 + 1 = 2
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);// 將state的值賦值為2
return true;
}
return false;
}
????到此擂错,可重入鎖加鎖的過(guò)程分析完畢味滞。解鎖的過(guò)程一樣,希望你能自己debug下【調(diào)用的是Sync抽象類(lèi)里面的tryRelease方法】
????我這里總結(jié)一下:
當(dāng)線程嘗試獲取鎖時(shí)钮呀,可重入鎖先嘗試獲取并更新state值
如果state == 0表示沒(méi)有其他線程在執(zhí)行同步代碼剑鞍,則通過(guò)CAS把state置為1 會(huì)成功,當(dāng)前線程繼續(xù)執(zhí)行爽醋。
如果status != 0蚁署,通過(guò)CAS把state置為1 會(huì)失敗,然后判斷當(dāng)前線程是否是獲取到這個(gè)鎖的線程蚂四,如果是的話執(zhí)行state+1形用,且當(dāng)前線程可以再次獲取鎖。釋放鎖時(shí)证杭,可重入鎖同樣先獲取當(dāng)前state的值田度,在當(dāng)前線程是持有鎖的線程的前提下。
如果status-1 == 0解愤,則表示當(dāng)前線程所有重復(fù)獲取鎖的操作都已經(jīng)執(zhí)行完畢镇饺,然后該線程才會(huì)真正釋放鎖。
????你需要注意的是state變量的定義送讲,其實(shí)AQS的實(shí)現(xiàn)類(lèi)都是通過(guò)控制state的值來(lái)控制鎖的狀態(tài)的奸笤。它被volatile所修飾,能保證可見(jiàn)性哼鬓。
private volatile int state;
????擴(kuò)展:如果要通過(guò)AQS的state來(lái)實(shí)現(xiàn)非可重入鎖怎么實(shí)現(xiàn)呢监右?明確這兩點(diǎn)就可以了:
- 獲取鎖時(shí):去獲取并嘗試更新當(dāng)前status的值,如果status != 0的話會(huì)導(dǎo)致其獲取鎖失敗异希,當(dāng)前線程阻塞健盒。
- 釋放鎖時(shí):在確定當(dāng)前線程是持有鎖的線程之后,直接將status置為0称簿,將鎖釋放扣癣。
5、可重入鎖的特點(diǎn)
????可重入鎖的一個(gè)優(yōu)點(diǎn)是可一定程度避免死鎖憨降。
????可重入鎖能避免一定線程的等待父虑,可想而知可重入鎖性能會(huì)高于非可重入鎖木西。你可以寫(xiě)程序測(cè)試一下哦V恕!热押!
滬漂程序員一枚膜毁。
堅(jiān)持寫(xiě)博客步绸,如果覺(jué)得還可以的話悴晰,給個(gè)小星星哦溯革,你的支持就是我創(chuàng)作的動(dòng)力。
個(gè)人微信公眾號(hào):“Java尖子生”膳殷,閱讀更多干貨。
<font color='red'>關(guān)注公眾號(hào)九火,領(lǐng)取學(xué)習(xí)赚窃、面試資料。加技術(shù)討論群岔激。</font>