鎖機制及其實現--java併發包中的鎖和redisson中的鎖

前言

在多線程環境中,多個線程訪問同一塊代碼時,就會發生競態條件(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

本質上還是設置一個標記,如果這個標記存在,則說明已經有線程獲取了鎖,使用完之後清除標記。

總結

鎖的本質就是設置標記,標記存在就說明已經被獲取,使用完之後清除標記,其他線程就可以獲取鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章