詳解 JUC 包下的鎖
本文將針對(duì)JUC包下常見的鎖進(jìn)行深入分析和演示,希望對(duì)你有幫助。
一、Java中的鎖
我們?nèi)粘i_發(fā)過程中為了保證臨界資源的線程安全可能會(huì)用到synchronized,但是synchronized局限性也是很強(qiáng)的,它無法做到以下幾點(diǎn):
- 讓當(dāng)前線程立刻釋放鎖。
- 判斷線程持有鎖的狀態(tài)。
- 線程爭(zhēng)搶的公平爭(zhēng)搶。
所以為了保證用戶能夠在合適的場(chǎng)景找到合適的鎖,Java設(shè)計(jì)者按照不同的維度為我們提供了各種鎖,鎖的分類按照不同的特征分為以下幾種:
二、Lock接口基本思想和規(guī)范
1. 為什么需要Lock接口式的鎖
鎖是一種解決資源共享問題的解決方案,相比于synchronized鎖,Lock鎖的自類增加了一些更高級(jí)的功能:
- 鎖等待。
- 鎖中斷。
- 可隨時(shí)中斷釋放。
- 鎖重入。
但這并不能表明,Lock鎖是synchronized鎖的替代品,它倆都有各自的適用場(chǎng)合。
2. Lock接口的基本規(guī)范
宏觀角度來看Lock接口不僅支持簡(jiǎn)單的上鎖和釋放鎖,還支持超時(shí)等待鎖、上可中斷鎖,鎖中斷等操作:
public interface Lock {
//上鎖
void lock();
//上一把可中斷的鎖
void lockInterruptibly() throws InterruptedException;
//非阻塞嘗試取鎖
boolean tryLock();
//超時(shí)等待鎖
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//鎖釋放
void unlock();
//......
}
3. 使用Lock的優(yōu)雅姿勢(shì)
我們以ReentrantLock來演示一下Lock類的加鎖和解鎖操作。細(xì)心的讀者在閱讀源碼時(shí)可能會(huì)發(fā)現(xiàn)下面這樣一段注釋,這就是lock類上鎖的解鎖的基本示例了。
* <pre> {@code
* class X {
* private final ReentrantLock lock = new ReentrantLock();
* // ...
*
* public void m() {
* lock.lock(); // block until condition holds
* try {
* // ... method body
* } finally {
* lock.unlock()
* }
* }
* }}
所以我們也按照上面這段示例編寫一下一段demo代碼。注意lock鎖必須手動(dòng)釋放,所以為了保證釋放的安全我們常常會(huì)在finally語句塊中進(jìn)行鎖釋放,如官方給出的代碼示例一樣:
ReentrantLock lock = new ReentrantLock();
//上鎖
lock.lock();
try {
System.out.println("當(dāng)前線程" + Thread.currentThread().getName() + "獲得鎖,進(jìn)行異常操作");
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
} finally {
//語句塊中優(yōu)雅釋放
lock.unlock();
}
log.info("當(dāng)前鎖是否被鎖定:{}", lock.isLocked());
對(duì)應(yīng)的我們也給出輸出結(jié)果:
當(dāng)前線程main獲得鎖,進(jìn)行異常操作
15:41:05.499 [main] INFO com.sharkChili.Main - 當(dāng)前鎖是否被鎖定:false
java.lang.ArithmeticException: / by zero
at com.sharkChili.Main.main(Main.java:23)
4. tryLock
相比于普通的lock來說,tryLock相對(duì)更加強(qiáng)大一些,tryLock可以根據(jù)當(dāng)前線程是否取得鎖進(jìn)行一些定制化操作。 而且tryLock可以立即返回或者在一定時(shí)間內(nèi)取鎖,如果拿得到就拿鎖并返回true,反之返回false。
我們現(xiàn)在創(chuàng)建一個(gè)任務(wù)給兩個(gè)線程使用,邏輯很簡(jiǎn)單,在每個(gè)線程在while循環(huán)中,flag為1的先取鎖1,flag為2的先取鎖2。 flag為1的先在規(guī)定時(shí)間內(nèi)獲取鎖1,獲得鎖1后再獲取鎖2,如果鎖2獲取失敗則釋放鎖1休眠一會(huì)。讓另一個(gè)先獲取鎖2在獲取鎖1的線程執(zhí)行完再進(jìn)行獲取鎖。
public class TryLockDemo implements Runnable {
//注意使用static 否則鎖的粒度用錯(cuò)了會(huì)導(dǎo)致無法鎖住彼此
private static Lock lock1 = new ReentrantLock();
private static Lock lock2 = new ReentrantLock();
//flag為1的先取鎖1再去鎖2,反之先取鎖2在取鎖1
private int flag;
public int getFlag() {
return flag;
}
public void setFlag(int flag) {
this.flag = flag;
}
@Override
public void run() {
while (true) {
//flag為1先取鎖1再取鎖2
if (flag == 1) {
try {
//800ms內(nèi)嘗試取鎖,如果失敗則直接輸出嘗試獲取鎖1失敗
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName()+"拿到了第一把鎖lock1");
//睡一會(huì),保證線程2拿鎖鎖2
Thread.sleep(new Random().nextInt(1000));
if (lock2.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName()+"取到鎖2");
System.out.println(Thread.currentThread().getName()+"拿到兩把鎖,執(zhí)行業(yè)務(wù)邏輯了。。。。");
break;
} finally {
lock2.unlock();
}
} else {
System.out.println(Thread.currentThread().getName()+"獲取第二把鎖鎖2失敗");
}
} finally {
//休眠一會(huì)再次獲取鎖
lock1.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println(Thread.currentThread().getName()+"嘗試獲取鎖1失敗");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
try {
//3000ms內(nèi)嘗試獲取鎖2,如果娶不到直接輸出失敗
if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
try{
System.out.println(Thread.currentThread().getName()+"先拿到了鎖2");
Thread.sleep(new Random().nextInt(1000));
if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
try {
System.out.println(Thread.currentThread().getName()+"取到鎖1");
System.out.println(Thread.currentThread().getName()+"拿到兩把鎖,執(zhí)行業(yè)務(wù)邏輯了。。。。");
break;
} finally {
lock1.unlock();
}
} else {
System.out.println(Thread.currentThread().getName()+"獲取第二把鎖1失敗");
}
}finally {
//休眠一會(huì),順便把鎖釋放讓其他線程獲取
lock2.unlock();
Thread.sleep(new Random().nextInt(1000));
}
} else {
System.out.println(Thread.currentThread().getName()+"獲取鎖2失敗");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
測(cè)試代碼:
public class TestTryLock {
public static void main(String[] args) {
//先獲取鎖1
TryLockDemo t1 = new TryLockDemo();
t1.setFlag(1);
//先獲取鎖2
TryLockDemo t2 = new TryLockDemo();
t2.setFlag(2);
new Thread(t1,"t1").start();
new Thread(t2,"t2").start();
}
}
輸出結(jié)果如下,可以看到tryLock的存在使得我們可以不再阻塞的去獲取鎖,而是可以根據(jù)鎖的持有情況進(jìn)行下一步邏輯。
t1拿到了第一把鎖lock1
t2先拿到了鎖2
t1獲取第二把鎖鎖2失敗
t2取到鎖1
t2拿到兩把鎖,執(zhí)行業(yè)務(wù)邏輯了。。。。
t1拿到了第一把鎖lock1
t1取到鎖2
t1拿到兩把鎖,執(zhí)行業(yè)務(wù)邏輯了。。。。
5. 可被中斷的lock
為避免synchronized這種獲取鎖過程無法中斷,進(jìn)而出現(xiàn)死鎖的情況。JUC包下的鎖提供了lockInterruptibly方法,即在獲取鎖過程中的線程可以被打斷。
public class LockInterruptiblyDemo implements Runnable {
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 嘗試取鎖");
try {
//設(shè)置為可被中斷的獲取鎖
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 取鎖成功");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 執(zhí)行業(yè)務(wù)邏輯時(shí)被中斷");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "嘗試取鎖時(shí)被中斷");
}
}
}
測(cè)試代碼如下,我們先讓線程1獲取鎖成功,此時(shí)線程2取鎖就會(huì)失敗,我們可以手動(dòng)通過interrupt將其打斷。
public class LockInterruptiblyTest {
public static void main(String[] args) throws InterruptedException {
LockInterruptiblyDemo lockInterruptiblyDemo = new LockInterruptiblyDemo();
//線程1先獲取鎖,會(huì)成功
Thread thread0 = new Thread(lockInterruptiblyDemo);
thread0.start();
//線程2獲取鎖失敗,不會(huì)中斷
Thread thread1 = new Thread(lockInterruptiblyDemo);
thread1.start();
Thread.sleep(5000);
//手動(dòng)調(diào)用interrupt將線程中斷
thread1.interrupt();
}
}
6. Lock鎖的可見性保證
可能很多人會(huì)對(duì)這些操作有這樣的疑問,我們lock的結(jié)果如何對(duì)之后操作該資源的線程保證可見性呢?
其實(shí)根據(jù)happens-before原則,前一個(gè)線程操作的結(jié)果,對(duì)后一個(gè)線程是都可見的原理即可保證鎖操作的可見性。
三、以不同分類的維度解析鎖
1. 按照是否鎖住資源分類
(1) 悲觀鎖
悲觀鎖認(rèn)為自己在修改數(shù)據(jù)過程中,其他人很可能會(huì)過來修改數(shù)據(jù),為了保證數(shù)據(jù)的準(zhǔn)確性,他會(huì)在自己修改數(shù)據(jù)時(shí)候持有鎖,在釋放鎖之前,其他線程是無法持有這把鎖。在Java中synchronized鎖和lock鎖都是典型的悲觀鎖。
對(duì)應(yīng)的我們給出悲觀鎖的使用示例:
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(2);
Thread t1 = new Thread(()->{
doSomething();
countDownLatch.countDown();
});
Thread t2 = new Thread(()->{
doSomething();
countDownLatch.countDown();
});
t1.start();
t2.start();
countDownLatch.await();
}
/**
* synchronized 悲觀鎖,保證上鎖成功后才能操作臨界資源
*/
public synchronized static void doSomething() {
log.info("{} do something", Thread.currentThread().getName());
}
(2) 樂觀鎖
樂觀鎖認(rèn)為自己的修改數(shù)據(jù)時(shí)不會(huì)有其他人會(huì)修改數(shù)據(jù),所以他每次修改數(shù)據(jù)后會(huì)判斷修改前的數(shù)據(jù)是否被修改過,如果沒有就將更新結(jié)果寫入,反之重新拉取數(shù)據(jù)的最新結(jié)果進(jìn)行更新再重復(fù)之前步驟完成寫入,在Java中樂觀鎖常常用CAS原子類來實(shí)現(xiàn):
如下代碼所示,原子類就是通過CAS樂觀鎖實(shí)現(xiàn)的:
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();
}
我們可以看看cas原子類getAndIncrement的源碼,它會(huì)調(diào)用unsafe的getAndAddInt,將this和偏移量,還有1傳入。
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
查看getAndAddInt的工作流程我們即可知曉CAS樂觀鎖操作的實(shí)現(xiàn)細(xì)節(jié):
- 通過getIntVolatile方法獲取到需要操作的變量的地址。
- 通過compareAndSwapInt的方式查看原有的值是否發(fā)生變化,如果沒有則將修改后的結(jié)果v + delta寫入到變量地址空間中。
- 如果發(fā)生變化則compareAndSwapInt會(huì)返回false,繼續(xù)從步驟1開始,直到修改操作成功。
對(duì)應(yīng)的我們給出getAndAddInt的源碼實(shí)現(xiàn):
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
//拉取操作變量最新值
v = getIntVolatile(o, offset);
//比對(duì)拉取結(jié)果與最新結(jié)果是否一致,若一致則寫入最新結(jié)果,反之繼續(xù)循環(huán)直到修改成功
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
(3) 悲觀鎖和樂觀鎖的比較
該問題我們可以從以下兩個(gè)角度進(jìn)行說明:
- 從資源開銷的角度:悲觀鎖的開銷遠(yuǎn)高于樂觀鎖,但它確實(shí)一勞永逸的,臨界區(qū)持有鎖的時(shí)間就算越來越長(zhǎng)也不會(huì)對(duì)互斥鎖有任何的影響。反之樂觀鎖假如持有鎖的時(shí)間越來越長(zhǎng)的話,其他等待線程的自選時(shí)間也會(huì)增加,從而導(dǎo)致資源消耗愈發(fā)嚴(yán)重。
- 從場(chǎng)景適用角度:悲觀更適合那些經(jīng)常操作修改的場(chǎng)景,而樂觀鎖更適合讀多修改少的情況。
2. 按照是否可重入進(jìn)行鎖分類
(1) 可重入鎖示例
代碼如下所示,我們創(chuàng)建一個(gè)MyRecursionDemo ,這個(gè)類的邏輯很簡(jiǎn)單,讓當(dāng)前線程通過遞歸的方式連續(xù)獲得鎖5次。
public class MyRecursionDemo {
private ReentrantLock lock = new ReentrantLock();
public void accessResource() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 第" + lock.getHoldCount() + "次處理資源中");
if (lock.getHoldCount() < 5) {
System.out.println("當(dāng)前線程是否是持有這把鎖的線程" + lock.isHeldByCurrentThread());
System.out.println("當(dāng)前等待隊(duì)列長(zhǎng)度" + lock.getQueueLength());
System.out.println("再次遞歸處理資源中........................................");
//再次遞歸調(diào)用該方法,嘗試重入這把鎖
accessResource();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("處理結(jié)束,釋放可重入鎖");
lock.unlock();
}
}
}
測(cè)試代碼:
public class MyRecursionDemoTest {
public static void main(String[] args) {
MyRecursionDemo myRecursinotallow=new MyRecursionDemo();
myRecursionDemo.accessResource();
}
}
從輸出結(jié)果來看main線程第一次成功取鎖之后,在不釋放的情況下,連續(xù)嘗試取ReentrantLock 5次都是成功的,是支持可重入的。
main 第1次處理資源中
當(dāng)前線程是否是持有這把鎖的線程true
當(dāng)前等待隊(duì)列長(zhǎng)度0
再次遞歸處理資源中........................................
main 第2次處理資源中
當(dāng)前線程是否是持有這把鎖的線程true
當(dāng)前等待隊(duì)列長(zhǎng)度0
再次遞歸處理資源中........................................
main 第3次處理資源中
當(dāng)前線程是否是持有這把鎖的線程true
當(dāng)前等待隊(duì)列長(zhǎng)度0
再次遞歸處理資源中........................................
main 第4次處理資源中
當(dāng)前線程是否是持有這把鎖的線程true
當(dāng)前等待隊(duì)列長(zhǎng)度0
再次遞歸處理資源中........................................
main 第5次處理資源中
處理結(jié)束,釋放可重入鎖
處理結(jié)束,釋放可重入鎖
處理結(jié)束,釋放可重入鎖
處理結(jié)束,釋放可重入鎖
處理結(jié)束,釋放可重入鎖
(2) 不可重入鎖
NonReentrantLock就是典型的不可重入鎖,代碼示例如下:
public class NonReentrantLockDemo {
public static void main(String[] args) {
NonReentrantLock lock=new NonReentrantLock();
lock.lock();
System.out.println(Thread.currentThread().getName()+"第一次獲取鎖成功");
lock.lock();
System.out.println(Thread.currentThread().getName()+"第二次獲取鎖成功");
}
}
從輸出結(jié)果來看,第一次獲取鎖之后就無法再次重入鎖了。
main第一次獲取鎖成功
(3) 源碼解析可重入鎖和非可重入鎖區(qū)別
查看ReentrantLock可重入鎖源碼可知,可重入鎖進(jìn)行鎖定邏輯時(shí),會(huì)判斷持有鎖的線程是否是當(dāng)前線程,如果是則將c(即count的縮寫)自增:
final boolean nonfairTryAcquire(int acquires) {
.....
//如果當(dāng)前線程仍然持有這把鎖,記錄一下持有鎖的次數(shù) 并返回拿鎖成功
else if (current == getExclusiveOwnerThread()) {
//增加上鎖次數(shù)
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//更新當(dāng)前鎖上鎖次數(shù)
setState(nextc);
return true;
}
return false;
}
相比之下不可重入鎖的邏輯就比較簡(jiǎn)單了,如下源碼NonReentrantLock所示,通過CAS修改取鎖狀態(tài),若成功則將鎖持有者設(shè)置為當(dāng)前線程。 同一個(gè)線程再去取鎖時(shí)并沒有重入的處理,仍然是進(jìn)行CAS操作,很明顯這種情況是會(huì)失敗的。
@Override
protected final boolean tryAcquire(int acquires) {
// 通過CAS修改鎖狀態(tài)
if (compareAndSetState(0, 1)) {
//若成功則將鎖持有者設(shè)置為當(dāng)前線程
owner = Thread.currentThread();
return true;
}
return false;
}
3. 按照公平性進(jìn)行鎖分類
公平鎖可以保證線程持鎖順序會(huì)有序進(jìn)行,在線程爭(zhēng)搶鎖的過程中如果上鎖失敗是會(huì)統(tǒng)一提交到等待隊(duì)列中,后續(xù)由隊(duì)列統(tǒng)一管理喚醒:
非公平鎖的設(shè)計(jì)初衷也很明顯,非公平鎖的設(shè)計(jì)就是為了在線程喚醒期間的空檔期讓其他線程可以插隊(duì),所以即使等待隊(duì)列中有線程,其他的不在隊(duì)列中的線程依然可以持有這把鎖:
(1) 公平鎖代碼示例
我們先創(chuàng)建一個(gè)任務(wù)類的代碼,run方法邏輯很簡(jiǎn)單,上一次鎖打印輸出一個(gè)文件,這里會(huì)上鎖兩次打印兩次。構(gòu)造方法中要求傳一個(gè)布爾值,這個(gè)布爾值如果為true則說明ReentrantLock 為公平,反之為非公平。
public class MyPrintQueue implements Runnable {
private boolean fair;
public MyPrintQueue(boolean fair) {
this.fair = fair;
}
/**
* true為公平鎖 false為非公平鎖
*/
private ReentrantLock lock = new ReentrantLock(fair);
/**
* 上鎖兩次打印輸出兩個(gè)文件
*/
public void printStr() {
lock.lock();
try {
int s = new Random().nextInt(10) + 1;
System.out.println("正在打印第一份文件。。。。當(dāng)前打印線程:" + Thread.currentThread().getName() + " 需要" + s + "秒");
Thread.sleep(s * 1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
lock.lock();
try {
int s = new Random().nextInt(10) + 1;
System.out.println("正在打印第二份文件。。。。當(dāng)前打印線程:" + Thread.currentThread().getName() + " 需要" + s + "秒");
Thread.sleep(s * 1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
@Override
public void run() {
printStr();
}
}
測(cè)試代碼:
public class FairLockTest {
public static void main(String[] args) {
//創(chuàng)建10個(gè)線程分別執(zhí)行這個(gè)任務(wù)
MyPrintQueue task=new MyPrintQueue(true);
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(task);
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
try{
Thread.sleep(100);
}catch (Exception e){
}
}
}
}
從輸出結(jié)果來看,線程是按順序執(zhí)行的:
正在打印第一份文件。。。。當(dāng)前打印線程:Thread-0 需要2秒
正在打印第二份文件。。。。當(dāng)前打印線程:Thread-0 需要8秒
正在打印第一份文件。。。。當(dāng)前打印線程:Thread-1 需要1秒
正在打印第二份文件。。。。當(dāng)前打印線程:Thread-1 需要8秒
正在打印第一份文件。。。。當(dāng)前打印線程:Thread-2 需要2秒
正在打印第二份文件。。。。當(dāng)前打印線程:Thread-2 需要9秒
正在打印第一份文件。。。。當(dāng)前打印線程:Thread-3 需要10秒
正在打印第二份文件。。。。當(dāng)前打印線程:Thread-3 需要2秒
正在打印第一份文件。。。。當(dāng)前打印線程:Thread-4 需要10秒
正在打印第二份文件。。。。當(dāng)前打印線程:Thread-4 需要1秒
正在打印第一份文件。。。。當(dāng)前打印線程:Thread-5 需要5秒
正在打印第二份文件。。。。當(dāng)前打印線程:Thread-5 需要8秒
正在打印第一份文件。。。。當(dāng)前打印線程:Thread-6 需要9秒
正在打印第二份文件。。。。當(dāng)前打印線程:Thread-6 需要6秒
正在打印第一份文件。。。。當(dāng)前打印線程:Thread-7 需要9秒
正在打印第二份文件。。。。當(dāng)前打印線程:Thread-7 需要8秒
正在打印第一份文件。。。。當(dāng)前打印線程:Thread-8 需要6秒
正在打印第二份文件。。。。當(dāng)前打印線程:Thread-8 需要6秒
正在打印第一份文件。。。。當(dāng)前打印線程:Thread-9 需要6秒
正在打印第二份文件。。。。當(dāng)前打印線程:Thread-9 需要4秒
非公平鎖將標(biāo)志調(diào)整為false即可,這里就不多做演示了。
(2) 通過源碼查看兩者實(shí)現(xiàn)邏輯
如下所示,我們可以在構(gòu)造方法中看到公平鎖和非公平鎖是如何根據(jù)參數(shù)決定的。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
我們不妨看看ReentrantLock公平鎖的內(nèi)部類公平鎖FairSync的源碼,如下所示,可以看到,他的取鎖邏輯必須保證當(dāng)前取鎖的節(jié)點(diǎn)沒有前驅(qū)節(jié)點(diǎn)才能搶鎖,這也就是為什么我們的線程會(huì)排隊(duì)取鎖。
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//當(dāng)前節(jié)點(diǎn)沒有前驅(qū)節(jié)點(diǎn)的情況下才能進(jìn)行取鎖
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;
}
}
相比之下,非公平鎖就很粗暴了,我們看看ReentrantLock內(nèi)部類NonfairSync,只要CAS成功就行了,所以鎖一旦空閑,所有線程都可以隨機(jī)爭(zhēng)搶。
final void lock() {
//無論隊(duì)列情況,直接CAS成功后即可持有鎖
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
相對(duì)之下公平鎖由于是有序執(zhí)行,所以相對(duì)非公平鎖來說執(zhí)行更慢,吞吐量更小一些。 而非公平鎖可以在特定場(chǎng)景下實(shí)現(xiàn)插隊(duì),所以很有可能出現(xiàn)某些線程被頻繁插隊(duì)而導(dǎo)致"線程饑餓"的情況。
4. 按是否共享性進(jìn)行分類
共享鎖最常見的使用就是ReentrantReadWriteLock,其讀鎖就是共享鎖,當(dāng)某一線程使用讀鎖時(shí),其他線程也可以使用讀鎖,因?yàn)樽x不會(huì)修改數(shù)據(jù),無論多少個(gè)線程讀都可以。而寫鎖就是獨(dú)占鎖的典型,當(dāng)某個(gè)線程執(zhí)行寫時(shí),為了保證數(shù)據(jù)的準(zhǔn)確性,其他線程無論使用讀鎖還是寫鎖,都得阻塞等待當(dāng)前正在使用寫鎖的線程釋放鎖才能執(zhí)行。
JUC下的讀寫鎖的本質(zhì)上是通過CAS修改AQS狀態(tài)值來完成鎖的獲取,如下圖,因?yàn)樽x鎖共享,所以多個(gè)線程獲取讀鎖是只要判斷鎖沒有被獨(dú)占(即沒有線程獲取讀鎖),則直接CAS修改state值,完成讀鎖獲取。而其它線程準(zhǔn)備獲取寫鎖時(shí)如果感知到state非0且持有者非自己則說明有線程上讀鎖,則阻塞等待釋放:
對(duì)應(yīng)我們給出讀鎖的持有邏輯即ReentrantReadWriteLock下的Sync的tryAcquireShared,本質(zhì)邏輯如上文所說,即判斷是否存在非本線程的獨(dú)占,如果沒有則持有CAS累加狀態(tài)完成讀鎖獲?。?/p>
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
//查看state的值
int c = getState();
//exclusiveCount非0說明有人上寫鎖,如果非自己的直接返回,說明上讀鎖失敗
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//獲取共享鎖持有者個(gè)數(shù),然后CAS累加完成讀鎖記錄維護(hù)
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//......
return 1;
}
return fullTryAcquireShared(current);
}
同理寫鎖的獲取邏輯則是通過state判斷是否有人獲取讀鎖,然后基于如下幾種情況決定是否可以上鎖:
- 如果state為0,說明沒有人獲取讀鎖,直接CAS修改state完成上鎖返回
- 如果state非0,則判斷寫鎖是否是自己持有,如果是則說明是重入直接累加state,反之說明上鎖失敗
寫鎖獲取過程,對(duì)應(yīng)源碼如下:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//獲取state查看有多少線程獲取讀鎖
int c = getState();
//基于w查看是否有線程獲取寫鎖
int w = exclusiveCount(c);
if (c != 0) {
//若c非0說明有人獲取讀鎖,然后進(jìn)入如下判斷,如果即如果寫鎖不是當(dāng)前線程獲取則直接返回
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//......
//來到這里說明是寫鎖重入,直接累加
setState(c + acquires);
return true;
}
//若沒有線程獲取讀鎖直接CAS修改獲取讀鎖返回
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
(1) 讀寫鎖使用示例
代碼的邏輯也很簡(jiǎn)單,獲取讀鎖讀取數(shù)據(jù),獲取寫鎖修改數(shù)據(jù)。
public class BaseRWdemo {
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//讀鎖
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//寫鎖
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static void read() {
//獲取讀鎖,讀取數(shù)據(jù)
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到讀鎖");
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println(Thread.currentThread().getName() + "釋放了讀鎖");
readLock.unlock();
}
}
private static void write() {
//獲取寫鎖,寫數(shù)據(jù)
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "得到寫鎖");
Thread.sleep(1000);
} catch (Exception e) {
}finally {
System.out.println(Thread.currentThread().getName() + "釋放了寫鎖");
writeLock.unlock();
}
}
}
測(cè)試代碼:
public static void main(String[] args) {
//讀鎖可以一起獲取
new Thread(() -> read(), "thread1").start();
new Thread(() -> read(), "thread2").start();
//等上面讀完寫鎖才能用 從而保證線程安全問題
new Thread(() -> write(), "thread3").start();
//等上面寫完 才能開始寫 避免線程安全問題
new Thread(() -> write(), "thread4").start();
}
從輸出結(jié)果不難看出,一旦資源被上了讀鎖,寫鎖就無法操作,只有讀鎖操作結(jié)束,寫鎖才能操作資源。
thread1得到讀鎖
thread2得到讀鎖
thread1釋放了讀鎖
thread2釋放了讀鎖
# 寫鎖必須等讀鎖釋放了才能操作
thread3得到寫鎖
thread3釋放了寫鎖
thread4得到寫鎖
thread4釋放了寫鎖
(2) 讀寫鎖非公平場(chǎng)景下的插隊(duì)問題
讀寫鎖ReentrantReadWriteLock設(shè)置為true,即公平鎖,其底層也很上述的ReentrantLock類似,同樣是通過AQS管理線程流程控制,同樣是非公平情況下任意線程都可以直接嘗試爭(zhēng)搶鎖而非,對(duì)應(yīng)的代碼示例如下,可以看到筆者初始化讀寫鎖ReentrantReadWriteLock并將fair標(biāo)識(shí)設(shè)置為true即公平鎖:
//設(shè)置為false之后 非公平 等待隊(duì)列前是讀鎖 就可以讓讀鎖插隊(duì)
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(false);
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
同時(shí)我們也給出讀寫鎖的實(shí)用代碼,邏輯比較簡(jiǎn)單,上鎖后休眠1000毫秒,同時(shí)在嘗試上鎖、得到鎖、釋放鎖附近打印日志:
private static void read() {
log.info("嘗試獲取讀鎖");
readLock.lock();
try {
log.info("獲取讀鎖成功,執(zhí)行業(yè)務(wù)邏輯......");
ThreadUtil.sleep(1000);
} catch (Exception e) {
//......
} finally {
readLock.unlock();
log.info("釋放讀鎖");
}
}
private static void write() {
log.info("嘗試獲取寫鎖");
writeLock.lock();
try {
log.info("獲取寫鎖成功,執(zhí)行業(yè)務(wù)邏輯......");
Thread.sleep(1000);
} catch (Exception e) {
} finally {
writeLock.unlock();
log.info("釋放寫鎖");
}
}
對(duì)應(yīng)的我們也給出使用的代碼示例,感興趣的讀者可以基于標(biāo)識(shí)自行調(diào)試一下:
public static void main(String[] args) {
new Thread(() -> write(), "t0").start();
new Thread(() -> read(), "t1").start();
new Thread(() -> read(), "t2").start();
new Thread(() -> write(), "t3").start();
new Thread(() -> read(), "t4").start();
ThreadUtil.sleep(1, TimeUnit.DAYS);
}
(3) 源碼解析非公平鎖插隊(duì)原理
我們可以看到一個(gè)tryAcquireShared方法,因?yàn)槲覀冊(cè)O(shè)置的是非公平鎖,所以代碼最后只能會(huì)走到NonfairSync 的tryAcquireShared。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
可以看到邏輯也很簡(jiǎn)單,一旦隊(duì)首節(jié)點(diǎn)釋放鎖之后,就會(huì)通知其他節(jié)點(diǎn)進(jìn)行爭(zhēng)搶,而其他節(jié)點(diǎn)都會(huì)走到這段邏輯,只要判斷到?jīng)]有人持有鎖,就直接進(jìn)行CAS爭(zhēng)搶。這就應(yīng)證了我們上述的觀點(diǎn),等待隊(duì)列首節(jié)點(diǎn)是寫鎖占有鎖的情況下,一旦寫鎖釋放之后,后續(xù)的線程可以任意插隊(duì)搶占并上讀鎖或者寫鎖,這也就是為什么我們上文的線程3先于線程2上了讀鎖的原因。
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
//如果小于0則說明沒有人持有可以直接通過CAS進(jìn)行爭(zhēng)搶
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
(4) 鎖降級(jí)思想
因?yàn)樽x寫鎖互斥,所以某些修改操作需要獲取寫鎖后才能進(jìn)行修改操作,這使得我們必須在持有寫鎖的情況下,完成修改后,通過鎖降級(jí)繼續(xù)讀取數(shù)據(jù)。
對(duì)應(yīng)代碼示例如下,可以看到在當(dāng)前線程獲取讀鎖情況下,整套鎖升級(jí)的步驟為:
- 先釋放讀鎖,嘗試獲取寫鎖更新數(shù)據(jù)
- 獲取寫鎖完成數(shù)據(jù)更新
- 因?yàn)楂@取寫鎖成功就說明鎖被當(dāng)前線程獨(dú)占,可直接獲取讀鎖(上述源碼已說明)
- 獲取讀鎖成功,釋放寫鎖,完成獨(dú)占鎖降級(jí)
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
//讀鎖
private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
//寫鎖
private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
private static volatile boolean update = false;
public static void main(String[] args) {
//模擬某些原因上了讀鎖
readLock.lock();
//因?yàn)閿?shù)據(jù)更新的原因需要上寫鎖
if (!update) {
try {
//釋放讀鎖,獲取寫鎖更新數(shù)據(jù)
readLock.unlock();
writeLock.lock();
if (!update) {
//模擬數(shù)據(jù)更新
update = true;
}
//寫鎖被當(dāng)前線程持有直接獲取讀鎖
readLock.lock();
} finally {
//釋放寫鎖,完成鎖降級(jí)
writeLock.unlock();
}
}
}
(5) 為什么讀寫鎖不支持鎖升級(jí)
讀寫鎖升級(jí)過程大體是:
- 持有讀鎖線程嘗試獲取寫鎖
- 如果沒有其它線程獲取讀鎖,則直接上互斥獨(dú)占的寫鎖,若其它線程上了讀鎖,則等待其它線程釋放讀鎖后,保證可獨(dú)占的情況下獲取寫鎖
- 獲取寫鎖操作數(shù)據(jù),完成鎖升級(jí)
這就存在死鎖的風(fēng)險(xiǎn),例如線程1和線程2同時(shí)獲取讀鎖,二者都希望完成鎖升級(jí),各自等待雙方釋放讀鎖后獲取寫鎖
5. 按照是否自旋進(jìn)行分類
我們都知道Java阻塞或者喚醒一個(gè)線程都需要切換CPU狀態(tài)的,這樣的操作非常耗費(fèi)時(shí)間,而很多線程切換后執(zhí)行的邏輯僅僅是一小段代碼,為了這一小段代碼而耗費(fèi)這么長(zhǎng)的時(shí)間確實(shí)是一件得不償失的事情。對(duì)此java設(shè)計(jì)者就設(shè)計(jì)了一種讓線程不阻塞,原地"稍等"即自旋一下的操作。
如下代碼所示,我們通過AtomicReference原子類實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的自旋鎖,通過compareAndSet嘗試讓當(dāng)前線程持有資源,如果成功則執(zhí)行業(yè)務(wù)邏輯,反之循環(huán)等待。
public class MySpinLock {
private AtomicReference<Thread> sign = new AtomicReference<>();
public void lock() {
Thread curThread = Thread.currentThread();
//使用原子類自旋設(shè)置原子類線程,若線程設(shè)置為當(dāng)前線程則說明當(dāng)前線程上鎖成功
while (!sign.compareAndSet(null, curThread)) {
System.out.println(curThread.getName() + "未得到鎖,自旋中");
}
}
public void unLock() {
Thread curThread = Thread.currentThread();
sign.compareAndSet(curThread, null);
System.out.println(curThread.getName() + "釋放鎖");
}
public static void main(String[] args) {
MySpinLock mySpinLock = new MySpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "嘗試獲取自旋鎖");
mySpinLock.lock();
System.out.println(Thread.currentThread().getName() + "得到了自旋鎖");
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mySpinLock.unLock();
System.out.println(Thread.currentThread().getName() + "釋放了自旋鎖");
}
}
};
Thread t1=new Thread(runnable,"t1");
Thread t2=new Thread(runnable,"t2");
t1.start();
t2.start();
}
}
輸出結(jié)果:
t1嘗試獲取自旋鎖
t2嘗試獲取自旋鎖
t1得到了自旋鎖
t2未得到鎖,自旋中
t2未得到鎖,自旋中
t2未得到鎖,自旋中
t2未得到鎖,自旋中
t2未得到鎖,自旋中
t2未得到鎖,自旋中
t1釋放鎖
t2得到了自旋鎖
t1釋放了自旋鎖
t2釋放鎖
t2釋放了自旋鎖
6. 按是否可支持中斷進(jìn)行分類
可中斷鎖上文lockInterruptibly上文已經(jīng)演示過了,這里就不多做贅述了。
public class LockInterruptiblyDemo implements Runnable {
//設(shè)置為static,所有對(duì)象共享
private static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 嘗試取鎖");
try {
//設(shè)置鎖可以被打斷
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 取鎖成功");
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " 執(zhí)行業(yè)務(wù)邏輯時(shí)被中斷");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "嘗試取鎖時(shí)被中斷");
}
}
}
測(cè)試代碼:
public static void main(String[] args) throws InterruptedException {
LockInterruptiblyDemo lockInterruptiblyDemo = new LockInterruptiblyDemo();
//線程1啟動(dòng)
Thread thread0 = new Thread(lockInterruptiblyDemo);
thread0.start();
//線程2啟動(dòng)
Thread thread1 = new Thread(lockInterruptiblyDemo);
thread1.start();
//主線程休眠,讓上述代碼執(zhí)行,然后執(zhí)行打斷線程1邏輯 thread0.interrupt();
Thread.sleep(2000);
thread0.interrupt();
}
這里補(bǔ)充一下可中斷鎖的原理,可中斷鎖實(shí)現(xiàn)的可中斷的方法很簡(jiǎn)單,通過acquireInterruptibly建立一個(gè)可中斷的取鎖邏輯。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
我們不如源碼可以看到,對(duì)于沒有獲得鎖的線程,判斷走到interrupted看看當(dāng)前線程是否被打斷,如果打斷了則直接拋出中斷異常。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//當(dāng)線程被打斷時(shí),直接拋出中斷異常
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}