'java高併發(12)JUC:ReentrantLock重入鎖'

Java 網絡爬蟲 大數據 Java虛擬機 BigDataKer 2019-08-14
"

synchronized的侷限性

synchronized是java內置的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由jvm實現,用戶不需要顯示的釋放鎖,非常方便,然而synchronized也有一定的侷限性,例如:

  1. 當線程嘗試獲取鎖的時候,如果獲取不到鎖會一直阻塞,這個阻塞的過程,用戶無法控制
  2. 如果獲取鎖的線程進入休眠或者阻塞,除非當前線程異常,否則其他線程嘗試獲取鎖必須一直等待

JDK1.5之後發佈,加入了Doug Lea實現的java.util.concurrent包。包內提供了Lock類,用來提供更多擴展的加鎖功能。Lock彌補了synchronized的侷限,提供了更加細粒度的加鎖功能。

ReentrantLock

ReentrantLock是Lock的默認實現,在聊ReentranLock之前,我們需要先弄清楚一些概念:

  1. 可重入鎖:可重入鎖是指同一個線程可以多次獲得同一把鎖;ReentrantLock和關鍵字Synchronized都是可重入鎖
  2. 可中斷鎖:可中斷鎖時只線程在獲取鎖的過程中,是否可以相應線程中斷操作。synchronized是不可中斷的,ReentrantLock是可中斷的
  3. 公平鎖和非公平鎖:公平鎖是指多個線程嘗試獲取同一把鎖的時候,獲取鎖的順序按照線程到達的先後順序獲取,而不是隨機插隊的方式獲取。synchronized是非公平鎖,而ReentrantLock是兩種都可以實現,不過默認是非公平鎖

ReentrantLock基本使用

我們使用3個線程來對一個共享變量++操作,先使用synchronized實現,然後使用ReentrantLock實現。

synchronized方式

package com.itsoku.chat06;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo2 {
private static int num = 0;
private static synchronized void add() {
num++;
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo2.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo2.num);
}
}

ReentrantLock方式:

package com.itsoku.chat06;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo3 {
private static int num = 0;
private static ReentrantLock lock = new ReentrantLock();
private static void add() {
lock.lock();
try {
num++;
} finally {
lock.unlock();
}
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo3.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo3.num);
}
}

ReentrantLock的使用過程:

  1. 創建鎖:ReentrantLock lock = new ReentrantLock();
  2. 獲取鎖:lock.lock()
  3. 釋放鎖:lock.unlock();

對比上面的代碼,與關鍵字synchronized相比,ReentrantLock鎖有明顯的操作過程,開發人員必須手動的指定何時加鎖,何時釋放鎖,正是因為這樣手動控制,ReentrantLock對邏輯控制的靈活度要遠遠勝於關鍵字synchronized,上面代碼需要注意lock.unlock()一定要放在finally中,否則,若程序出現了異常,鎖沒有釋放,那麼其他線程就再也沒有機會獲取這個鎖了。

ReentrantLock是可重入鎖

來驗證一下ReentrantLock是可重入鎖,實例代碼:

package com.itsoku.chat06;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo4 {
private static int num = 0;
private static ReentrantLock lock = new ReentrantLock();
private static void add() {
lock.lock();
lock.lock();
try {
num++;
} finally {
lock.unlock();
lock.unlock();
}
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo4.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo4.num);
}
}

上面代碼中add()方法中,當一個線程進入的時候,會執行2次獲取鎖的操作,運行程序可以正常結束,並輸出和期望值一樣的30000,假如ReentrantLock是不可重入的鎖,那麼同一個線程第2次獲取鎖的時候由於前面的鎖還未釋放而導致死鎖,程序是無法正常結束的。ReentrantLock命名也挺好的Re entrant Lock,和其名字一樣,可重入鎖。

代碼中還有幾點需要注意:

  1. lock()方法和unlock()方法需要成對出現,鎖了幾次,也要釋放幾次,否則後面的線程無法獲取鎖了;可以將add中的unlock刪除一個事實,上面代碼運行將無法結束
  2. unlock()方法放在finally中執行,保證不管程序是否有異常,鎖必定會釋放

ReentrantLock實現公平鎖

在大多數情況下,鎖的申請都是非公平的,也就是說,線程1首先請求鎖A,接著線程2也請求了鎖A。那麼當鎖A可用時,是線程1可獲得鎖還是線程2可獲得鎖呢?這是不一定的,系統只是會從這個鎖的等待隊列中隨機挑選一個,因此不能保證其公平性。這就好比買票不排隊,大家都圍在售票窗口前,售票員忙的焦頭爛額,也顧及不上誰先誰後,隨便找個人出票就完事了,最終導致的結果是,有些人可能一直買不到票。而公平鎖,則不是這樣,它會按照到達的先後順序獲得資源。公平鎖的一大特點是:它不會產生飢餓現象,只要你排隊,最終還是可以等到資源的;synchronized關鍵字默認是有jvm內部實現控制的,是非公平鎖。而ReentrantLock運行開發者自己設置鎖的公平性。

看一下jdk中ReentrantLock的源碼,2個構造方法:

public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

默認構造方法創建的是非公平鎖。

第2個構造方法,有個fair參數,當fair為true的時候創建的是公平鎖,公平鎖看起來很不錯,不過要實現公平鎖,系統內部肯定需要維護一個有序隊列,因此公平鎖的實現成本比較高,性能相對於非公平鎖來說相對低一些。因此,在默認情況下,鎖是非公平的,如果沒有特別要求,則不建議使用公平鎖。

公平鎖和非公平鎖在程序調度上是很不一樣,來一個公平鎖示例看一下:

package com.itsoku.chat06;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo5 {
private static int num = 0;
private static ReentrantLock fairLock = new ReentrantLock(true);
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
fairLock.lock();
try {
System.out.println(this.getName() + "獲得鎖!");
} finally {
fairLock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
T t3 = new T("t3");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}

運行結果輸出:

t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!

看一下輸出的結果,鎖時按照先後順序獲得的。

修改一下上面代碼,改為非公平鎖試試,如下:

ReentrantLock fairLock = new ReentrantLock(false);

運行結果如下:

t1獲得鎖!
t3獲得鎖!
t3獲得鎖!
t3獲得鎖!
t3獲得鎖!
t1獲得鎖!
t1獲得鎖!
t1獲得鎖!
t1獲得鎖!
t2獲得鎖!
t2獲得鎖!
t2獲得鎖!
t2獲得鎖!
t2獲得鎖!
t3獲得鎖!

可以看到t3可能會連續獲得鎖,結果是比較隨機的,不公平的。

ReentrantLock獲取鎖的過程是可中斷的

對於synchronized關鍵字,如果一個線程在等待獲取鎖,最終只有2種結果:

  1. 要麼獲取到鎖然後繼續後面的操作
  2. 要麼一直等待,直到其他線程釋放鎖為止

而ReentrantLock提供了另外一種可能,就是在等的獲取鎖的過程中(發起獲取鎖請求到還未獲取到鎖這段時間內)是可以被中斷的,也就是說在等待鎖的過程中,程序可以根據需要取消獲取鎖的請求。有些使用這個操作是非常有必要的。比如:你和好朋友越好一起去打球,如果你等了半小時朋友還沒到,突然你接到一個電話,朋友由於突發狀況,不能來了,那麼你一定達到回府。中斷操作正是提供了一套類似的機制,如果一個線程正在等待獲取鎖,那麼它依然可以收到一個通知,被告知無需等待,可以停止工作了。

示例代碼:

package com.itsoku.chat06;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo6 {
private static ReentrantLock lock1 = new ReentrantLock(false);
private static ReentrantLock lock2 = new ReentrantLock(false);
public static class T extends Thread {
int lock;
public T(String name, int lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
if (this.lock == 1) {
lock1.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
System.out.println("中斷標誌:" + this.isInterrupted());
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1", 1);
T t2 = new T("t2", 2);
t1.start();
t2.start();
}
}

先運行一下上面代碼,發現程序無法結束,使用jstack查看線程堆棧信息,發現2個線程死鎖了。

Found one Java-level deadlock:
=============================
"t2":
waiting for ownable synchronizer 0x0000000717380c20, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "t1"
"t1":
waiting for ownable synchronizer 0x0000000717380c50, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "t2"

lock1被線程t1佔用,lock2倍線程t2佔用,線程t1在等待獲取lock2,線程t2在等待獲取lock1,都在相互等待獲取對方持有的鎖,最終產生了死鎖,如果是在synchronized關鍵字情況下發生了死鎖現象,程序是無法結束的。

我們隊上面代碼改造一下,線程t2一直無法獲取到lock1,那麼等待5秒之後,我們中斷獲取鎖的操作。主要修改一下main方法,如下:

T t1 = new T("t1", 1);
T t2 = new T("t2", 2);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(5);
t2.interrupt();

新增了2行代碼TimeUnit.SECONDS.sleep(5);t2.interrupt();,程序可以結束了,運行結果:

java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.itsoku.chat06.Demo6$T.run(Demo6.java:31)
中斷標誌:false

從上面信息中可以看出,代碼的31行觸發了異常,中斷標誌輸出:false

"

synchronized的侷限性

synchronized是java內置的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由jvm實現,用戶不需要顯示的釋放鎖,非常方便,然而synchronized也有一定的侷限性,例如:

  1. 當線程嘗試獲取鎖的時候,如果獲取不到鎖會一直阻塞,這個阻塞的過程,用戶無法控制
  2. 如果獲取鎖的線程進入休眠或者阻塞,除非當前線程異常,否則其他線程嘗試獲取鎖必須一直等待

JDK1.5之後發佈,加入了Doug Lea實現的java.util.concurrent包。包內提供了Lock類,用來提供更多擴展的加鎖功能。Lock彌補了synchronized的侷限,提供了更加細粒度的加鎖功能。

ReentrantLock

ReentrantLock是Lock的默認實現,在聊ReentranLock之前,我們需要先弄清楚一些概念:

  1. 可重入鎖:可重入鎖是指同一個線程可以多次獲得同一把鎖;ReentrantLock和關鍵字Synchronized都是可重入鎖
  2. 可中斷鎖:可中斷鎖時只線程在獲取鎖的過程中,是否可以相應線程中斷操作。synchronized是不可中斷的,ReentrantLock是可中斷的
  3. 公平鎖和非公平鎖:公平鎖是指多個線程嘗試獲取同一把鎖的時候,獲取鎖的順序按照線程到達的先後順序獲取,而不是隨機插隊的方式獲取。synchronized是非公平鎖,而ReentrantLock是兩種都可以實現,不過默認是非公平鎖

ReentrantLock基本使用

我們使用3個線程來對一個共享變量++操作,先使用synchronized實現,然後使用ReentrantLock實現。

synchronized方式

package com.itsoku.chat06;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo2 {
private static int num = 0;
private static synchronized void add() {
num++;
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo2.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo2.num);
}
}

ReentrantLock方式:

package com.itsoku.chat06;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo3 {
private static int num = 0;
private static ReentrantLock lock = new ReentrantLock();
private static void add() {
lock.lock();
try {
num++;
} finally {
lock.unlock();
}
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo3.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo3.num);
}
}

ReentrantLock的使用過程:

  1. 創建鎖:ReentrantLock lock = new ReentrantLock();
  2. 獲取鎖:lock.lock()
  3. 釋放鎖:lock.unlock();

對比上面的代碼,與關鍵字synchronized相比,ReentrantLock鎖有明顯的操作過程,開發人員必須手動的指定何時加鎖,何時釋放鎖,正是因為這樣手動控制,ReentrantLock對邏輯控制的靈活度要遠遠勝於關鍵字synchronized,上面代碼需要注意lock.unlock()一定要放在finally中,否則,若程序出現了異常,鎖沒有釋放,那麼其他線程就再也沒有機會獲取這個鎖了。

ReentrantLock是可重入鎖

來驗證一下ReentrantLock是可重入鎖,實例代碼:

package com.itsoku.chat06;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo4 {
private static int num = 0;
private static ReentrantLock lock = new ReentrantLock();
private static void add() {
lock.lock();
lock.lock();
try {
num++;
} finally {
lock.unlock();
lock.unlock();
}
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo4.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo4.num);
}
}

上面代碼中add()方法中,當一個線程進入的時候,會執行2次獲取鎖的操作,運行程序可以正常結束,並輸出和期望值一樣的30000,假如ReentrantLock是不可重入的鎖,那麼同一個線程第2次獲取鎖的時候由於前面的鎖還未釋放而導致死鎖,程序是無法正常結束的。ReentrantLock命名也挺好的Re entrant Lock,和其名字一樣,可重入鎖。

代碼中還有幾點需要注意:

  1. lock()方法和unlock()方法需要成對出現,鎖了幾次,也要釋放幾次,否則後面的線程無法獲取鎖了;可以將add中的unlock刪除一個事實,上面代碼運行將無法結束
  2. unlock()方法放在finally中執行,保證不管程序是否有異常,鎖必定會釋放

ReentrantLock實現公平鎖

在大多數情況下,鎖的申請都是非公平的,也就是說,線程1首先請求鎖A,接著線程2也請求了鎖A。那麼當鎖A可用時,是線程1可獲得鎖還是線程2可獲得鎖呢?這是不一定的,系統只是會從這個鎖的等待隊列中隨機挑選一個,因此不能保證其公平性。這就好比買票不排隊,大家都圍在售票窗口前,售票員忙的焦頭爛額,也顧及不上誰先誰後,隨便找個人出票就完事了,最終導致的結果是,有些人可能一直買不到票。而公平鎖,則不是這樣,它會按照到達的先後順序獲得資源。公平鎖的一大特點是:它不會產生飢餓現象,只要你排隊,最終還是可以等到資源的;synchronized關鍵字默認是有jvm內部實現控制的,是非公平鎖。而ReentrantLock運行開發者自己設置鎖的公平性。

看一下jdk中ReentrantLock的源碼,2個構造方法:

public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

默認構造方法創建的是非公平鎖。

第2個構造方法,有個fair參數,當fair為true的時候創建的是公平鎖,公平鎖看起來很不錯,不過要實現公平鎖,系統內部肯定需要維護一個有序隊列,因此公平鎖的實現成本比較高,性能相對於非公平鎖來說相對低一些。因此,在默認情況下,鎖是非公平的,如果沒有特別要求,則不建議使用公平鎖。

公平鎖和非公平鎖在程序調度上是很不一樣,來一個公平鎖示例看一下:

package com.itsoku.chat06;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo5 {
private static int num = 0;
private static ReentrantLock fairLock = new ReentrantLock(true);
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
fairLock.lock();
try {
System.out.println(this.getName() + "獲得鎖!");
} finally {
fairLock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
T t3 = new T("t3");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}

運行結果輸出:

t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!

看一下輸出的結果,鎖時按照先後順序獲得的。

修改一下上面代碼,改為非公平鎖試試,如下:

ReentrantLock fairLock = new ReentrantLock(false);

運行結果如下:

t1獲得鎖!
t3獲得鎖!
t3獲得鎖!
t3獲得鎖!
t3獲得鎖!
t1獲得鎖!
t1獲得鎖!
t1獲得鎖!
t1獲得鎖!
t2獲得鎖!
t2獲得鎖!
t2獲得鎖!
t2獲得鎖!
t2獲得鎖!
t3獲得鎖!

可以看到t3可能會連續獲得鎖,結果是比較隨機的,不公平的。

ReentrantLock獲取鎖的過程是可中斷的

對於synchronized關鍵字,如果一個線程在等待獲取鎖,最終只有2種結果:

  1. 要麼獲取到鎖然後繼續後面的操作
  2. 要麼一直等待,直到其他線程釋放鎖為止

而ReentrantLock提供了另外一種可能,就是在等的獲取鎖的過程中(發起獲取鎖請求到還未獲取到鎖這段時間內)是可以被中斷的,也就是說在等待鎖的過程中,程序可以根據需要取消獲取鎖的請求。有些使用這個操作是非常有必要的。比如:你和好朋友越好一起去打球,如果你等了半小時朋友還沒到,突然你接到一個電話,朋友由於突發狀況,不能來了,那麼你一定達到回府。中斷操作正是提供了一套類似的機制,如果一個線程正在等待獲取鎖,那麼它依然可以收到一個通知,被告知無需等待,可以停止工作了。

示例代碼:

package com.itsoku.chat06;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo6 {
private static ReentrantLock lock1 = new ReentrantLock(false);
private static ReentrantLock lock2 = new ReentrantLock(false);
public static class T extends Thread {
int lock;
public T(String name, int lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
if (this.lock == 1) {
lock1.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
System.out.println("中斷標誌:" + this.isInterrupted());
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1", 1);
T t2 = new T("t2", 2);
t1.start();
t2.start();
}
}

先運行一下上面代碼,發現程序無法結束,使用jstack查看線程堆棧信息,發現2個線程死鎖了。

Found one Java-level deadlock:
=============================
"t2":
waiting for ownable synchronizer 0x0000000717380c20, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "t1"
"t1":
waiting for ownable synchronizer 0x0000000717380c50, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "t2"

lock1被線程t1佔用,lock2倍線程t2佔用,線程t1在等待獲取lock2,線程t2在等待獲取lock1,都在相互等待獲取對方持有的鎖,最終產生了死鎖,如果是在synchronized關鍵字情況下發生了死鎖現象,程序是無法結束的。

我們隊上面代碼改造一下,線程t2一直無法獲取到lock1,那麼等待5秒之後,我們中斷獲取鎖的操作。主要修改一下main方法,如下:

T t1 = new T("t1", 1);
T t2 = new T("t2", 2);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(5);
t2.interrupt();

新增了2行代碼TimeUnit.SECONDS.sleep(5);t2.interrupt();,程序可以結束了,運行結果:

java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.itsoku.chat06.Demo6$T.run(Demo6.java:31)
中斷標誌:false

從上面信息中可以看出,代碼的31行觸發了異常,中斷標誌輸出:false

java高併發(12)JUC:ReentrantLock重入鎖

t2在31行一直獲取不到lock1的鎖,主線程中等待了5秒之後,t2線程調用了interrupt()方法,將線程的中斷標誌置為true,此時31行會觸發InterruptedException異常,然後線程t2可以繼續向下執行,釋放了lock2的鎖,然後線程t1可以正常獲取鎖,程序得以繼續進行。線程發送中斷信號觸發InterruptedException異常之後,中斷標誌將被清空。

關於獲取鎖的過程中被中斷,注意幾點:

  1. ReentrankLock中必須使用實例方法lockInterruptibly()獲取鎖時,在線程調用interrupt()方法之後,才會引發InterruptedException異常
  2. 線程調用interrupt()之後,線程的中斷標誌會被置為true
  3. 觸發InterruptedException異常之後,線程的中斷標誌有會被清空,即置為false
  4. 所以當線程調用interrupt()引發InterruptedException異常,中斷標誌的變化是:false->true->false

ReentrantLock鎖申請等待限時

申請鎖等待限時是什麼意思?一般情況下,獲取鎖的時間我們是不知道的,synchronized關鍵字獲取鎖的過程中,只能等待其他線程把鎖釋放之後才能夠有機會獲取到所。所以獲取鎖的時間有長有短。如果獲取鎖的時間能夠設置超時時間,那就非常好了。

ReentrantLock剛好提供了這樣功能,給我們提供了獲取鎖限時等待的方法tryLock(),可以選擇傳入時間參數,表示等待指定的時間,無參則表示立即返回鎖申請的結果:true表示獲取鎖成功,false表示獲取鎖失敗。

tryLock無參方法

看一下源碼中tryLock方法:

public boolean tryLock()

返回boolean類型的值,此方法會立即返回,結果表示獲取鎖是否成功,示例:

package com.itsoku.chat06;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo8 {
private static ReentrantLock lock1 = new ReentrantLock(false);
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
try {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "開始獲取鎖!");
//獲取鎖超時時間設置為3秒,3秒內是否能否獲取鎖都會返回
if (lock1.tryLock()) {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "獲取到了鎖!");
//獲取到鎖之後,休眠5秒
TimeUnit.SECONDS.sleep(5);
} else {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "未能獲取到鎖!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
t1.start();
t2.start();
}
}

代碼中獲取鎖成功之後,休眠5秒,會導致另外一個線程獲取鎖失敗,運行代碼,輸出:

1563356291081:t2開始獲取鎖!
1563356291081:t2獲取到了鎖!
1563356291081:t1開始獲取鎖!
1563356291081:t1未能獲取到鎖!

可以看到t2獲取成功,t1獲取失敗了,tryLock()是立即響應的,中間不會有阻塞。

tryLock有參方法

可以明確設置獲取鎖的超時時間,該方法簽名:

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

該方法在指定的時間內不管是否可以獲取鎖,都會返回結果,返回true,表示獲取鎖成功,返回false表示獲取失敗。此方法由2個參數,第一個參數是時間類型,是一個枚舉,可以表示時、分、秒、毫秒等待,使用比較方便,第1個參數表示在時間類型上的時間長短。此方法在執行的過程中,如果調用了線程的中斷interrupt()方法,會觸發InterruptedException異常。

示例:

package com.itsoku.chat06;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo7 {
private static ReentrantLock lock1 = new ReentrantLock(false);
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
try {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "開始獲取鎖!");
//獲取鎖超時時間設置為3秒,3秒內是否能否獲取鎖都會返回
if (lock1.tryLock(3, TimeUnit.SECONDS)) {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "獲取到了鎖!");
//獲取到鎖之後,休眠5秒
TimeUnit.SECONDS.sleep(5);
} else {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "未能獲取到鎖!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
t1.start();
t2.start();
}
}

程序中調用了ReentrantLock的實例方法tryLock(3, TimeUnit.SECONDS),表示獲取鎖的超時時間是3秒,3秒後不管是否能否獲取鎖,該方法都會有返回值,獲取到鎖之後,內部休眠了5秒,會導致另外一個線程獲取鎖失敗。

運行程序,輸出:

1563355512901:t2開始獲取鎖!
1563355512901:t1開始獲取鎖!
1563355512902:t2獲取到了鎖!
1563355515904:t1未能獲取到鎖!

輸出結果中分析,t2獲取到鎖了,然後休眠了5秒,t1獲取鎖失敗,t1打印了2條信息,時間相差3秒左右。

關於tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,說明一下:

  1. 都會返回boolean值,結果表示獲取鎖是否成功
  2. tryLock()方法,不管是否獲取成功,都會立即返回;而有參的tryLock方法會嘗試在指定的時間內去獲取鎖,中間會阻塞的現象,在指定的時間之後會不管是否能夠獲取鎖都會返回結果
  3. tryLock()方法不會響應線程的中斷方法;而有參的tryLock方法會響應線程的中斷方法,而出發InterruptedException異常,這個從2個方法的聲明上可以可以看出來

ReentrantLock其他常用的方法

  1. isHeldByCurrentThread:實例方法,判斷當前線程是否持有ReentrantLock的鎖,上面代碼中有使用過。

獲取鎖的4中方法對比

"

synchronized的侷限性

synchronized是java內置的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由jvm實現,用戶不需要顯示的釋放鎖,非常方便,然而synchronized也有一定的侷限性,例如:

  1. 當線程嘗試獲取鎖的時候,如果獲取不到鎖會一直阻塞,這個阻塞的過程,用戶無法控制
  2. 如果獲取鎖的線程進入休眠或者阻塞,除非當前線程異常,否則其他線程嘗試獲取鎖必須一直等待

JDK1.5之後發佈,加入了Doug Lea實現的java.util.concurrent包。包內提供了Lock類,用來提供更多擴展的加鎖功能。Lock彌補了synchronized的侷限,提供了更加細粒度的加鎖功能。

ReentrantLock

ReentrantLock是Lock的默認實現,在聊ReentranLock之前,我們需要先弄清楚一些概念:

  1. 可重入鎖:可重入鎖是指同一個線程可以多次獲得同一把鎖;ReentrantLock和關鍵字Synchronized都是可重入鎖
  2. 可中斷鎖:可中斷鎖時只線程在獲取鎖的過程中,是否可以相應線程中斷操作。synchronized是不可中斷的,ReentrantLock是可中斷的
  3. 公平鎖和非公平鎖:公平鎖是指多個線程嘗試獲取同一把鎖的時候,獲取鎖的順序按照線程到達的先後順序獲取,而不是隨機插隊的方式獲取。synchronized是非公平鎖,而ReentrantLock是兩種都可以實現,不過默認是非公平鎖

ReentrantLock基本使用

我們使用3個線程來對一個共享變量++操作,先使用synchronized實現,然後使用ReentrantLock實現。

synchronized方式

package com.itsoku.chat06;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo2 {
private static int num = 0;
private static synchronized void add() {
num++;
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo2.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo2.num);
}
}

ReentrantLock方式:

package com.itsoku.chat06;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo3 {
private static int num = 0;
private static ReentrantLock lock = new ReentrantLock();
private static void add() {
lock.lock();
try {
num++;
} finally {
lock.unlock();
}
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo3.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo3.num);
}
}

ReentrantLock的使用過程:

  1. 創建鎖:ReentrantLock lock = new ReentrantLock();
  2. 獲取鎖:lock.lock()
  3. 釋放鎖:lock.unlock();

對比上面的代碼,與關鍵字synchronized相比,ReentrantLock鎖有明顯的操作過程,開發人員必須手動的指定何時加鎖,何時釋放鎖,正是因為這樣手動控制,ReentrantLock對邏輯控制的靈活度要遠遠勝於關鍵字synchronized,上面代碼需要注意lock.unlock()一定要放在finally中,否則,若程序出現了異常,鎖沒有釋放,那麼其他線程就再也沒有機會獲取這個鎖了。

ReentrantLock是可重入鎖

來驗證一下ReentrantLock是可重入鎖,實例代碼:

package com.itsoku.chat06;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo4 {
private static int num = 0;
private static ReentrantLock lock = new ReentrantLock();
private static void add() {
lock.lock();
lock.lock();
try {
num++;
} finally {
lock.unlock();
lock.unlock();
}
}
public static class T extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
Demo4.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
T t2 = new T();
T t3 = new T();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println(Demo4.num);
}
}

上面代碼中add()方法中,當一個線程進入的時候,會執行2次獲取鎖的操作,運行程序可以正常結束,並輸出和期望值一樣的30000,假如ReentrantLock是不可重入的鎖,那麼同一個線程第2次獲取鎖的時候由於前面的鎖還未釋放而導致死鎖,程序是無法正常結束的。ReentrantLock命名也挺好的Re entrant Lock,和其名字一樣,可重入鎖。

代碼中還有幾點需要注意:

  1. lock()方法和unlock()方法需要成對出現,鎖了幾次,也要釋放幾次,否則後面的線程無法獲取鎖了;可以將add中的unlock刪除一個事實,上面代碼運行將無法結束
  2. unlock()方法放在finally中執行,保證不管程序是否有異常,鎖必定會釋放

ReentrantLock實現公平鎖

在大多數情況下,鎖的申請都是非公平的,也就是說,線程1首先請求鎖A,接著線程2也請求了鎖A。那麼當鎖A可用時,是線程1可獲得鎖還是線程2可獲得鎖呢?這是不一定的,系統只是會從這個鎖的等待隊列中隨機挑選一個,因此不能保證其公平性。這就好比買票不排隊,大家都圍在售票窗口前,售票員忙的焦頭爛額,也顧及不上誰先誰後,隨便找個人出票就完事了,最終導致的結果是,有些人可能一直買不到票。而公平鎖,則不是這樣,它會按照到達的先後順序獲得資源。公平鎖的一大特點是:它不會產生飢餓現象,只要你排隊,最終還是可以等到資源的;synchronized關鍵字默認是有jvm內部實現控制的,是非公平鎖。而ReentrantLock運行開發者自己設置鎖的公平性。

看一下jdk中ReentrantLock的源碼,2個構造方法:

public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

默認構造方法創建的是非公平鎖。

第2個構造方法,有個fair參數,當fair為true的時候創建的是公平鎖,公平鎖看起來很不錯,不過要實現公平鎖,系統內部肯定需要維護一個有序隊列,因此公平鎖的實現成本比較高,性能相對於非公平鎖來說相對低一些。因此,在默認情況下,鎖是非公平的,如果沒有特別要求,則不建議使用公平鎖。

公平鎖和非公平鎖在程序調度上是很不一樣,來一個公平鎖示例看一下:

package com.itsoku.chat06;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo5 {
private static int num = 0;
private static ReentrantLock fairLock = new ReentrantLock(true);
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
fairLock.lock();
try {
System.out.println(this.getName() + "獲得鎖!");
} finally {
fairLock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
T t3 = new T("t3");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}

運行結果輸出:

t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!
t1獲得鎖!
t2獲得鎖!
t3獲得鎖!

看一下輸出的結果,鎖時按照先後順序獲得的。

修改一下上面代碼,改為非公平鎖試試,如下:

ReentrantLock fairLock = new ReentrantLock(false);

運行結果如下:

t1獲得鎖!
t3獲得鎖!
t3獲得鎖!
t3獲得鎖!
t3獲得鎖!
t1獲得鎖!
t1獲得鎖!
t1獲得鎖!
t1獲得鎖!
t2獲得鎖!
t2獲得鎖!
t2獲得鎖!
t2獲得鎖!
t2獲得鎖!
t3獲得鎖!

可以看到t3可能會連續獲得鎖,結果是比較隨機的,不公平的。

ReentrantLock獲取鎖的過程是可中斷的

對於synchronized關鍵字,如果一個線程在等待獲取鎖,最終只有2種結果:

  1. 要麼獲取到鎖然後繼續後面的操作
  2. 要麼一直等待,直到其他線程釋放鎖為止

而ReentrantLock提供了另外一種可能,就是在等的獲取鎖的過程中(發起獲取鎖請求到還未獲取到鎖這段時間內)是可以被中斷的,也就是說在等待鎖的過程中,程序可以根據需要取消獲取鎖的請求。有些使用這個操作是非常有必要的。比如:你和好朋友越好一起去打球,如果你等了半小時朋友還沒到,突然你接到一個電話,朋友由於突發狀況,不能來了,那麼你一定達到回府。中斷操作正是提供了一套類似的機制,如果一個線程正在等待獲取鎖,那麼它依然可以收到一個通知,被告知無需等待,可以停止工作了。

示例代碼:

package com.itsoku.chat06;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo6 {
private static ReentrantLock lock1 = new ReentrantLock(false);
private static ReentrantLock lock2 = new ReentrantLock(false);
public static class T extends Thread {
int lock;
public T(String name, int lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
if (this.lock == 1) {
lock1.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
System.out.println("中斷標誌:" + this.isInterrupted());
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1", 1);
T t2 = new T("t2", 2);
t1.start();
t2.start();
}
}

先運行一下上面代碼,發現程序無法結束,使用jstack查看線程堆棧信息,發現2個線程死鎖了。

Found one Java-level deadlock:
=============================
"t2":
waiting for ownable synchronizer 0x0000000717380c20, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "t1"
"t1":
waiting for ownable synchronizer 0x0000000717380c50, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
which is held by "t2"

lock1被線程t1佔用,lock2倍線程t2佔用,線程t1在等待獲取lock2,線程t2在等待獲取lock1,都在相互等待獲取對方持有的鎖,最終產生了死鎖,如果是在synchronized關鍵字情況下發生了死鎖現象,程序是無法結束的。

我們隊上面代碼改造一下,線程t2一直無法獲取到lock1,那麼等待5秒之後,我們中斷獲取鎖的操作。主要修改一下main方法,如下:

T t1 = new T("t1", 1);
T t2 = new T("t2", 2);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(5);
t2.interrupt();

新增了2行代碼TimeUnit.SECONDS.sleep(5);t2.interrupt();,程序可以結束了,運行結果:

java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.itsoku.chat06.Demo6$T.run(Demo6.java:31)
中斷標誌:false

從上面信息中可以看出,代碼的31行觸發了異常,中斷標誌輸出:false

java高併發(12)JUC:ReentrantLock重入鎖

t2在31行一直獲取不到lock1的鎖,主線程中等待了5秒之後,t2線程調用了interrupt()方法,將線程的中斷標誌置為true,此時31行會觸發InterruptedException異常,然後線程t2可以繼續向下執行,釋放了lock2的鎖,然後線程t1可以正常獲取鎖,程序得以繼續進行。線程發送中斷信號觸發InterruptedException異常之後,中斷標誌將被清空。

關於獲取鎖的過程中被中斷,注意幾點:

  1. ReentrankLock中必須使用實例方法lockInterruptibly()獲取鎖時,在線程調用interrupt()方法之後,才會引發InterruptedException異常
  2. 線程調用interrupt()之後,線程的中斷標誌會被置為true
  3. 觸發InterruptedException異常之後,線程的中斷標誌有會被清空,即置為false
  4. 所以當線程調用interrupt()引發InterruptedException異常,中斷標誌的變化是:false->true->false

ReentrantLock鎖申請等待限時

申請鎖等待限時是什麼意思?一般情況下,獲取鎖的時間我們是不知道的,synchronized關鍵字獲取鎖的過程中,只能等待其他線程把鎖釋放之後才能夠有機會獲取到所。所以獲取鎖的時間有長有短。如果獲取鎖的時間能夠設置超時時間,那就非常好了。

ReentrantLock剛好提供了這樣功能,給我們提供了獲取鎖限時等待的方法tryLock(),可以選擇傳入時間參數,表示等待指定的時間,無參則表示立即返回鎖申請的結果:true表示獲取鎖成功,false表示獲取鎖失敗。

tryLock無參方法

看一下源碼中tryLock方法:

public boolean tryLock()

返回boolean類型的值,此方法會立即返回,結果表示獲取鎖是否成功,示例:

package com.itsoku.chat06;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo8 {
private static ReentrantLock lock1 = new ReentrantLock(false);
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
try {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "開始獲取鎖!");
//獲取鎖超時時間設置為3秒,3秒內是否能否獲取鎖都會返回
if (lock1.tryLock()) {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "獲取到了鎖!");
//獲取到鎖之後,休眠5秒
TimeUnit.SECONDS.sleep(5);
} else {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "未能獲取到鎖!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
t1.start();
t2.start();
}
}

代碼中獲取鎖成功之後,休眠5秒,會導致另外一個線程獲取鎖失敗,運行代碼,輸出:

1563356291081:t2開始獲取鎖!
1563356291081:t2獲取到了鎖!
1563356291081:t1開始獲取鎖!
1563356291081:t1未能獲取到鎖!

可以看到t2獲取成功,t1獲取失敗了,tryLock()是立即響應的,中間不會有阻塞。

tryLock有參方法

可以明確設置獲取鎖的超時時間,該方法簽名:

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

該方法在指定的時間內不管是否可以獲取鎖,都會返回結果,返回true,表示獲取鎖成功,返回false表示獲取失敗。此方法由2個參數,第一個參數是時間類型,是一個枚舉,可以表示時、分、秒、毫秒等待,使用比較方便,第1個參數表示在時間類型上的時間長短。此方法在執行的過程中,如果調用了線程的中斷interrupt()方法,會觸發InterruptedException異常。

示例:

package com.itsoku.chat06;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* 微信公眾號:路人甲Java,專注於java技術分享(帶你玩轉 爬蟲、分佈式事務、異步消息服務、任務調度、分庫分表、大數據等),喜歡請關注!
*/
public class Demo7 {
private static ReentrantLock lock1 = new ReentrantLock(false);
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
try {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "開始獲取鎖!");
//獲取鎖超時時間設置為3秒,3秒內是否能否獲取鎖都會返回
if (lock1.tryLock(3, TimeUnit.SECONDS)) {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "獲取到了鎖!");
//獲取到鎖之後,休眠5秒
TimeUnit.SECONDS.sleep(5);
} else {
System.out.println(System.currentTimeMillis() + ":" + this.getName() + "未能獲取到鎖!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
t1.start();
t2.start();
}
}

程序中調用了ReentrantLock的實例方法tryLock(3, TimeUnit.SECONDS),表示獲取鎖的超時時間是3秒,3秒後不管是否能否獲取鎖,該方法都會有返回值,獲取到鎖之後,內部休眠了5秒,會導致另外一個線程獲取鎖失敗。

運行程序,輸出:

1563355512901:t2開始獲取鎖!
1563355512901:t1開始獲取鎖!
1563355512902:t2獲取到了鎖!
1563355515904:t1未能獲取到鎖!

輸出結果中分析,t2獲取到鎖了,然後休眠了5秒,t1獲取鎖失敗,t1打印了2條信息,時間相差3秒左右。

關於tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,說明一下:

  1. 都會返回boolean值,結果表示獲取鎖是否成功
  2. tryLock()方法,不管是否獲取成功,都會立即返回;而有參的tryLock方法會嘗試在指定的時間內去獲取鎖,中間會阻塞的現象,在指定的時間之後會不管是否能夠獲取鎖都會返回結果
  3. tryLock()方法不會響應線程的中斷方法;而有參的tryLock方法會響應線程的中斷方法,而出發InterruptedException異常,這個從2個方法的聲明上可以可以看出來

ReentrantLock其他常用的方法

  1. isHeldByCurrentThread:實例方法,判斷當前線程是否持有ReentrantLock的鎖,上面代碼中有使用過。

獲取鎖的4中方法對比

java高併發(12)JUC:ReentrantLock重入鎖

總結

  1. ReentrantLock可以實現公平鎖和非公平鎖
  2. ReentrantLock默認實現的是非公平鎖
  3. ReentrantLock的獲取鎖和釋放鎖必須成對出現,鎖了幾次,也要釋放幾次
  4. 釋放鎖的操作必須放在finally中執行
  5. lockInterruptibly()實例方法可以相應線程的中斷方法,調用線程的interrupt()方法時,lockInterruptibly()方法會觸發InterruptedException異常
  6. 關於InterruptedException異常說一下,看到方法聲明上帶有 throws InterruptedException,表示該方法可以相應線程中斷,調用線程的interrupt()方法時,這些方法會觸發InterruptedException異常,觸發InterruptedException時,線程的中斷中斷狀態會被清除。所以如果程序由於調用interrupt()方法而觸發InterruptedException異常,線程的標誌由默認的false變為ture,然後又變為false
  7. 實例方法tryLock()獲會嘗試獲取鎖,會立即返回,返回值表示是否獲取成功
  8. 實例方法tryLock(long timeout, TimeUnit unit)會在指定的時間內嘗試獲取鎖,指定的時間內是否能夠獲取鎖,都會返回,返回值表示是否獲取鎖成功,該方法會響應線程的中斷

作者:路人甲java

原文:https://www.cnblogs.com/itsoku123/p/11203107.html

"

相關推薦

推薦中...