java并發(fā)編程專題(五)----詳解(JUC)ReentrantLock
上一節(jié)我們了解了Lock接口的一些簡單的說明,知道Lock鎖的常用形式,那么這節(jié)我們正式開始進入JUC鎖(java.util.concurrent包下的鎖,簡稱JUC鎖)。下面我們來看一下Lock最常用的實現(xiàn)類ReentrantLock。
1.ReentrantLock簡介
由單詞意思我們可以知道這是可重入的意思。那么可重入對于鎖而言到底意味著什么呢?簡單來說,它有一個與鎖相關的獲取計數(shù)器,如果擁有鎖的某個線程再次得到鎖,那么獲取計數(shù)器就加1,然后鎖需要被釋放兩次才能獲得真正釋放。這模仿了 synchronized 的語義;如果線程進入由線程已經擁有的監(jiān)控器保護的 synchronized 塊,就允許線程繼續(xù)進行,當線程退出第二個(或者后續(xù)) synchronized 塊的時候,不釋放鎖,只有線程退出它進入的監(jiān)控器保護的第一個 synchronized 塊時,才釋放鎖。
1.1公平鎖與非公平鎖
我們查看ReentrantLock的源碼可以看到無參構造函數(shù)是這樣的:
public ReentrantLock() {
sync = new NonfairSync();
}
NonfairSync()方法為一個非公平鎖的實現(xiàn)方法,另外Reentrantlock還有一個有參的構造方法:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
它允許您選擇想要一個 公平(fair)鎖,還是一個 不公平(unfair)鎖。公平鎖使線程按照請求鎖的順序依次獲得鎖;而不公平鎖則允許直接獲取鎖,在這種情況下,線程有時可以比先請求鎖的其他線程先得到鎖。
為什么我們不讓所有的鎖都公平呢?畢竟,公平是好事,不公平是不好的,不是嗎?(當孩子們想要一個決定時,總會叫嚷“這不公平”。我們認為公平非常重要,孩子們也知道。)在現(xiàn)實中,公平保證了鎖是非常健壯的鎖,有很大的性能成本。要確保公平所需要的記帳(bookkeeping)和同步,就意味著被爭奪的公平鎖要比不公平鎖的吞吐率更低。作為默認設置,應當把公平設置為 false ,除非公平對您的算法至關重要,需要嚴格按照線程排隊的順序對其進行服務。
下面我們先來看一個例子:
public class TestReentrantLock implements Runnable{
ReentrantLock lock = new ReentrantLock();
public void get() {
lock.lock();
System.out.println(Thread.currentThread().getId());
set();
lock.unlock();
}
public void set() {
lock.lock();
System.out.println(Thread.currentThread().getId());
lock.unlock();
}
@Override
public void run() {
get();
}
public static void main(String[] args) {
TestReentrantLock ss = new TestReentrantLock();
new Thread(ss).start();
new Thread(ss).start();
new Thread(ss).start();
}
}
運行結果:
10
10
12
12
11
11Process finished with exit code 0
由結果我們可以看出同一個線程進入了同一個ReentrantLock鎖兩次。
2.condition條件變量
我們知道根類 Object 包含某些特殊的方法,用來在線程的 wait() 、 notify() 和 notifyAll() 之間進行通信。那么為了在對象上 wait 或 notify ,您必須持有該對象的鎖。就像 Lock 是同步的概括一樣, Lock 框架包含了對 wait 和 notify 的概括,這個概括叫作 條件(Condition)。 Condition 的方法與 wait 、 notify 和 notifyAll 方法類似,分別命名為 await 、 signal 和signalAll ,因為它們不能覆蓋 Object 上的對應方法。
首先我們來計算一道題:
我們要打印1到9這9個數(shù)字,由A線程先打印1,2,3,然后由B線程打印4,5,6,然后再由A線程打印7,8,9. 這道題有很多種解法,我們先用Object的wait,notify方法來實現(xiàn):
public class WaitNotifyDemo {
private volatile int val = 1;
private synchronized void printAndIncrease() {
System.out.println(Thread.currentThread().getName() +"prints " + val);
val++;
}
// print 1,2,3 7,8,9
public class PrinterA implements Runnable {
@Override
public void run() {
while (val <= 3) {
printAndIncrease();
}
// print 1,2,3 then notify printerB
synchronized (WaitNotifyDemo.this) {
System.out.println("PrinterA printed 1,2,3; notify PrinterB");
WaitNotifyDemo.this.notify();
}
try {
while (val <= 6) {
synchronized (WaitNotifyDemo.this) {
System.out.println("wait in printerA");
WaitNotifyDemo.this.wait();
}
}
System.out.println("wait end printerA");
} catch (InterruptedException e) {
e.printStackTrace();
}
while (val <= 9) {
printAndIncrease();
}
System.out.println("PrinterA exits");
}
}
// print 4,5,6 after printA print 1,2,3
public class PrinterB implements Runnable {
@Override
public void run() {
while (val < 3) {
synchronized (WaitNotifyDemo.this) {
try {
System.out
.println("printerB wait for printerA printed 1,2,3");
WaitNotifyDemo.this.wait();
System.out
.println("printerB waited for printerA printed 1,2,3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
while (val <= 6) {
printAndIncrease();
}
System.out.println("notify in printerB");
synchronized (WaitNotifyDemo.this) {
WaitNotifyDemo.this.notify();
}
System.out.println("notify end printerB");
System.out.println("PrinterB exits.");
}
}
public static void main(String[] args) {
WaitNotifyDemo demo = new WaitNotifyDemo();
demo.doPrint();
}
private void doPrint() {
PrinterA pa = new PrinterA();
PrinterB pb = new PrinterB();
Thread a = new Thread(pa);
a.setName("printerA");
Thread b = new Thread(pb);
b.setName("printerB");
// 必須讓b線程先執(zhí)行,否則b線程有可能得不到鎖,執(zhí)行不了wait,而a線程一直持有鎖,會先notify了
b.start();
a.start();
}
}
運行結果為:
printerB wait for printerA printed 1,2,3
printerA prints 1
printerA prints 2
printerA prints 3
PrinterA printed 1,2,3; notify PrinterB
wait in printerA
printerB waited for printerA printed 1,2,3
printerB prints 4
printerB prints 5
printerB prints 6
notify in printerB
notify end printerB
wait end printerA
printerA prints 7
printerA prints 8
printerA prints 9
PrinterA exits
PrinterB exits.Process finished with exit code 0
我們來分析一下上面的程序:
首先在main方法中我們看到是先啟動了B線程,因為B線程持有wait()對象,而A線程則持有notify(),如果先啟動A有可能會造成死鎖的狀態(tài)。
B線程啟動以后進入run()方法:
while (val < 3) {
synchronized (WaitNotifyDemo.this) {
try {
System.out.println("printerB wait for printerA printed 1,2,3");
WaitNotifyDemo.this.wait();
System.out.println("printerB waited for printerA printed 1,2,3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
while (val <= 6) {
printAndIncrease();
}
這里有一個while循環(huán),如果val的值小于3,那么在WaitNotifyDemo的實例的同步塊中調用WaitNotifyDemo.this.wait()方法,這里要注意無論是wait,還是notify,notifyAll方法都需要在其實例對象的同步塊中執(zhí)行,這樣當前線程才能獲得同步實例的同步控制權,如果不在同步塊中執(zhí)行wait或者notify方法會出java.lang.IllegalMonitorStateException異常。另外還要注意在wait方法兩邊的同步塊會在wait執(zhí)行完畢之后釋放對象鎖。
這樣PrinterB就進入了等待狀態(tài),我們再看下PrinterA的run方法:
while (val <= 3) {
printAndIncrease();
}
// print 1,2,3 then notify printerB
synchronized (WaitNotifyDemo.this) {
System.out.println("PrinterA printed 1,2,3; notify PrinterB");
WaitNotifyDemo.this.notify();
}
try {
while (val <= 6) {
synchronized (WaitNotifyDemo.this) {
System.out.println("wait in printerA");
WaitNotifyDemo.this.wait();
}
}
System.out.println("wait end printerA");
} catch (InterruptedException e) {
e.printStackTrace();
}
這里首先打印了1、2、3,然后在同步塊中調用了WaitNotifyDemo實例的notify方法,這樣PrinterB就得到了繼續(xù)執(zhí)行的通知,然后PrinterA進入等待狀態(tài),等待PrinterB通知。
我們再看下PrinterB run方法剩下的代碼:
while (val <= 6) {
printAndIncrease();
}
System.out.println("notify in printerB");
synchronized (WaitNotifyDemo.this) {
WaitNotifyDemo.this.notify();
}
System.out.println("notify end printerB");
System.out.println("PrinterB exits.");
PrinterB首先打印了4、5、6,然后在同步塊中調用了notify方法,通知PrinterA開始執(zhí)行。
PrinterA得到通知后,停止等待,打印剩下的7、8、9三個數(shù)字,如下是PrinterA run方法中剩下的代碼:
while (val <= 9) {
printAndIncrease();
}
整個程序就分析完了,下面我們再來使用Condition來做這道題:
public class TestCondition {
static class NumberWrapper {
public int value = 1;
}
public static void main(String[] args) {
//初始化可重入鎖
final Lock lock = new ReentrantLock();
//第一個條件當屏幕上輸出到3
final Condition reachThreeCondition = lock.newCondition();
//第二個條件當屏幕上輸出到6
final Condition reachSixCondition = lock.newCondition();
//NumberWrapper只是為了封裝一個數(shù)字,一邊可以將數(shù)字對象共享,并可以設置為final
//注意這里不要用Integer, Integer 是不可變對象
final NumberWrapper num = new NumberWrapper();
//初始化A線程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//需要先獲得鎖
lock.lock();
try {
System.out.println("threadA start write");
//A線程先輸出前3個數(shù)
while (num.value <= 3) {
System.out.println(num.value);
num.value++;
}
//輸出到3時要signal,告訴B線程可以開始了
reachThreeCondition.signal();
} finally {
lock.unlock();
}
lock.lock();
try {
//等待輸出6的條件
reachSixCondition.await();
System.out.println("threadA start write");
//輸出剩余數(shù)字
while (num.value <= 9) {
System.out.println(num.value);
num.value++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
while (num.value <= 3) {
//等待3輸出完畢的信號
reachThreeCondition.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
try {
lock.lock();
//已經收到信號,開始輸出4,5,6
System.out.println("threadB start write");
while (num.value <= 6) {
System.out.println(num.value);
num.value++;
}
//4,5,6輸出完畢,告訴A線程6輸出完了
reachSixCondition.signal();
} finally {
lock.unlock();
}
}
});
//啟動兩個線程
threadB.start();
threadA.start();
}
}
基本思路就是首先要A線程先寫1,2,3,這時候B線程應該等待reachThredCondition信號,而當A線程寫完3之后就通過signal告訴B線程“我寫到3了,該你了”,這時候A線程要等嗲reachSixCondition信號,同時B線程得到通知,開始寫4,5,6,寫完4,5,6之后B線程通知A線程reachSixCondition條件成立了,這時候A線程就開始寫剩下的7,8,9了。
我們可以看到上例中我們創(chuàng)建了兩個Condition,在不同的情況下可以使用不同的Condition,與wait和notify相比提供了更細致的控制。
3.線程阻塞原語–LockSupport
我們一再提線程、鎖等概念,但鎖是如果實現(xiàn)的呢?又是如何知道當前阻塞線程的又是哪個對象呢?LockSupport是JDK中比較底層的類,用來創(chuàng)建鎖和其他同步工具類的基本線程阻塞原語。
java鎖和同步器框架的核心 AQS: AbstractQueuedSynchronizer,就是通過調用 LockSupport .park()和 LockSupport .unpark()實現(xiàn)線程的阻塞和喚醒 的。 LockSupport 很類似于二元信號量(只有1個許可證可供使用),如果這個許可還沒有被占用,當前線程獲取許可并繼 續(xù) 執(zhí)行;如果許可已經被占用,當前線 程阻塞,等待獲取許可。
LockSupport是針對特定線程來進行阻塞和解除阻塞操作的;而Object的wait()/notify()/notifyAll()是用來操作特定對象的等待集合的。
LockSupport的兩個主要方法是park()和Unpark(),我們來看一下他們的實現(xiàn):
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
unsafe.park(false, 0L);
setBlocker(t, null);
}
public static void park() {
unsafe.park(false, 0L);
}
public static void unpark(Thread thread) {
if (thread != null)
unsafe.unpark(thread);
}
由源碼我們可見在park方法內部首先獲得當前線程然后阻塞當前線程,unpark方法傳入一個可配置的線程來為該線程解鎖。以“線程”作為方法的參數(shù), 語義更清晰,使用起來也更方便。而wait/notify的實現(xiàn)使得“線程”的阻塞/喚醒對線程本身來說是被動的,要準確的控制哪個線程、什么時候阻塞/喚醒很困難, 要不隨機喚醒一個線程(notify)要不喚醒所有的(notifyAll)。
下面我們來看一個例子:
public class TestLockSupport {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super.setName(name);
}
public void run() {
synchronized (u) {
System.out.println("in" + getName());
LockSupport.park();
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(2000);
t2.start();
LockSupport.unpark(t1);
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}
當我們把”LockSupport.unpark(t1);”這一句注掉的話我們會發(fā)現(xiàn)程序陷入死鎖。而且我們看到再main方法中unpark是在t1和t2啟動之后才執(zhí)行,但是為什么t1啟動之后,t2也啟動了呢?注意,**unpark函數(shù)可以先于park調用。比如線程B調用unpark函數(shù),給線程A發(fā)了一個“許可”,那么當線程A調用park時,它發(fā)現(xiàn)已經有“許可”了,那么它會馬上再繼續(xù)運行。**unpark函數(shù)為線程提供“許可(permit)”,線程調用park函數(shù)則等待“許可”。這個有點像信號量,但是這個“許可”是不能疊加的,“許可”是一次性的。比如線程B連續(xù)調用了三次unpark函數(shù),當線程A調用park函數(shù)就使用掉這個“許可”,如果線程A再次調用park,則進入等待狀態(tài)。
除了有定時阻塞的功能外,還支持中斷影響,但是和其他接收中斷函數(shù)不一樣,他不會拋出
InterruptedException異常,他只會默默的返回,但是我們可以從Thread.Interrupted()等方法獲得中斷標記.
我們來看一個例子:
public class TestLockSupport {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super.setName(name);
}
public void run() {
synchronized (u) {
System.out.println("in " + getName());
LockSupport.park();
if (Thread.interrupted()) {
System.out.println(getName() + " 被中斷了!");
}
}
System.out.println(getName() + " 執(zhí)行結束");
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
t1.interrupt();
LockSupport.unpark(t2);
}
}
輸出:
in t1
t1 被中斷了!
t1 執(zhí)行結束
in t2
t2 執(zhí)行結束Process finished with exit code 0
由run方法中的終端異常捕獲我們可以看到線程在中斷時并沒有拋出異常而是正常執(zhí)行下去了。
關于LockSupport其實要介紹的東西還是很多,因為這個類實現(xiàn)了底層的一些方法,各種的鎖實現(xiàn)都是這個基礎上發(fā)展而來的。以后會專門用一個篇章來學習jdk內部的阻塞機制。說前面我們講到Object的wait和notify,講到Condition條件,講到jdk中不對外部暴露的LockSupport阻塞原語,那么在JUC包中還有另外一個阻塞機制—信號量機制(Semaphore),下一節(jié)我們一起探討一下。
以上就是java并發(fā)編程專題(五)----詳解(JUC)ReentrantLock的詳細內容,更多關于java ReentrantLock的資料請關注腳本之家其它相關文章!
相關文章
Java利用FileUtils讀取數(shù)據(jù)和寫入數(shù)據(jù)到文件
這篇文章主要介紹了Java利用FileUtils讀取數(shù)據(jù)和寫入數(shù)據(jù)到文件,下面文章圍繞FileUtils的相關資料展開怎么讀取數(shù)據(jù)和寫入數(shù)據(jù)到文件的內容,具有一定的參考價值,徐婭奧德小伙伴可以參考一下2021-12-12
Java 異常的棧軌跡(Stack Trace)詳解及實例代碼
這篇文章主要介紹了Java 異常的棧軌跡(Stack Trace)詳解及實例代碼的相關資料,需要的朋友可以參考下2017-03-03
IDEA遇到Internal error. Please refer to http://jb. gg/ide/crit
這篇文章主要介紹了IDEA遇到Internal error. Please refer to http://jb. gg/ide/critical-startup-errors的問題及解決辦法,本文通過圖文并茂的形式給大家介紹的非常詳細,需要的朋友可以參考下2020-08-08

