前言
最近在看并發編程藝術這本書,對看書的一些筆記及個人工作中的總結。
Lock接口
鎖是用來控制多個線程訪問共享資源的方式,一般來說,一個鎖能夠防止多個線程同時訪問共享資源(但是有些鎖可以允許多個線程并發的訪問共享資源,比如讀寫鎖)。
public class LockUseCase {
public void lock() {
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock(); //保證鎖一定能釋放
}
}
}
Lock接口提供的synchronized關鍵字所不具備的主要特性
特性 | 描述 |
---|---|
嘗試非阻塞地獲取鎖 | 當前線程嘗試獲取鎖,如果這一時刻沒有被其他線程獲取到,則成功獲取并持有鎖 |
能被中斷獲取鎖 | 與synchronized不同,獲取到鎖的線程能夠響應中斷,當獲取到鎖的線程被中斷時,中斷異常將會被拋出,同時鎖會被釋放。 |
超時獲取鎖 | 在指定的截止時間之前獲取鎖,如果截止時間到了仍舊無法獲取鎖,則返回。 |
Lock是一個接口,它定義了鎖獲取和釋放的基本操作,Lock的API如下:
方法名稱 | 描述 |
---|---|
void lock() | 獲取鎖,調用該方法當前線程將會獲取鎖,當鎖獲得后,從該方法返回 |
void lockInterruptibly | 可中斷地獲取鎖,和lock()方法的不同之處在于該方法會響應中斷,即在鎖的獲取中可以中斷當前線程. |
boolean tryLock() | 嘗試非阻塞的獲取鎖,調用該方法后立即返回,如果能夠獲取則返回true,否則返回false |
boolean tryLock(long time, TimeUnit unit) | 超時獲取鎖,具體解釋看官方api |
void unlock() | 釋放鎖 |
Condition newCondition() | 獲取等待通知組件,該組件和當前的鎖綁定,當前線程只有獲得了鎖,才能調用該組件的wait()方法,而調用后,當前線程將釋放鎖。 |
ReentrantLock(重入鎖)
- 重入鎖ReentrantLock,顧名思義,就是支持重進入的鎖,它表示該鎖能夠支持一個線程對資源的重復加鎖。除此之外,該鎖的還支持獲取鎖時的公平和非公平性選擇。
- synchronized關鍵字隱式的支持重進入,比如一個synchronized修飾的遞歸方法,在方法執行時,執行線程在獲取了鎖之后仍能連續多次地獲得該鎖。
/**
* 在加鎖的方法中再次獲得加鎖的方法
*/
public class ReentrantTest {
public synchronized void test1(){
System.out.println("test1..");
test2();
}
public synchronized void test2(){
System.out.println("test2..");
}
public static void main(String[] args) {
final ReentrantTest reentrantTest = new ReentrantTest();
new Thread(() -> reentrantTest.test1()).start();
}
}
/**
* 子類中調用父類帶synchronized也是線程安全的
*/
public class ReentrantTest2 {
static class Main {
public synchronized void operationSup(){
System.out.println("main類的方法");
}
}
static class Sub extends Main {
public synchronized void operationSub(){
super.operationSup();
System.out.println("Sub類的方法");
}
}
public static void main(String[] args) {
final Sub sub = new Sub();
new Thread(() -> sub.operationSub()).start();
}
}
- ReentrantLock雖然沒能像synchronized關鍵字一樣支持隱式的重進入,但是在調用lock()方法時,已經獲取到鎖的線程,能夠再次調用lock()方法獲取鎖而不被阻塞。
public class ReentrantTest3 {
private ReentrantLock lock = new ReentrantLock();
public void test1(){
try {
lock.lock();
System.out.println("進入m1方法,holdCount數為:" + lock.getHoldCount()); //1
//調用m2方法
test2();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void test2(){
try {
lock.lock();
System.out.println("進入m2方法,holdCount數為:" + lock.getHoldCount()); //2
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantTest3 thc = new ReentrantTest3();
thc.test1();
}
}
這里提到一個鎖獲取的公平性問題,如果在絕對時間上,先對鎖進行獲取的請求一定先被滿足,那么這個鎖是公平的,反之,是不公平的。公平的獲取鎖,也就是等待時間最長的線程最優先獲取鎖,也可以說鎖獲取是順序的。ReentrantLock提供了一個構造函數,能夠控制鎖是否是公平的。
事實上,公平的鎖機制往往沒有非公平的效率高,但是,并不是任何場景都是以TPS作為唯一的指標,公平鎖能夠減少“饑餓”發生的概率,等待越久的請求越是能夠得到優先滿足。
原理:
重進入是指任意線程在獲取到鎖之后能夠再次獲取該鎖而不會被鎖所阻塞,
1)線程再次獲取鎖。鎖需要去識別獲取鎖的線程是否為當前占據鎖的線程,如果是,則再次成功獲取。
2)鎖的最終釋放。線程重復n次獲取了鎖,隨后在第n次釋放該鎖后,其他線程能夠獲取到該鎖。鎖的最終釋放要求鎖對于獲取進行計數自增,計數表示當前鎖被重復獲取的次數,而鎖被釋放時,計數自減,當計數等于0時表示鎖已經成功釋放。
通過判斷當前線程是否為獲取鎖的線程來決定獲取操作是否成功,如果是獲取鎖的線程再次請求,則將同步狀態值進行增加并返回true,表示獲取同步狀態成功。
以上面的ReentrantTest3代碼分析,
/**
* Acquires the lock.
*
* <p>Acquires the lock if it is not held by another thread and returns
* immediately, setting the lock hold count to one.
*
* <p>If the current thread already holds the lock then the hold
* count is incremented by one and the method returns immediately.
*
* <p>If the lock is held by another thread then the
* current thread becomes disabled for thread scheduling
* purposes and lies dormant until the lock has been acquired,
* at which time the lock hold count is set to one.
*/
public void lock() {
sync.lock();
}
然后進入非公平鎖的實現:
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1)) //第一次執行是true,進入if邏輯
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
如果當前狀態值等于期望值,則將同步狀態原子設置為給定的更新值。 此操作具有內存可見性(volatile)的內存語義。
/**
* Atomically sets synchronization state to the given updated
* value if the current state value equals the expected value.
* This operation has memory semantics of a {@code volatile} read
* and write.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that the actual
* value was not equal to the expected value.
*/
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
設置當前線程為獨占訪問權限。
/**
* Sets the thread that currently owns exclusive access.
* A {@code null} argument indicates that no thread owns access.
* This method does not otherwise impose any synchronization or
* {@code volatile} field accesses.
* @param thread the owner thread
*/
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
重入鎖的時候由進入了lock方法
final void lock() {
if (compareAndSetState(0, 1)). //false
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); //執行這一步
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
非公平鎖的實現
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //進入這個判斷
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
很明顯判斷此時去嘗試獲得鎖的對象是否是占據鎖的對象,是則表明可以重入。分析到此結束,其他的細節這邊就不分析了。
ReentrantReadWriteLock(讀寫鎖)
排他鎖,這些鎖在同一時刻只允許一個線程進行訪問,而讀寫鎖在同一時刻可以允許多個讀線程訪問,但是在寫線程訪問時,所有的讀線程和其他寫線程均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖,使得并發性相比一般的排他鎖有了很大提升。
除了保證寫操作對讀操作的可見性以及并發性的提升之外,讀寫鎖能夠簡化讀寫交互場景的編程方式。假設在程序中定義一個共享的用作緩存數據結構,它大部分時間提供讀服務(例如查詢和搜索),而寫操作占有的時間很少,但是寫操作完成之后的更新需要對后續的讀服務可見。
在沒有讀寫鎖支持的(Java 5之前)時候,如果需要完成上述工作就要使用Java的等待通知機制,就是當寫操作開始時,所有晚于寫操作的讀操作均會進入等待狀態,只有寫操作完成并進行通知之后,所有等待的讀操作才能繼續執行(寫操作之間依靠synchronized關鍵進行同步),這樣做的目的是使讀操作能讀取到正確的數據,不會出現臟讀。改用讀寫鎖實現上述功能,只需要在讀操作時獲取讀鎖,寫操作時獲取寫鎖即可。當寫鎖被獲取到時,后續(非當前寫操作線程)的讀寫操作都會被阻塞,寫鎖釋放之后,所有操作繼續執行,編程方式相對于使用等待通知機制的實現方式而言,變得簡單明了。
一般情況下,讀寫鎖的性能都會比排它鎖好,因為大多數場景讀是多于寫的。在讀多于寫的情況下,讀寫鎖能夠提供比排它鎖更好的并發性和吞吐量。Java并發包提供讀寫鎖的實現是ReentrantReadWriteLock,
特性 | 說明 |
---|---|
公平性選擇 | 支持非公平(默認)和公平的鎖的獲取方式,吞吐量還是非公平優于公平 |
重進入 | 該鎖支持重進入,以讀寫線程為列,讀線程在獲取讀鎖之后,能夠再次獲取讀鎖,而寫線程在獲取了寫鎖之后能夠再次獲得寫鎖,同時也可以獲得讀鎖。 |
鎖降級 | 遵循獲取寫鎖,獲取讀鎖在釋放寫鎖的次序,寫鎖能夠降級為讀鎖 |
** 示列 **
//Cache組合一個非線程安全的HashMap作為緩存的實現,同時使用讀寫鎖的讀鎖和寫鎖來保證Cache是線程安全的。在讀操作get(String key)方法中,
//需要獲取讀鎖,這使得并發訪問該方法時不會被阻塞。寫操作put(String key,Object value)方法和clear()方法,在更新HashMap時必須提前獲取寫鎖,
//當獲取寫鎖后,其他線程對于讀鎖和寫鎖的獲取均被阻塞,而只有寫鎖被釋放之后,其他讀寫操作才能繼續。
//Cache使用讀寫鎖提升讀操作的并發性,也保證每次寫操作對所有的讀寫操作的可見性,同時簡化了編程方式。
public class Cache {
private static final Map<String, Object> map = new HashMap<>();
private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private static final Lock r = rwl.readLock();
private static final Lock w = rwl.writeLock();
//獲取一個key對應的value,使用的讀鎖
public static final Object get(String key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
//設置key對應的value,并返回舊的value,寫鎖
public static final Object put(String key, Object value) {
w.lock();
try {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
return map.put(key, value);
} finally {
w.unlock();
}
}
//清空所有的內容
public static final void clear() {
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
//驗證了寫鎖一旦獲取鎖,其他線程的讀寫都處于阻塞
public static void main(String[] args) {
new Thread(() -> Cache.put("username","miaozhihao")).start();
new Thread(() -> {
String username = (String)Cache.get("username");
System.out.println(username);
}).start();
}
}
這個列子很好的展示了寫操作開始時,所有晚于寫操作的讀操作均會進入等待狀態,只有寫操作完成并進行通知之后,所有等待的讀操作才能繼續執行。也就是如果讀取操作時候有寫操作,那么必須等待寫操作完成之后才能進行讀取操作。
讀寫鎖的實現分析
主要包括:讀寫狀態的設計、寫鎖的獲取與釋放、讀鎖的獲取與釋放以及鎖降級
讀寫鎖要確保寫鎖的操作對讀鎖可見,如果允許讀鎖在已被獲取的情況下對寫鎖的獲取,那么正在運行的其他讀線程就無法感知到當前寫線程的操作。因此,只有等待其他讀線程都釋放了讀鎖,寫鎖才能被當前線程獲取,而寫鎖一旦被獲取,則其他讀寫線程的后續訪問均被阻塞。
讀鎖是一個支持重進入的共享鎖,它能夠被多個線程同時獲取,在沒有其他寫線程訪問(或者寫狀態為0)時,讀鎖總會被成功地獲取,而所做的也只是(線程安全的)增加讀狀態。如果當前線程已經獲取了讀鎖,則增加讀狀態。如果當前線程在獲取讀鎖時,寫鎖已被其他線程獲取,則進入等待狀態。
鎖降級指的是寫鎖降級成為讀鎖。如果當前線程擁有寫鎖,然后將其釋放,最后再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級。鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨后釋放(先前擁有的)寫鎖的過程。
鎖降級:鎖降級是指把持住(當前擁有的)寫鎖,再獲取到讀鎖,隨后釋放(先前擁有的)寫鎖的過程。
代碼示例:
public class ProcessData {
private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private static final Lock readLock = rwl.readLock();
private static final Lock writeLock = rwl.writeLock();
private volatile boolean update = false;
public void processData() {
readLock.lock();
if (!update) {
//必須先釋放讀鎖
readLock.unlock();
//鎖降級從寫鎖獲取到開始
writeLock.lock();
try {
if (!update) {
// 準備數據的流程(略)
update = true;
}
readLock.lock();
} finally {
writeLock.unlock();
}
// 鎖降級完成,寫鎖降級為讀鎖
}
try {
// 使用數據的流程(略)
} finally {
readLock.unlock();
}
}
}
如果讀鎖已被多個線程獲取,其中任意線程成功獲取了寫鎖并更新了數據,則其更新對其他獲取到讀鎖的線程是不可見的。
一個先后順序,如果是寫鎖先占用,那么所有的讀鎖就被阻塞;如果是讀鎖被占用,那么此后的寫鎖修改的內容讀鎖線程不可見。