Java多執行緒程式設計--使用Lock物件實現同步以及執行緒間通訊
前幾篇:
在《Java多執行緒程式設計-(4)-執行緒間通訊機制的介紹與使用》已經學習了,可以使用方法wait/notify
結合同步關鍵字synchronized實現同步和執行緒間通訊,下邊介紹一種更為方便的方式實現同步和執行緒間通訊的效果,那就是Lock物件。
Lock物件簡介
這裡為什麼說Lock物件哪?Lock其實是一個介面,在JDK1.5以後開始提供,其實現類常用的有ReentrantLock,這裡所說的Lock物件即是隻Lock介面的實現類,為了方便記憶或理解,都簡稱為Lock物件。
我們知道synchronized關鍵字可以實現執行緒間的同步互斥,從JDK1.5開始新增的ReentrantLock類能夠達到同樣的效果,並且在此基礎上還擴充套件了很多實用的功能,比使用synchronized更佳的靈活。
ReentrantLock的另一個稱呼就是“重入鎖”,Reentrant的英文釋義為:重入。
何為重入鎖,前幾篇在學習synchronized的時候,也談到了重入鎖,“一個物件一把鎖,多個物件多把鎖”,可重入鎖的概念就是:自己可以獲取自己的內部鎖。
ReentrantLock實現了Lock中的介面,繼承關係和方法屬性如下:
下邊,就開始一起學習一下ReentrantLock物件。
使用ReentrantLock實現執行緒同步
public class Run {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
//lambda寫法
new Thread(() -> runMethod(lock), "thread1").start();
new Thread(() -> runMethod(lock), "thread2").start();
new Thread(() -> runMethod(lock), "thread3").start();
new Thread(() -> runMethod(lock), "thread4").start();
//常規寫法
new Thread(new Runnable() {
@Override
public void run() {
runMethod(lock);
}
}, "thread5").start();
}
private static void runMethod(Lock lock) {
lock.lock();
for (int i = 1; i <= 5; i++) {
System.out.println("ThreadName:" + Thread.currentThread().getName() + (" i=" + i));
}
System.out.println();
lock.unlock();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
執行結果:
ThreadName:thread1 i=1
ThreadName:thread1 i=2
ThreadName:thread1 i=3
ThreadName:thread1 i=4
ThreadName:thread1 i=5
ThreadName:thread2 i=1
ThreadName:thread2 i=2
ThreadName:thread2 i=3
ThreadName:thread2 i=4
ThreadName:thread2 i=5
ThreadName:thread3 i=1
ThreadName:thread3 i=2
ThreadName:thread3 i=3
ThreadName:thread3 i=4
ThreadName:thread3 i=5
ThreadName:thread4 i=1
ThreadName:thread4 i=2
ThreadName:thread4 i=3
ThreadName:thread4 i=4
ThreadName:thread4 i=5
ThreadName:thread5 i=1
ThreadName:thread5 i=2
ThreadName:thread5 i=3
ThreadName:thread5 i=4
ThreadName:thread5 i=5
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
可以看出,當前執行緒列印完畢之後釋放鎖,其他執行緒才可以獲取鎖然後進行列印。執行緒列印的資料是分組列印的,這是因為當前執行緒已經持有鎖,在當前執行緒列印完之後才會釋放鎖,但執行緒之間列印的順序是隨機的。
為了進一步說明使用ReentrantLock可以實現執行緒之間同步,測試程式碼如下:
public class Run {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(() -> runMethod(lock, 0), "thread1").start();
new Thread(() -> runMethod(lock, 5000), "thread2").start();
new Thread(() -> runMethod(lock, 1000), "thread3").start();
new Thread(() -> runMethod(lock, 5000), "thread4").start();
new Thread(() -> runMethod(lock, 1000), "thread5").start();
}
private static void runMethod(Lock lock, long sleepTime) {
lock.lock();
try {
Thread.sleep(sleepTime);
System.out.println("ThreadName:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
執行結果:
ThreadName:thread1
ThreadName:thread2
ThreadName:thread3
ThreadName:thread4
ThreadName:thread5
- 1
- 2
- 3
- 4
- 5
可以看出,在sleep指定的時間內,當呼叫了lock.lock()方法執行緒就持有了”物件監視器”,其他執行緒只能等待鎖被釋放後再次爭搶,效果和使用synchronized關鍵字是一樣的。
使用Lock物件實現執行緒間通訊
上述,已經大致看了一下如何使用ReentrantLock實現執行緒之間的同步,下邊再看一下ReentrantLock是如何實現執行緒間通訊的。
在前文中我們已經知道可以使用關鍵字synchronized與wait()方法和notify()方式結合實現執行緒間通訊,也就是等待/通知模式。在ReentrantLock中,是藉助Condition物件進行實現的。
Condition的建立方式如下:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
- 1
- 2
Condition按字面意思理解就是條件,當然,我們也可以將其認為是條件進行使用,這樣的話我們可以通過上述的程式碼建立多個Condition條件,我們就可以根據不同的條件來控制現成的等待和通知。而我們還知道,在使用關鍵字synchronized與wait()方法和notify()方式結合實現執行緒間通訊的時候,notify/notifyAll的通知等待的執行緒時是隨機的,顯然使用Condition相對靈活很多,可以實現”選擇性通知”。
這是因為,synchronized關鍵字相當於整個Lock物件只有一個單一的Condition物件,所有的執行緒都註冊到這個物件上。執行緒開始notifAll的時候,需要通知所有等待的執行緒,讓他們開始競爭獲得鎖物件,沒有選擇權,這種方式相對於Condition條件的方式在效率上肯定Condition較高一些。
下邊,我們首先看一個例項。
使用Lock物件和Condition實現等待/通知例項
主要方法對比如下:
(1)Object的wait()方法相當於Condition類中的await()方法; (2)Object的notify()方法相當於Condition類中的signal()方法; (3)Object的notifyAll()方法相當於Condition類中的signalAll()方法;
首先,使用Lock的時候,和《Java多執行緒程式設計-(4)-執行緒間通訊機制的介紹與使用》介紹的一樣,都需要先獲取鎖。
示例程式碼如下:
public class LockConditionDemo {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
//使用同一個LockConditionDemo物件,使得lock、condition一樣
LockConditionDemo demo = new LockConditionDemo();
new Thread(() -> demo.await(), "thread1").start();
Thread.sleep(3000);
new Thread(() -> demo.signal(), "thread2").start();
}
private void await() {
try {
lock.lock();
System.out.println("開始等待await! ThreadName:" + Thread.currentThread().getName());
condition.await();
System.out.println("等待await結束! ThreadName:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void signal() {
lock.lock();
System.out.println("傳送通知signal! ThreadName:" + Thread.currentThread().getName());
condition.signal();
lock.unlock();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
執行結果:
開始等待await! ThreadName:thread1
傳送通知signal! ThreadName:thread2
等待await結束! ThreadName:thread1
- 1
- 2
- 3
可以看出結果正確執行!
使用Lock物件和多個Condition實現等待/通知例項
示例程式碼如下:
public class LockConditionDemo {
private Lock lock = new ReentrantLock();
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
LockConditionDemo demo = new LockConditionDemo();
new Thread(() -> demo.await(demo.conditionA), "thread1_conditionA").start();
new Thread(() -> demo.await(demo.conditionB), "thread2_conditionB").start();
new Thread(() -> demo.signal(demo.conditionA), "thread3_conditionA").start();
System.out.println("稍等5秒再通知其他的執行緒!");
Thread.sleep(5000);
new Thread(() -> demo.signal(demo.conditionB), "thread4_conditionB").start();
}
private void await(Condition condition) {
try {
lock.lock();
System.out.println("開始等待await! ThreadName:" + Thread.currentThread().getName());
condition.await();
System.out.println("等待await結束! ThreadName:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private void signal(Condition condition) {
lock.lock();
System.out.println("傳送通知signal! ThreadName:" + Thread.currentThread().getName());
condition.signal();
lock.unlock();
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
執行結果:
開始等待await! ThreadName:thread1_conditionA
開始等待await! ThreadName:thread2_conditionB
傳送通知signal! ThreadName:thread3_conditionA
等待await結束! ThreadName:thread1_conditionA
稍等5秒再通知其他的執行緒!
傳送通知signal! ThreadName:thread4_conditionB
等待await結束! ThreadName:thread2_conditionB
- 1
- 2
- 3
- 4
- 5
- 6
- 7
可以看出實現了分別通知。因此,我們可以使用Condition進行分組,可以單獨的通知某一個分組,另外還可以使用signalAll()方法實現通知某一個分組的所有等待的執行緒。
公平鎖和非公平鎖
概念很好理解,公平鎖表示執行緒獲取鎖的順序是按照執行緒加鎖的順序來分配,即先進先出,那麼他就是公平的;非公平是一種搶佔機制,是隨機獲得鎖,並不是先來的一定能先得到鎖,結果就是不公平的。
ReentrantLock提供了一個構造方法,可以很簡單的實現公平鎖或非公平鎖,原始碼建構函式如下:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
- 1
- 2
- 3
引數:fair為true表示是公平鎖,反之為非公平鎖,這裡不再寫程式碼測試。
ReentrantLock的其他方法
ReentrantLock原始碼結構如下:
方法很簡單,看到名稱就可以想到作用是什麼,挑一些簡單介紹一下:
(1)getHoldCount()方法:查詢當前執行緒保持此鎖定的個數,也就是呼叫lock()的次數;
(2)getQueueLength()方法:返回正等待獲取此鎖定的執行緒估計數目;
(3)isFair()方法:判斷是不是公平鎖;
使用ReentrantReadWriteLock實現併發
上述的類ReentrantLock具有完全互斥排他的效果,即同一時間只能有一個執行緒在執行ReentrantLock.lock()之後的任務。
類似於我們集合中有同步類容器 和 併發類容器,HashTable(HashTable幾乎可以等價於HashMap,並且是執行緒安全的)也是完全排他的,即使是讀也只能同步執行,而ConcurrentHashMap就可以實現同一時刻多個執行緒之間併發。為了提高效率,ReentrantLock的升級版ReentrantReadWriteLock就可以實現效率的提升。
ReentrantReadWriteLock有兩個鎖:一個是與讀相關的鎖,稱為“共享鎖”;另一個是與寫相關的鎖,稱為“排它鎖”。也就是多個讀鎖之間不互斥,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。
在沒有執行緒進行寫操作時,進行讀操作的多個執行緒都可以獲取到讀鎖,而寫操作的執行緒只有獲取寫鎖後才能進行寫入操作。即:多個執行緒可以同時進行讀操作,但是同一時刻只允許一個執行緒進行寫操作。
ReentrantReadWriteLock鎖的特性:
(1)讀讀共享; (2)寫寫互斥; (3)讀寫互斥; (4)寫讀互斥;
ReentrantReadWriteLock例項程式碼
(1)讀讀共享
public class ReentrantReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
new Thread(() -> demo.read(), "ThreadA").start();
new Thread(() -> demo.read(), "ThreadB").start();
}
private void read() {
try {
try {
lock.readLock().lock();
System.out.println("獲得讀鎖" + Thread.currentThread().getName()
+ " 時間:" + System.currentTimeMillis());
//模擬讀操作時間為5秒
Thread.sleep(5000);
} finally {
lock.readLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
執行結果:
獲得讀鎖ThreadA 時間:1507720692022
獲得讀鎖ThreadB 時間:1507720692022
- 1
- 2
可以看出兩個執行緒之間,獲取鎖的時間幾乎同時,說明lock.readLock().lock();
允許多個執行緒同時執行lock()方法後面的程式碼。
(2)寫寫互斥
public class ReentrantReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
new Thread(() -> demo.write(), "ThreadA").start();
new Thread(() -> demo.write(), "ThreadB").start();
}
private void write() {
try {
try {
lock.writeLock().lock();
System.out.println("獲得寫鎖" + Thread.currentThread().getName()
+ " 時間:" + System.currentTimeMillis());
//模擬寫操作時間為5秒
Thread.sleep(5000);
} finally {
lock.writeLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
執行結果:
獲得寫鎖ThreadA 時間:1507720931662
獲得寫鎖ThreadB 時間:1507720936662
- 1
- 2
可以看出執行結果大致差了5秒的時間,可以說明多個寫執行緒是互斥的。
(3)讀寫互斥或寫讀互斥
public class ReentrantReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
new Thread(() -> demo.read(), "ThreadA").start();
Thread.sleep(1000);
new Thread(() -> demo.write(), "ThreadB").start();
}
private void read() {
try {
try {
lock.readLock().lock();
System.out.println("獲得讀鎖" + Thread.currentThread().getName()
+ " 時間:" + System.currentTimeMillis());
Thread.sleep(3000);
} finally {
lock.readLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void write() {
try {
try {
lock.writeLock().lock();
System.out.println("獲得寫鎖" + Thread.currentThread().getName()
+ " 時間:" + System.currentTimeMillis());
Thread.sleep(3000);
} finally {
lock.writeLock().unlock();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
執行結果:
獲得讀鎖ThreadA 時間:1507721135908
獲得寫鎖ThreadB 時間:1507721138908
- 1
- 2
- 3
可以看出執行結果大致差了3秒的時間,可以說明讀寫執行緒是互斥的。