点击上方蓝字关注我们
CAS
CAS 是 Compare And Swap 的缩写,意思是比较并替换。
如果本次修改不成功,怎么办?很多情况下,它将一直重试,直到修改为期望的值。
立春
LiChun
拿 AtomicInteger 类来说,相关的代码如下:
public final boolean compareAndSet(int expectedValue, int newValue) {return U.compareAndSetInt(this, VALUE, expectedValue, newValue);}
比较和替换是两个动作,CAS 是如何保证这两个操作的原子性呢?
我们继续向下追踪,发现是 jdk.internal.misc.Unsafe 类实现的,循环重试就是在这里发生的:
@HotSpotIntrinsicCandidatepublic final int getAndAddInt(Object o, long offset, int delta) {int v;do {v = getIntVolatile(o, offset);} while (!weakCompareAndSetInt(o, offset, v, v + delta));return v;}
JVM 内部,在 linux 机器上参照 os_cpu/linux_x86/atomic_linux_x86.hpp。可以看到,最底层的调用,是汇编语言,而最重要的,就是cmpxchgl指令。到这里没法再往下找代码了,因为 CAS 的原子性实际上是硬件 CPU 直接保证的。
关于 Atomic 类,还有一个小细节,那就是它的主要变量,使用了 volatile 关键字进行修饰。代码如下,你知道它是用来干什么的吗?
private volatile int value;
答案:使用了 volatile 关键字的变量,每当变量的值有变动的时候,都会将更改立即同步到主内存中;而如果某个线程想要使用这个变量,就先要从主存中刷新到工作内存,这样就确保了变量的可见性。有了这个关键字的修饰,就能保证每次比较的时候,拿到的值总是最新的。
立春,为二十四节气之首,又名正月节、岁节、改岁、岁旦等。立,是“开始”之意。春,代表着温暖、生长。
乐观锁
为什么读多写少的情况,就适合使用乐观锁呢?悲观锁在读多写少的情况下,不也是有很少的冲突吗?
其实,问题不在于冲突的频繁性,而在于加锁这个动作上。
悲观锁需要遵循下面三种模式:一锁、二读、三更新,即使在没有冲突的情况下,执行也会非常慢;
如之前所说,乐观锁本质上不是锁,它只是一个判断逻辑,资源冲突少的情况下,它不会产生任何开销。
Redis 分布式锁
我们先来看一段代码:
public void lock(String key, int timeOutSecond) {for (; ; ) {boolean exist = redisTemplate.opsForValue().setIfAbsent(key, "", timeOutSecond, TimeUnit.SECONDS);if (exist) {break;}}}public void unlock(String key) {redisTemplate.delete(key);}
这段代码中的问题很多,我们只指出其中一个最严重的问题。在多线程中,执行 unlock方法的,只能是当前的线程,但在上面的实现中,由于超时存在的原因,锁被提前释放了。考虑下面 3 个请求的时序:
请求A:获取了资源 x 的锁,锁的超时时间为 5 秒
请求A:由于业务执行时间比较长,业务阻塞等待,超过 5 秒
请求B:第 6 秒发起请求,结果发现锁 x 已经失效,于是顺利获得锁
请求A:第 7 秒,请求 A 执行完毕,然后执行锁释放动作
请求C:请求 C 在锁刚释放的时候发起了请求,结果顺利拿到了锁资源
此时,请求 B 和请求 C 都成功地获取了锁 x,我们的分布式锁失效了,在执行业务逻辑的时候,就容易发生问题。
所以,在删除锁的时候,需要判断它的请求方是否正确。首先,获取锁中的当前标识,然后,在删除的时候,判断这个标识是否和解锁请求中的相同。
可以看到,读取和判断是两个不同的操作,在这两个操作之间同样会有间隙,高并发下会出现执行错乱问题,而稳妥的方案,是使用 lua 脚本把它们封装成原子操作。
改造后的代码如下:
public String lock(String key, int timeOutSecond) {for (; ; ) {String stamp = String.valueOf(System.nanoTime());boolean exist = redisTemplate.opsForValue().setIfAbsent(key, stamp, timeOutSecond, TimeUnit.SECONDS);if (exist) {return stamp;}}}public void unlock(String key, String stamp) {redisTemplate.execute(script, Arrays.asList(key), stamp);}
相应的 lua 脚本如下
local stamp = ARGV[1]local key = KEYS[1]local current = redis.call("GET",key)if stamp == current thenredis.call("DEL",key)return "OK"end
可以看到,reids 实现分布式锁,还是有一定难度的。推荐使用 redlock 的 Java 客户端实现 redisson。
我们可以看下 redisson 分布式锁的典型使用代码。
String resourceKey = "goodgirl";RLock lock = redisson.getLock(resourceKey);try {lock.lock(5, TimeUnit.SECONDS);//真正的业务Thread.sleep(100);} catch (Exception ex) {ex.printStackTrace();} finally {if (lock.isLocked()) {lock.unlock();}}
使用redis monitor可以看到具体的操作步骤。
无锁(Lock-Free)
这里就不展开了,大家可以google看下,类似的应用有ConcurrentLinkedQueue
Disruptor 是一个无锁、有界的队列框架,它的性能非常高。它使用 RingBuffer、无锁和缓存行填充等技术,追求性能的极致,在极高并发的场景,可以使用它替换传统的 BlockingQueue。
小结
乐观锁严格来说,并不是一种锁。它提供了一种检测冲突的机制,并在有冲突的时候,采取重试的方法完成某项操作。假如没有重试操作,乐观锁就仅仅是一个判断逻辑而已。
悲观锁每次操作数据的时候,都会认为别人会修改,所以每次在操作数据的时候,都会加锁,除非别人释放掉锁。
乐观锁在读多写少的情况下,之所以比悲观锁快,是因为悲观锁需要进行很多额外的操作,并且乐观锁在没有冲突的情况下,也根本不耗费资源。但乐观锁在冲突比较严重的情况下,由于不断地重试,其性能在大多数情况下,是不如悲观锁的。
由于乐观锁的这个特性,乐观锁在读多写少的互联网环境中被广泛应用。
我们主要看了在数据库层面的一个乐观锁实现,以及Redis 分布式锁的实现,后者在实现的时候,还是有很多细节需要注意的,建议使用 redisson 的 RLock。
当然,乐观锁有它的使用场景。当冲突非常严重的情况下,会进行大量的无效计算;它也只能保护单一的资源,处理多个资源的情况下就捉襟见肘;它还会有 ABA 问题,使用带版本号的乐观锁变种可以解决这个问题。

