前言
在多線程環境中,多個線程訪問同一塊代碼時,就會發生競態條件(race condition),這意味着在某個時刻,我們無法確定到底是哪個線程在執行那塊代碼中的某個操作,也無法確定在那個操作之後,是不是同一個線程會繼續執行下一個操作。這樣帶來的問題就是無法預測程序的行爲,程序不會按照我們的期望運行,程序很容易出錯甚至奔潰。
在多線程環境中,我們需要一種機制來獲得確定性,也就是一次共享代碼塊的完整操作只有一個線程參與,我們可以通過鎖機制來實現。
鎖機制
在現實中,我們想聲明自己在使用某件東西時,通常會做個標記,其他人看到這個標記時就會知道我們正在使用,我們使用完之後清除標記,下次別人就可以來使用了。
在計算機程序中,鎖機制的實現也很類似,我們在鎖中設置一個標記,第一個獲取鎖的線程把標記設置爲“已被獲取”,其他線程獲取鎖時看到這個標記,根據不同策略,就會等待或者放棄;等到獲取鎖的線程使用完鎖的時候,會清除標記,後面的線程就可以獲取鎖了。
鎖的使用
鎖的使用很廣泛,但是有一點需要指出,獲取鎖並不是目的,獲取鎖的目的是爲了獨佔地執行某塊代碼,所以鎖是需要放在對象裏面保護對象中的代碼的,有需要獨佔式執行的代碼,就需要鎖。
ReentrantLock中鎖的實現
在Java5發佈時,增加了併發編程大師Doug Lea主導編寫的java.util.concurrent包,也就是我們常說的Java併發包,併發包提供了不同於synchronize關鍵字的更靈活的同步機制,包括Lock接口,其中最常用的Lock實現類就是ReentrantLock,我們可以看看ReentrantLock中,鎖是怎麼實現的。
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
上面的代碼就是我們最常見的使用Lock接口的方式。我們可以在源碼中查看ReentrantLock是怎麼實現lock方法的。
public void lock() {
sync.lock();
}
ReentrantLock的lock方法是直接調用它的成員對象sync的lock方法。
private final Sync sync;
sync是一個Sync類型的對象
abstract static class Sync extends AbstractQueuedSynchronizer
Sync是一個靜態抽象內部類
abstract void lock();
Sync的lock方法是抽象的,所以我們繼續找它的實現。
static final class NonfairSync extends Sync
static final class FairSync extends Sync
有兩個具體類繼承了Sync,我們看這兩個類是怎麼實現lock方法的。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
這是NonfairSync中的lock方法
final void lock() {
acquire(1);
}
這是FairSync中的lock方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
這是Sync的父類AbstractQueuedSynchronizer中的acquire方法,和鎖標記的狀態有關的方法是tryAcquire(arg)。我們看子類中的tryAcquire。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
這是FairSync中的tryAcquire,NonfairSync中的tryAcquire調用的是nonfairTryAcquire,如下:
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;
}
我們可以看到,上述兩個方法共同調用的方法是compareAndSetState(0, acquires),這是父類AbstractQueuedSynchronizer中的方法,源碼如下:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
其他相關的代碼如下:
private static final long stateOffset;
private volatile int state;
static {
try {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
到這裏,我們可以知道ReentrantLock中鎖的實現如下:
ReentrantLock中鎖通過AbstractQueuedSynchronizer實現,AbstractQueuedSynchronizer中有一個volatile修飾的int型屬性state,初始值是0,我們要獲取鎖時,用unsafe提供的CAS算法驗證state的值是不是0,如果是0,就設置爲1。ReentrantLock中的鎖是可重入的,所以如果state的值不是0,我們還會判斷之前獲取鎖的線程是不是當前線程,如果是,則此線程還能繼續獲取鎖。
用Redis實現的分佈式鎖
Redis官網上面有推薦的分佈式鎖方案,Java語言的實現是Redisson,其中獲取鎖的核心代碼如下:
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
我也不是很懂,哈哈哈,其實就是Redis官網上面的兩條命令:
SET resource_name my_random_value NX PX 30000
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
本質上還是設置一個標記,如果這個標記存在,則說明已經有線程獲取了鎖,使用完之後清除標記。
總結
鎖的本質就是設置標記,標記存在就說明已經被獲取,使用完之後清除標記,其他線程就可以獲取鎖。