前言
在多线程环境中,多个线程访问同一块代码时,就会发生竞态条件(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
本质上还是设置一个标记,如果这个标记存在,则说明已经有线程获取了锁,使用完之后清除标记。
总结
锁的本质就是设置标记,标记存在就说明已经被获取,使用完之后清除标记,其他线程就可以获取锁。