其他鎖: StampedLock(戳)、Semaphore(信號量)、CountdownLatch(倒計時)

1. StampedLock(戳)

該類自 JDK 8 加入,是爲了進一步優化讀性能,它的特點是在使用讀鎖、寫鎖時都必須配合【戳】使用

加鎖解鎖:讀鎖

long stamp = lock.readLock();
lock.unlockRead(stamp);

加鎖解鎖:寫鎖

long stamp = lock.writeLock();
lock.unlockWrite(stamp);

樂觀讀,StampedLock 支持tryOptimisticRead() 方法(樂觀讀),讀取完畢後需要做一次 戳校驗 如果校驗通
過,表示這期間確實沒有寫操作,數據可以安全使用,如果校驗沒通過,需要重新獲取讀鎖,保證數據安全。

long stamp = lock.tryOptimisticRead();
// 驗戳
if(!lock.validate(stamp)){
// 鎖升級
}

提供一個數據容器類內部分別使用讀鎖保護數據的 read() 方法,寫鎖保護數據的 write() 方法

class DataContainerStamped {
private int data;
private final StampedLock lock = new StampedLock();
public DataContainerStamped(int data) {
this.data = data;
}
public int read(int readTime) {
//這裏是樂觀讀,不是真正意義上的加鎖
long stamp = lock.tryOptimisticRead();
log.debug("optimistic read locking...{}", stamp);
sleep(readTime);
//對樂觀讀取後,需要做一次戳校驗,校驗通過,表示這期間確實沒有寫操作,數據可以安全使用返回data
//校驗沒通過,則從無鎖的樂觀讀,鎖升級 - 讀鎖
if (lock.validate(stamp)) {
log.debug("read finish...{}, data:{}", stamp, data);
return data;
}
// 鎖升級 - 讀鎖
log.debug("updating to read lock... {}", stamp);
try {
//帶戳的讀鎖
stamp = lock.readLock();
log.debug("read lock {}", stamp);
sleep(readTime);
log.debug("read finish...{}, data:{}", stamp, data);
return data;
} finally {
log.debug("read unlock {}", stamp);
lock.unlockRead(stamp);
}
}
public void write(int newData) {
//帶戳的寫鎖
long stamp = lock.writeLock();
log.debug("write lock {}", stamp);
try {
sleep(2);
this.data = newData;
} finally {
log.debug("write unlock {}", stamp);
lock.unlockWrite(stamp);
}
}
}

測試 讀-讀 可以優化

public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.read(0);
}, "t2").start();
}
結果:輸出結果,可以看到實際沒有加讀鎖,因爲讀讀校驗通過,不走鎖升級--讀鎖
15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256
15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256
15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1
15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1

測試 讀-寫 時優化讀補加讀鎖

public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.write(100);
}, "t2").start();
}
結果:有寫鎖,校驗不通過,升級到讀鎖,等到t2線程釋放寫鎖,t1才能加讀鎖
15:57:00.219 c.DataContainerStamped [t1] - optimistic read locking...256
15:57:00.717 c.DataContainerStamped [t2] - write lock 384
15:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256
15:57:02.719 c.DataContainerStamped [t2] - write unlock 384
15:57:02.719 c.DataContainerStamped [t1] - read lock 513
15:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000
15:57:03.719 c.DataContainerStamped [t1] - read unlock 513

注意

  • StampedLock 不支持條件變量
  • StampedLock 不支持可重入

2. Semaphore(信號量)

信號量,用來限制能同時訪問共享資源的線程上限。

1.自定義例子

public static void main(String[] args) {
// 1. 創建 semaphore 對象,每次都有3個線程能同時工作,
//和線程池類似
Semaphore semaphore = new Semaphore(3);
// 2. 10個線程同時運行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 3. 獲取許可
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("running...");
sleep(1);
log.debug("end...");
} finally {
// 4. 釋放許可
semaphore.release();
}
}).start();
}
}

2. Semaphore加鎖解鎖流程

Semaphore 有點像一個停車場,permits 就好像停車位數量,當線程獲得了 permits 就像是獲得了停車位,然後
停車場顯示空餘車位減一
剛開始,permits(state)爲 3,這時 5 個線程來獲取資源

注意:這裏的state表示可以加進來的線程數,0就是沒有線程空位了,與之前的state正好相反。
在這裏插入圖片描述

假設其中 Thread-1,Thread-2,Thread-4 cas 競爭成功,
而 Thread-0 和 Thread-3 競爭失敗,進入 AQS 隊列park 阻塞

在這裏插入圖片描述

這時 Thread-4 釋放了 permits,狀態如下

在這裏插入圖片描述

接下來 Thread-0 競爭成功,permits 再次設置爲 0,
設置自己爲 head 節點,斷開原來的 head 節點,unpark 
接下來的 Thread-3 節點,但由於 permits 是 0,
因此 Thread-3 在嘗試不成功後再次進入 park 狀態

在這裏插入圖片描述

3. Semaphore源碼

其實和之前的源碼分析差不多,這裏直接上源碼了

static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
// permits 即 state
super(permits);
}
// Semaphore 方法, 方便閱讀, 放在此處
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AQS 繼承過來的方法, 方便閱讀, 放在此處
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// 嘗試獲得共享鎖
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
// Sync 繼承過來的方法, 方便閱讀, 放在此處
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (
// 如果許可已經用完, 返回負數, 表示獲取失敗, 進入 doAcquireSharedInterruptibly
remaining < 0 ||
// 如果 cas 重試成功, 返回正數, 表示獲取成功
compareAndSetState(available, remaining)
) {
return remaining;
}
}
}
// AQS 繼承過來的方法, 方便閱讀, 放在此處
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再次嘗試獲取許可
int r = tryAcquireShared(arg);
if (r >= 0) {
// 成功後本線程出隊(AQS), 所在 Node設置爲 head
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一個節點 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
// r 表示可用資源數, 爲 0 則不會繼續傳播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 不成功, 設置上一個節點 waitStatus = Node.SIGNAL, 下輪進入 park 阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// Semaphore 方法, 方便閱讀, 放在此處
public void release() {
sync.releaseShared(1);
}
// AQS 繼承過來的方法, 方便閱讀, 放在此處
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// Sync 繼承過來的方法, 方便閱讀, 放在此處
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
}

3. CountdownLatch(倒計時)

用來進行線程同步協作,等待所有線程完成倒計時。
其中構造參數用來初始化等待計數值await() 用來等待計數歸零countDown() 用來讓計數減一

public static void main(String[] args) throws InterruptedException {
//3代表倒計時從3開始到0結束
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> {
log.debug("begin...");
sleep(1);
//countDown() 用來讓計數減一
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();
new Thread(() -> {
log.debug("begin...");
sleep(2);
//countDown() 用來讓計數減一
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();
new Thread(() -> {
log.debug("begin...");
sleep(1.5);
//countDown() 用來讓計數減一
latch.countDown();
log.debug("end...{}", latch.getCount());
}).start();
log.debug("waiting...");
//當計數減爲0的時候,開始運行在這之後的程序
latch.await();
log.debug("wait end...");
}
結果:
18:44:00.778 c.TestCountDownLatch [main] - waiting...
18:44:00.778 c.TestCountDownLatch [Thread-2] - begin...
18:44:00.778 c.TestCountDownLatch [Thread-0] - begin...
18:44:00.778 c.TestCountDownLatch [Thread-1] - begin...
18:44:01.782 c.TestCountDownLatch [Thread-0] - end...2
18:44:02.283 c.TestCountDownLatch [Thread-2] - end...1
18:44:02.782 c.TestCountDownLatch [Thread-1] - end...0
18:44:02.782 c.TestCountDownLatch [main] - wait end...
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章