jdk中獨占鎖的實現除了使用關鍵字synchronized外,還可以使用ReentrantLock。雖然在性能上ReentrantLock和synchronized沒有什么區別,但ReentrantLock相比synchronized而言功能更加豐富,使用起來更為靈活,也更適合復雜的並發場景。
2. ReentrantLock和synchronized的相同點
2.1 ReentrantLock是獨占鎖且可重入的
- 例子
public class ReentrantLockTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
for (int i = 1; i <= 3; i++) {
lock.lock();
}
for(int i=1;i<=3;i++){
try {
} finally {
lock.unlock();
}
}
}
}
上面的代碼通過lock()
方法先獲取鎖三次,然后通過unlock()
方法釋放鎖3次,程序可以正常退出。從上面的例子可以看出,ReentrantLock是可以重入的鎖,當一個線程獲取鎖時,還可以接着重復獲取多次。在加上ReentrantLock的的獨占性,我們可以得出以下ReentrantLock和synchronized的相同點。
-
1.ReentrantLock和synchronized都是獨占鎖,只允許線程互斥的訪問臨界區。但是實現上兩者不同:synchronized加鎖解鎖的過程是隱式的,用戶不用手動操作,優點是操作簡單,但顯得不夠靈活。一般並發場景使用synchronized的就夠了;ReentrantLock需要手動加鎖和解鎖,且解鎖的操作盡量要放在finally代碼塊中,保證線程正確釋放鎖。ReentrantLock操作較為復雜,但是因為可以手動控制加鎖和解鎖過程,在復雜的並發場景中能派上用場。
-
2.ReentrantLock和synchronized都是可重入的。synchronized因為可重入因此可以放在被遞歸執行的方法上,且不用擔心線程最后能否正確釋放鎖;而ReentrantLock在重入時要卻確保重復獲取鎖的次數必須和重復釋放鎖的次數一樣,否則可能導致其他線程無法獲得該鎖。
3. ReentrantLock相比synchronized的額外功能
3.1 ReentrantLock可以實現公平鎖。
公平鎖是指當鎖可用時,在鎖上等待時間最長的線程將獲得鎖的使用權。而非公平鎖則隨機分配這種使用權。和synchronized一樣,默認的ReentrantLock實現是非公平鎖,因為相比公平鎖,非公平鎖性能更好。當然公平鎖能防止飢餓,某些情況下也很有用。在創建ReentrantLock的時候通過傳進參數true
創建公平鎖,如果傳入的是false
或沒傳參數則創建的是非公平鎖
ReentrantLock lock = new ReentrantLock(true);
繼續跟進看下源碼
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
可以看到公平鎖和非公平鎖的實現關鍵在於成員變量sync
的實現不同,這是鎖實現互斥同步的核心。以后有機會我們再細講。
- 一個公平鎖的例子
public class ReentrantLockTest {
static Lock lock = new ReentrantLock(true);
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<5;i++){
new Thread(new ThreadDemo(i)).start();
}
}
static class ThreadDemo implements Runnable {
Integer id;
public ThreadDemo(Integer id) {
this.id = id;
}
@Override
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<2;i++){
lock.lock();
System.out.println("獲得鎖的線程:"+id);
lock.unlock();
}
}
}
}
- 公平鎖結果
我們開啟5個線程,讓每個線程都獲取釋放鎖兩次。為了能更好的觀察到結果,在每次獲取鎖前讓線程休眠10毫秒。可以看到線程幾乎是輪流的獲取到了鎖。如果我們改成非公平鎖,再看下結果
- 非公平鎖結果
線程會重復獲取鎖。如果申請獲取鎖的線程足夠多,那么可能會造成某些線程長時間得不到鎖。這就是非公平鎖的“飢餓”問題。
- 公平鎖和非公平鎖該如何選擇
大部分情況下我們使用非公平鎖,因為其性能比公平鎖好很多。但是公平鎖能夠避免線程飢餓,某些情況下也很有用。
3.2 .ReentrantLock可響應中斷
當使用synchronized實現鎖時,阻塞在鎖上的線程除非獲得鎖否則將一直等待下去,也就是說這種無限等待獲取鎖的行為無法被中斷。而ReentrantLock給我們提供了一個可以響應中斷的獲取鎖的方法lockInterruptibly()
。該方法可以用來解決死鎖問題。
- 響應中斷的例子
public class ReentrantLockTest {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadDemo(lock1, lock2));//該線程先獲取鎖1,再獲取鎖2
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//該線程先獲取鎖2,再獲取鎖1
thread.start();
thread1.start();
thread.interrupt();//是第一個線程中斷
}
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
firstLock.lockInterruptibly();
TimeUnit.MILLISECONDS.sleep(10);//更好的觸發死鎖
secondLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常結束!");
}
}
}
}
- 結果
構造死鎖場景:創建兩個子線程,子線程在運行時會分別嘗試獲取兩把鎖。其中一個線程先獲取鎖1在獲取鎖2,另一個線程正好相反。如果沒有外界中斷,該程序將處於死鎖狀態永遠無法停止。我們通過使其中一個線程中斷,來結束線程間毫無意義的等待。被中斷的線程將拋出異常,而另一個線程將能獲取鎖后正常結束。
3.3 獲取鎖時限時等待
ReentrantLock還給我們提供了獲取鎖限時等待的方法tryLock()
,可以選擇傳入時間參數,表示等待指定的時間,無參則表示立即返回鎖申請的結果:true表示獲取鎖成功,false表示獲取鎖失敗。我們可以使用該方法配合失敗重試機制來更好的解決死鎖問題。
- 更好的解決死鎖的例子
public class ReentrantLockTest {
static Lock lock1 = new ReentrantLock();
static Lock lock2 = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadDemo(lock1, lock2));//該線程先獲取鎖1,再獲取鎖2
Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//該線程先獲取鎖2,再獲取鎖1
thread.start();
thread1.start();
}
static class ThreadDemo implements Runnable {
Lock firstLock;
Lock secondLock;
public ThreadDemo(Lock firstLock, Lock secondLock) {
this.firstLock = firstLock;
this.secondLock = secondLock;
}
@Override
public void run() {
try {
while(!lock1.tryLock()){
TimeUnit.MILLISECONDS.sleep(10);
}
while(!lock2.tryLock()){
lock1.unlock();
TimeUnit.MILLISECONDS.sleep(10);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
firstLock.unlock();
secondLock.unlock();
System.out.println(Thread.currentThread().getName()+"正常結束!");
}
}
}
}
- 結果
線程通過調用tryLock()
方法獲取鎖,第一次獲取鎖失敗時會休眠10毫秒,然后重新獲取,直到獲取成功。第二次獲取失敗時,首先會釋放第一把鎖,再休眠10毫秒,然后重試直到成功為止。線程獲取第二把鎖失敗時將會釋放第一把鎖,這是解決死鎖問題的關鍵,避免了兩個線程分別持有一把鎖然后相互請求另一把鎖。
4. 結合Condition實現等待通知機制
使用synchronized結合Object上的wait和notify方法可以實現線程間的等待通知機制。ReentrantLock結合Condition接口同樣可以實現這個功能。而且相比前者使用起來更清晰也更簡單。
4.1 Condition使用簡介
Condition由ReentrantLock對象創建,並且可以同時創建多個
static Condition notEmpty = lock.newCondition();
static Condition notFull = lock.newCondition();
Condition接口在使用前必須先調用ReentrantLock的lock()方法獲得鎖。之后調用Condition接口的await()將釋放鎖,並且在該Condition上等待,直到有其他線程調用Condition的signal()方法喚醒線程。使用方式和wait,notify類似。
- 一個使用condition的簡單例子
public class ConditionTest {
static ReentrantLock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
lock.lock();
new Thread(new SignalThread()).start();
System.out.println("主線程等待通知");
try {
condition.await();
} finally {
lock.unlock();
}
System.out.println("主線程恢復運行");
}
static class SignalThread implements Runnable {
@Override
public void run() {
lock.lock();
try {
condition.signal();
System.out.println("子線程通知");
} finally {
lock.unlock();
}
}
}
}
- 運行結果
4.2 使用Condition實現簡單的阻塞隊列
阻塞隊列是一種特殊的先進先出隊列,它有以下幾個特點
1.入隊和出隊線程安全
2.當隊列滿時,入隊線程會被阻塞;當隊列為空時,出隊線程會被阻塞。
- 阻塞隊列的簡單實現
public class MyBlockingQueue<E> {
int size;//阻塞隊列最大容量
ReentrantLock lock = new ReentrantLock();
LinkedList<E> list=new LinkedList<>();//隊列底層實現
Condition notFull = lock.newCondition();//隊列滿時的等待條件
Condition notEmpty = lock.newCondition();//隊列空時的等待條件
public MyBlockingQueue(int size) {
this.size = size;
}
public void enqueue(E e) throws InterruptedException {
lock.lock();
try {
while (list.size() ==size)//隊列已滿,在notFull條件上等待
notFull.await();
list.add(e);//入隊:加入鏈表末尾
System.out.println("入隊:" +e);
notEmpty.signal(); //通知在notEmpty條件上等待的線程
} finally {
lock.unlock();
}
}
public E dequeue() throws InterruptedException {
E e;
lock.lock();
try {
while (list.size() == 0)//隊列為空,在notEmpty條件上等待
notEmpty.await();
e = list.removeFirst();//出隊:移除鏈表首元素
System.out.println("出隊:"+e);
notFull.signal();//通知在notFull條件上等待的線程
return e;
} finally {
lock.unlock();
}
}
}
- 測試代碼
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(2);
for (int i = 0; i < 10; i++) {
int data = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
queue.enqueue(data);
} catch (InterruptedException e) {
}
}
}).start();
}
for(int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Integer data = queue.dequeue();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
- 運行結果
5. 總結
ReentrantLock是可重入的獨占鎖。比起synchronized功能更加豐富,支持公平鎖實現,支持中斷響應以及限時等待等等。可以配合一個或多個Condition條件方便的實現等待通知機制。