重入鎖簡單理解就是對同一個線程而言,它可以重復的獲取鎖。例如這個線程可以連續獲取兩次鎖,但是釋放鎖的次數也一定要是兩次。下面是一個簡單例子:
public class ReenterLock {
private static ReentrantLock lock = new ReentrantLock();
private static int i = 0;
// 循環1000000次
private static Runnable runnable = () -> IntStream.range(0, 1000000).forEach((j) -> {
lock.lock();
try {
i++;
} finally {
lock.unlock();
}
});
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
// 利用join,等thread1,thread2結束后,main線程才繼續運行,并打印 i
thread1.join();
thread2.join();
// 利用lock保護的 i,最終結果為 2000000,如果不加,則值肯定小于此數值
System.out.println(i);
}
}
從上面的代碼可以看到,相比于synchronized
,開發者必須手動指定鎖的位置和什么時候釋放鎖,這樣必然增加了靈活性。
線程中斷響應
如果線程阻塞于synchronized
,那么要么獲取到鎖,繼續執行,要么一直等待。重入鎖提供了另一種可能,就是中斷線程。下面的例子是利用兩個線程構建一個死鎖,然后中斷其中一個線程,使另一個線程獲取鎖的例子:
public class ReenterLockInterrupt {
private static ReentrantLock lock = new ReentrantLock();
private static Runnable runnable = () -> {
try {
// 利用 lockInterruptibly 申請鎖,這是可以進中斷申請的申請鎖操作
lock.lockInterruptibly();
// 睡眠20秒,在睡眠結束之前,main方法里要中斷thread2的獲取鎖操作
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
String threadName = Thread.currentThread().getName();
// 中斷后拋出異常,最后要釋放鎖
// 如果是線程1則釋放鎖,因為線程2就沒拿到鎖,所以不用釋放
if ("Thread-1".equals(threadName)) lock.unlock();
System.out.println(threadName+" 停止");
}
};
public static void main(String[] args) {
Thread thread1 = new Thread(runnable, "thread-1");
Thread thread2 = new Thread(runnable, "thread-2");
thread1.start();
// 讓主線程停一下,讓thread1獲取鎖后再啟動thread2
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 這里什么也不做
}
thread2.start();
thread2.interrupt();
}
}
thread-1拿到鎖之后,線程即持有鎖并等待20秒,然后thread-2啟動,并沒有拿到鎖,這時候中斷thread-2線程,線程2退出。
有限時間的等待鎖
顧名思義,簡單理解就是在指定的時間內如果拿不到鎖,則不再等待鎖。當持有鎖的線程出問題導致長時間持有鎖的時候,你不可能讓其他線程永遠等待其釋放鎖。下面是一個例子:
public class ReenterTryLock {
private static ReentrantLock reenterLock = new ReentrantLock();
private static Runnable runnable = () -> {
try {
// tryLock()方法會返回一個布爾值,獲取鎖成功則為true
if (reenterLock.tryLock(3, TimeUnit.SECONDS)) {
Thread.sleep(5000);
} else {
System.out.println(Thread.currentThread().getName() + "獲取鎖失敗");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 最后,如果當前前程在持有鎖,則釋放鎖
if (reenterLock.isHeldByCurrentThread()) {
System.out.println(Thread.currentThread().getName() + "釋放鎖了");
reenterLock.unlock();
}
}
};
public static void main(String[] args) {
Thread thread1 = new Thread(runnable, "thread-1");
Thread thread2 = new Thread(runnable, "thread-2");
thread1.start();
thread2.start();
}
}
這里使用tryLock()
第一個獲取鎖的線程,會停止5秒。而獲取鎖的設置為3秒獲取不到鎖則放棄,所以第二個去嘗試獲取鎖的線程是獲取不到鎖而被迫停止的。如果tryLock()
方法不傳入任何參數,那么獲取鎖的線程不會等待鎖,則立即返回false。
公平鎖與非公平鎖
當一個線程釋放鎖時,其他等待的線程則有機會獲取鎖,如果是公平鎖,則分先來后到的獲取鎖,如果是非公平鎖則誰搶到鎖算誰的,這就相當于排隊買東西和不排隊買東西是一個道理。Java的synchronized
關鍵字就是非公平鎖。
那么重入鎖ReentrantLock()
是公平鎖還是非公平鎖?
重入鎖ReentrantLock()
是可以設置公平性的,可以參考其構造方法:
// 通過傳入一個布爾值來設置公平鎖,為true則是公平鎖,false則為非公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
構建一個公平鎖需要維護一個有序隊列,如果實際需求用不到公平鎖則不需要使用公平鎖。下面用一個例子來演示公平鎖與非公平鎖的區別:
public class ReenterTryLockFair {
// 分別設置公平鎖和非公平鎖,分析打印結果
private static ReentrantLock lock = new ReentrantLock(true);
private static Runnable runnable = () -> {
while (true) {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + " 獲取了鎖");
} finally {
lock.unlock();
}
}
};
public static void main(String[] args) {
Thread thread1 = new Thread(runnable, "thread---1");
Thread thread2 = new Thread(runnable, "thread---2");
Thread thread3 = new Thread(runnable, "thread---3");
thread1.start();
thread2.start();
thread3.start();
}
}
當設置為true
即公平鎖的時候,可以看到打印非常規律,截取一段兒打印結果:
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
thread---1 獲取了鎖
thread---2 獲取了鎖
thread---3 獲取了鎖
可以看到,都是thread--1,thread--2,thread--3,無限循環下去,如果設置的為非公平鎖,打印結果就混亂沒有規律了:
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---3 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---2 獲取了鎖
thread---1 獲取了鎖
Condition
同jdk中的等待/通知機制類似,只不過Condition是用在重入鎖這里的。有了Condition,線程就可以在合適的時間等待,在合適的時間繼續執行。
Condition接口包含以下方法:
// 讓當前線程等待,并釋放鎖
void await() throws InterruptedException;
// 和await類似,但在等待過程中不會相應中斷
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 喚醒等待中的線程
void signal();
// 喚醒等待中的所有線程
void signalAll();
下面是一個簡單示例:
public class ReenterLockCondition {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static Runnable runnable = () -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "進入等待。。");
condition.await();
System.out.println(Thread.currentThread().getName() + "繼續執行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
};
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(runnable, "thread--1");
thread.start();
Thread.sleep(2000);
lock.lock();
condition.signal();
System.out.println("主線程發出信號");
lock.unlock();
}
}
thread--1啟動,拿到鎖,然后進入等待并且釋放鎖,2秒后,主線程拿到鎖,然后發出信號并釋放鎖,最后,thread--1繼續執行。下面是打印結果:
thread--1進入等待。。
主線程發出信號
thread--1繼續執行