锁机制及其实现--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

本质上还是设置一个标记,如果这个标记存在,则说明已经有线程获取了锁,使用完之后清除标记。

总结

锁的本质就是设置标记,标记存在就说明已经被获取,使用完之后清除标记,其他线程就可以获取锁。

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