Lock鎖的公平性和非公平性
1、lock鎖項目使用
在項目中的使用方式:
public class AQSTestOne {
// 使用公平鎖來進行測試
private static final Lock LOCK = new ReentrantLock(true);
public static void main(String[] args) {
LOCK.lock();
try {
System.out.println("so something ");
}catch (Exception e){
System.out.println("do Exception something............");
}finally {
LOCK.unlock();
}
}
}
因為對於對象來說,對於成員變量LOCK鎖來說,會在堆內存中,任何一個線程進來的時候執行了對應的方法,都會執行到lock鎖上來進行排隊。
每個線程都會來使用lock鎖,那么lock.lock()方法是如何保證多線程環境下,在JVM中在某一個時刻,只有一個線程占用鎖呢?
2、AQS繼承體系
對於ReentrantLock類中,存在AbstractQueuedSynchronizer類以及對應的子類Sync和Sync的兩個子類:FairSync和NonfairSync
對應的是公平同步鎖和非公平同步鎖。
3、構造函數
首先從構造函數來講起,因為非公平鎖的效率高,所以推薦使用的是非公平鎖。
從構造函數中可以看到對應的結構:
private final Sync sync;
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 默認采用非公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
// 繼承體系圖
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
}
4、加鎖流程
lock鎖是如何保證多線程能夠保證線程安全呢?
那么看一下lock鎖又是如何來進行加鎖的。
首先來看這行代碼到底做了什么?
lock.lock();
進源碼查看:
public void lock() {
sync.lock();
}
那么這里看公平鎖的實現方式:
final void lock() {
acquire(1);
}
重點就來到了acquire方法,看看對應的代碼實現:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
通過代碼可以看到首先會來嘗試獲取。
可以根據代碼來畫一幅流程圖來描述是如何來獲取得到鎖的:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
// 判斷當前鎖是否被其他線程持有!利用一個int類型的變量來進行修飾
int c = getState();
if (c == 0) {
// 這里判斷是無鎖狀態之后,並不是直接獲取得到鎖,還需要來進行判斷CLH隊列中是否有線程在排隊
// 因為這里是公平鎖,公平鎖就需要保證如果CLH隊列中有在排隊的線程,那么讓他們先獲取得到
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;
}
}
看一下是如何查看隊列中是否是有線程在排隊的:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
}
在這里來組成一個雙向鏈表,又稱之為CLH隊列。head指向隊頭,tail指向隊尾。
如果h!=t,表示的是CLH隊列中是有線程排隊的,后面的兩個判斷只要有一個判斷是成功的,那么就說明隊列中是有值的。
兩個判斷:1、如果頭結點的下一個節點為空;2、頭結點的下一個節點不為空並且不為當前線程;
如果返回true的話,那么表示CLH隊列中是有線程在排隊的;
如果是false的話,那么表示的是CLH隊列中是沒有線程在排隊的。
這里就是直接判斷是否存在首節點,head的節點的下一個節點(首節點)是有是有值的,如果有值,那么說明CLH隊列中是有值的。
如果隊列中沒有線程在等待鎖,可以看到利用CAS來獲取得到鎖,然后設置鎖被哪個線程獲取得到;
如果已經有線程持有了鎖,那么判斷是否是當前的線程,如果是,那么進行再次加鎖;
如果隊列中有線程在排隊等待鎖並且不是當前線程,那么直接獲取得到鎖失敗;
那么對應的流程如下所示:
對應的流程如下所示:
- 1、每個線程在獲取鎖的時候,判斷鎖的狀態,如果是無鎖狀態,那么進入到隊列中查看隊列中是否有其它線程,如果沒有,那么去獲取得到鎖;如果有的話,那么獲取鎖事變,排隊等鎖;
- 2、如果當前線程檢查是有鎖狀態,那么判斷持有鎖的是否是當前線程,如果是,那么鎖重入次數+1;
- 3、如果不是當前線程持有鎖,那么獲取得到鎖失敗;
4.1、加鎖流程的兩種情況
總結起來,獲取得到鎖的線程只有兩種情況:
1、當前鎖狀態是無鎖,且隊列中沒有線程在排隊,那么獲取得到鎖;
2、當前鎖狀態是有鎖,且持有鎖的線程是自己,那么這個時候鎖是可重入的;
5、線程沒有搶到鎖之后需要排隊
沒有搶到鎖的線程需要進行排隊,繼續看下源碼:
public final void acquire(int arg) {
// 沒有搶到鎖,返回false,取反為true,那么執行后面的邏輯
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先會創建線程等待者,當前是獨占模式:
private Node addWaiter(Node mode) {
// 創建節點保存當前的線程
Node node = new Node(Thread.currentThread(), mode);
// 獲取得到尾結點
Node pred = tail;
if (pred != null) {
// 如果尾結點不為空
// 1、首先將尾節點的前驅指針指針尾指針指向的節點
node.prev = pred;
// 2、比較並交換,並將尾結點指針后移一位
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 這里有兩種情況存在。因為上面如果存在多線程競爭,稱為不了尾結點的線程將會走到這里來。看一下入隊操作
// 1、tail為null;2、比較並交換稱為尾結點的節點
enq(node);
return node;
}
注意看下上面的if判斷,在入隊尾的時進行比較並交換時,是失敗的,那么這個時候將會再次執行enq方法。
看一下enq方法:
private Node enq(final Node node) {
// 死循環
for (;;) {
Node t = tail;
// 如果tail為null,那么這種也是上面的一種的情況。這里需要來進行初始化
// 從這里也可以看到初始化的是一個空節點,不保存任何線程
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 上面的另外一種情況。和上面的addWaiter中判斷是一致的
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
這里一直在使用死循環,這里存在着兩個作用:
-
1、初始化,讓head和tail節點指向一個空的節點;
-
2、將排隊的節點稱為尾結點(隊列特點:先進先出FIFO)
這里的for循環是為了構建CLH阻塞隊列。這里是第一個死循環隊列來進行構建的。
總之是為了將所有沒有搶到鎖的線程來進行入隊,這里的圖形畫出來:
這里就對應着上面的for循環操作。上面在死循環中構建一個CLH隊列,但是此時還沒有做任何操作,比如說節點中的值還沒有來得及對其進行設置。
6、CLH隊列中線程先搶鎖后阻塞
final boolean acquireQueued(final Node node, int arg) {
// 獲取鎖失敗為true
boolean failed = true;
try {
// 線程中斷狀態
boolean interrupted = false;
for (;;) {
// 獲取得到前驅節點。已經之前已經排好隊
final Node p = node.predecessor();
// 如果當前節點的頭結點是頭結點!那么再次來嘗試獲取得到一次鎖看看能不能成功
// 因為可能在執行到這一步的時候,線程已經將鎖釋放了,所以這里再次來嘗試一下
if (p == head && tryAcquire(arg)) {
// 將當前節點設置為頭結點
setHead(node);
// 當前的頭結點沒有作用了,需要GC掉
p.next = null; // help GC
// 因為搶占鎖成功,所以這里標注為fasle;
failed = false;
// 不是因為中斷引起的線程搶占鎖中斷
return interrupted;
}
// 在失敗獲取得到鎖的時候,應該將線程進行阻塞!這是才是重點!
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
6.1、for循環
這里的for循環是為了修改每個線程對應的節點的執行狀態的。只有節點狀態是SINGAL的時候才會在喚醒的時候有機會獲取得到線程。
而剛剛入隊的節點是並不是SINGAL狀態的,所以這里是在循環設置。那么看一下在失敗獲取得到鎖之后是如何將線程進行阻塞的:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 首先獲取得到每個線程排隊節點的前驅節點中的等待狀態!
int ws = pred.waitStatus;
// 如果是SINGAL,那么標識,就等着被喚醒來獲取得到鎖
if (ws == Node.SIGNAL)
return true;
// 這里標識的是線程搶鎖取消,不再去搶鎖了
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
// 如果是其他的,那么比較並交換,將當前的接地那的waitstatus進行設置
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
因為只有當這一步為true的時候,才會將線程進行阻塞。那么為true的就只有一步
if (ws == Node.SIGNAL)
return true;
只有所有的節點中的status為Node.SIGNAL的時候,才會為true。
那么為fales的時候,將會再次走到下面的死循環中來:
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
// 只有當線程阻塞過程中,是因為線程中斷而導致排隊中的線程被喚醒,那么這里標記
// 將會被設置為true;
interrupted = true;
}
知道為true的時候,才會走到parkAndCheckInterrupt方法中來,而這一步是真正的做到將線程進行終止的操作的方法:
private final boolean parkAndCheckInterrupt() {
// 將當前線程阻塞到對象上來
LockSupport.park(this);
// 當前線程是否是以中斷引起的?如果是,那么返回true;如果不是,那么返回false;
return Thread.interrupted();
}
被park住的線程,此時被阻塞了,要是想蘇醒過來,必須要等到前一個線程來將其進行喚醒。
注意park()和park(this)的使用區別:
將隊列中的前驅節點中的waitstatus進行修改,表示的是可以將后來的線程來進行喚醒。
7、鎖釋放
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
直接看嘗試釋放鎖的代碼:
protected final boolean tryRelease(int releases) {
// 這里體現出現來可重入鎖的特性
int c = getState() - releases;
// 如果不是當前線程來釋放鎖,那么將會報錯!
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 將釋放標記來進行標記
boolean free = false;
// 如果c==0,那么表示的是鎖能夠釋放。如果是可重入鎖,那么這里不為0的時候,將會繼續來進行設置
// 所以這里也就要求!加了多少次鎖,就要釋放多少次鎖
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
// 只有c==0的時候,這里才為true
return free;
}
那么再次回到上一步,成功釋放鎖之后操作
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
獲取得到頭結點,然后判斷頭節點不為空以及等待狀態不為0(必須是SIGNAL狀態)的時候,喚醒頭結點的下一個節點:
private void unparkSuccessor(Node node) {
// 獲取得到頭結點的狀態信息
int ws = node.waitStatus;
// 如果<0,那么比較並交換,將當前節點的狀態的status修改成0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 獲取得到下一個節點
Node s = node.next;
// 如果為空或者是等待狀態>0(明顯為0)
if (s == null || s.waitStatus > 0) {
// 失去引用,那么會GC掉
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 喚醒線程
LockSupport.unpark(s.thread);
}
具體的執行圖如下所示:
當然,這里並沒有將將前驅節點斷掉,而是在喚醒線程后做的操作:
那么又再次回到原始起點:
for (;;) {
// 喚醒之后,又會來到這里
final Node p = node.predecessor();
// 嘗試獲取得到鎖。對於公平鎖來說,一定會執行到這一步,公平鎖一定會獲取得到
if (p == head && tryAcquire(arg)) {
// 當前節點設置為頭結點,並消除掉前置指針
setHead(node);
// 后置指向置為空
p.next = null; // help GC
failed = false;
// 跳出循環
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
看一下這里的setHead(node)方法:
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
將當前的節點設置為head節點,然后將節點中的線程置為空,然后斷掉前置指針。
最終的結果就是圖如下所示:
8、線程等待狀態補充說明
waitStatus 非常重要,也是關鍵,有下面幾個枚舉值:
枚舉 | 含義 |
---|---|
CANCELLED | 為1,表示線程獲取鎖的請求已經取消了 |
SIGNAL | 為-1,表示線程已經准備好了,就等資源釋放了 |
CONDITION | 為-2,表示節點在等待隊列中,節點線程等待喚醒 |
PROPAGATE | 為-3,當前線程處在SHARED情況下,該字段才會使用 |
0 | 當一個Node被初始化的時候的默認值 |
至此公平鎖的流程分析結束:
lock.lock();
xxxx;
lock.unlock();
這段代碼的執行邏輯分析結束。
9、總結公平鎖的獲取流程
這里對應的是我自己畫的一個流程圖:
兩個for循環的作用:
- 1、第一個for循環是排隊進入阻塞隊列隊尾;
- 2、第二個for循環是修改每個節點的狀態;
隊頭喚醒之后進入循環
可以看到鎖在釋放的時候會喚醒下一個節點,喚醒下一個節點的時候,是線程自己進入到for循環中來再次嘗試獲取得到鎖。
對於公平鎖而言,隊頭元素肯定是可以獲取得到鎖的。因為有個判斷,判斷前驅節點是隊頭的才可以。
10、非公平鎖的加鎖流程
直接看對應的代碼:
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
之所以是非公平的,上來就進行比較並交換,如果成功,就比較並設置為當前的線程,野蠻至極。
就這一步的區別,其他的也沒有任何的變化了,所以總結起來只需要來畫個圖即可。
其實就是在線程剛剛進來的時候直接搶鎖,非常類似syncronized的流程。因為syncronzied鎖是非公平性的,也會嘗試搶占。
11、Lock鎖的特性講解
11.1、Lock鎖特性
- 阻塞等待隊列
- 共享鎖和獨占鎖(排他鎖)
- 公平和非公平性
- 可重入
- 可中斷
11.2、Lock鎖是用變量state標識
用一個state標識來表示當前的鎖是否被占有,需要注意的是state變量是用volatile關鍵字來進行修飾的。
能夠及時讓其他線程線程看到鎖是否被搶占。
11.3、兩種隊列
- 阻塞隊列
- 條件隊列
阻塞對象是將沒有獲取得到鎖的線程放到CLH隊列中來進行阻塞;
條件隊列是在滿足條件的地方,調用condition.await方法的時候,將當前線程占有的鎖釋放掉,然后放入到條件隊列中來;當調用condition.sign()或者是condition.sinalAll()方法的時候,將被放在條件隊列中的線程追加到阻塞隊列上來,讓條件隊列中的線程有機會獲取得到鎖。
注:條件隊列的使用及其類似多線程中原始的wait()/notify()/notifyAll()方法的使用
示例:
/**
* @Description 等待喚醒機制 除了wait() 和notify\notifyAll方法而喚醒的
*
* 使用lock鎖的condition十分類似於notify和notifyAll的機制,只是通知,但是並沒有真正的將鎖給釋放掉
* 而await方法,是將當前線程的執行權讓出去;讓當前的線程陷入到阻塞中去,等待其他線程的喚醒!
*
* await在當前持有鎖的階段中釋放鎖,然后將自己放入到阻塞線程中去;
* sinal在持有鎖階段,將因為放到條件隊列中的線程喚醒,將條件隊列中的線程追加到阻塞隊列上去;
* @Author liguang
* @Date 2022/03/19/10:27
*/
public class LockTestOne {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
// 條件隊列
Condition condition = lock.newCondition();
new Thread(()->{
lock.lock();
try {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"---------開始處理任務");
condition.await();
System.out.println(threadName+"---------處理任務結束");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
new Thread(()->{
lock.lock();
try {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"---------開始處理任務");
condition.signal();
System.out.println(threadName+"---------處理任務結束");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}).start();
}
}
11.4、節點五種狀態
- 值為0,初始化狀態,表示當前節點在sync隊列中,等待着獲取鎖。
- CANCELLED,值為1,表示當前的線程被取消;
- SIGNAL,值為-1,表示當前節點的后繼節點包含的線程需要運行,也就是unpark;
- CONDITION,值為-2,表示當前節點在等待condition,也就是在condition隊列中;
- PROPAGATE,值為-3,表示當前場景下后續的acquireShared能夠得以執行;
不同的自定義同步器競爭共享資源的方式也不同。自定義同步器在實現時只需要實現共享
資源state的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出
隊等),AQS已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:
isHeldExclusively():該線程是否正在獨占資源。只有用到condition才需要去實現
它。
tryAcquire(int):獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false。
tryRelease(int):獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false。
tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但
沒有剩余可用資源;正數表示成功,且有剩余資源。
tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放后允許喚醒后續等待
結點返回true,否則返回false。
12、測試Lock鎖特性
12.1、可重入鎖
/**
* 測試可重入性
*/
public class LockTestThree {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
method1();
}).start();
}
}
public static void method1(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"method1");
method2();
}finally {
lock.unlock();
}
}
private static void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"method2");
method3();
}finally {
lock.unlock();
}
}
private static void method3() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"method3");
}finally {
lock.unlock();
}
}
}
每次去獲取得到鎖的時候,都會發現占用的鎖的是當前的線程,所以會在state基礎之上+1。
問題:加了多少次鎖,就要釋放多少次鎖。不能多一次,否則將會導致一把鎖一直被一個線程一直占用。
示例:
public class ThreadLockCount {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
for (int i = 0; i < 5; i++) {
new Thread(()->{
lock.lock();
System.out.println("hello,world");
}).start();
}
}
}
這里正是因為一個線程一直持有鎖(state),一直不為0,那么所有的線程都將會阻塞在隊列中,無法繼續向下繼續運行。
12.2、可中斷性
線程被喚醒有兩種情況
- 鎖釋放,喚醒后續節點
- 線程中斷
可以在源碼中的if判斷中,因為中斷而喚醒的,會有對應的中斷標記表示的是因為中斷而引起的線程中斷。
而給線程打上標記之后,在外部的某個地方,可以通過判斷線程狀態,來獲取得到對應的。對應的源碼體現:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
// 如果是因為中斷引起的,那么會清除中斷標記並返回true
parkAndCheckInterrupt())
// 然后會給這個標識修改為true,表示是以為線程中斷引起的,外部可以做另外操作。
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
然后將標記暴露給外部:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
對應的狀態:
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
在外部程序可以檢測到這個異常信息。
來寫個代碼表示一下:
/**
*
* 100個線程來進行自增操作,但是其中一個線程在排隊時,發送中斷標記,告知該線程不應該繼續操作
* 終止其當前線程正在運行的動作
* @author liguang
* @date 2022/7/28 10:00
*/
public class Test2 {
private static final ReentrantLock lock = new ReentrantLock(false);
private static int i = 0;
public static void main(String[] args) {
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
reentrantLock();
});
threadList.add(thread);
thread.start();
}
try {
// 發一個中斷信號,將其進行喚醒
threadList.get(90).interrupt();
Thread.sleep(2000);
System.out.println("最終確定的值是:"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void reentrantLock(){
try {
lock.lock();
// 如果線程中斷了,就不應該再來進行后續的任務了
while (Thread.currentThread().isInterrupted()){
// 擦除掉中斷標記,然后返回,后面的事情不做了
boolean interrupted = Thread.interrupted();
System.out.println("線程標記裝填清除了"+interrupted);
System.out.println(Thread.currentThread().isInterrupted());
}
i++;
} catch (Exception e) {
System.out.println(e);
System.out.println("線程中斷了"+Thread.currentThread().getName());
// 說明有一個線程是因為線程中斷喚醒的!所以需要將其進行替換掉
System.out.println("當前線程狀態是:"+Thread.currentThread().isInterrupted());
}finally {
lock.unlock();
}
}
}
多運行幾次,會出現以下效果。因為線程運行過程中,如果中斷了,碰上了Thread.sleep的話,會導致異常出現。
因為沒有將線程睡眠一會兒,所以線程中斷可能在線程運行完成之后,也可能是在線程運行之前。
線程標記裝填清除了true
false
最終確定的值是:100
也有另外一個方法
lock.lockInterruptibly();
看看其實現原理:
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果是因為線程中斷引起,那么直接拋出異常
throw new InterruptedException();
}
} finally {
// 拋出異常之前,這里執行取消對應的排隊節點
if (failed)
cancelAcquire(node);
}
}
具體實例如下所示:
/**
*
* 100個線程來進行自增操作
* @author liguang
* @date 2022/7/28 10:00
*/
public class Test1 {
private static final ReentrantLock lock = new ReentrantLock(false);
private static int i = 0;
public static void main(String[] args) {
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
reentrantLock();
});
threadList.add(thread);
thread.start();
}
try {
threadList.get(90).interrupt();
Thread.sleep(1000);
System.out.println("最終確定的值是:"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void reentrantLock(){
try {
lock.lockInterruptibly();
Thread.sleep(10);
i++;
} catch (Exception e) {
System.out.println("線程中斷了,拋出異常了");
}finally {
lock.unlock();
}
}
}
12.3.1、立即失敗
/**
* 嘗試獲取得到鎖,這里是立即失敗,不管是公平鎖還是非公平鎖,都是理解返回的狀態
*/
public class LockTestFive {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
System.out.println("線程t1啟動");
if (!lock.tryLock()){
System.out.println("線程t1沒有獲取得到鎖,返回");
return;
}
try {
System.out.println("獲取得到了鎖!");
}finally {
// 將鎖釋放
lock.unlock();
}
}, "lig");
// main線程
lock.lock();
try {
System.out.println("main線程獲取得到了鎖");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
}
}
12.3.2、超時失敗
/**
* 在指定的時間內沒有獲取得到鎖之后,失敗
*/
public class LockTestSix {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
System.out.println("線程t1啟動");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)){
System.out.println("線程t1沒有獲取得到鎖,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
// 獲取得到鎖之后,下面的就不需要再來進行執行了
return;
}
try {
System.out.println("獲取得到了鎖!");
}finally {
// 將鎖釋放
lock.unlock();
}
}, "lig");
// main線程
lock.lock();
try {
System.out.println("main線程獲取得到了鎖");
t1.start();
try {
// 休眠兩秒鍾來進行測試
Thread.sleep(2000);
System.out.println("main線程釋放了鎖");
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
lock.unlock();
}
}
}
12.4、公平鎖和非公平鎖
/**
* 嘗試獲取得到鎖,這里是立即失敗,不管是公平鎖還是非公平鎖,都是理解返回的狀態
*/
public class LockTestSeven {
public static void main(String[] args) {
// 嘗試公平鎖和非公平鎖
// ReentrantLock lock = new ReentrantLock(true);
ReentrantLock lock = new ReentrantLock();
for (int i = 0; i < 5000; i++) {
new Thread(()->{
lock.lock();
try {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("當前線程"+Thread.currentThread().getName()+" is running.........");
}finally {
lock.unlock();
}
},"t"+i).start();
}
// 休眠之后再次去搶鎖
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 5000; i++) {
new Thread(()->{
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" is running.........");
}finally {
lock.unlock();
}
},"強行搶鎖"+i).start();
}
}
}
讓控制台交替打印,可以看到非公平鎖是可以來交替打印對應的線程Name的。
12.5、條件變量
調用 Condition.await() 方法使線程等待,其他線程調用Condition.signal() 或 Condition.signalAll() 方法喚醒等待的線程。
注意:調用Condition的await()和signal()方法,都必須在lock保護之內
/**
* 條件變量:模擬生產者和生產者!這是這里明顯,可以有多個條件,可以利用多個條件來進行操作
*/
public class LockTestEight {
private static Lock lock = new ReentrantLock();
private static Condition cigCon = lock.newCondition();
private static Condition takeCon = lock.newCondition();
private static boolean hasCig;
private static boolean hasTakeOut;
public void cigratee(){
lock.lock();
try {
while (!hasCig){
try {
System.out.println("沒有煙了,歇一會兒");
cigCon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("有煙了,開始干活...........");
}finally {
lock.unlock();
}
}
/**
* 送外賣
*/
public void takeOut(){
lock.lock();
try {
while (!hasTakeOut){
try {
System.out.println("飯還沒有好,歇一會兒");
takeCon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("有飯了,吃完飯開始干活...........");
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
LockTestEight lockTestEight = new LockTestEight();
new Thread(()->{
lockTestEight.cigratee();
}).start();
new Thread(()->{
lockTestEight.takeOut();
}).start();
new Thread(()->{
lock.lock();
try {
hasCig = true;
cigCon.signal();
}finally {
lock.unlock();
}
}).start();
new Thread(()->{
lock.lock();
try {
hasTakeOut = true;
takeCon.signal();
}finally {
lock.unlock();
}
}).start();
}
}
這個具體分析會在后面給列出來。