深入分析 Java 乐观锁

前言

激烈的锁竞争,会造成线程阻塞挂起,导致系统的上下文切换,增加系统的性能开销。那有没有不阻塞线程,且保证线程安全的机制呢?——乐观锁

乐观锁是什么?

操作共享资源时,总是很乐观,认为自己可以成功。在操作失败时(资源被其他线程占用),并不会挂起阻塞,而仅仅是返回,并且失败的线程可以重试。

优点:

  • 不会死锁
  • 不会饥饿
  • 不会因竞争造成系统开销

乐观锁的实现

CAS 原子操作

CAS。在 java.util.concurrent.atomic 中的类都是基于 CAS 实现的。

以 AtomicLong 为例,一段测试代码:

@Test
public void testCAS() {
    AtomicLong atomicLong = new AtomicLong();
    atomicLong.incrementAndGet();
}

java.util.concurrent.atomic.AtomicLong#incrementAndGet 的实现方法是:

public final long incrementAndGet() {
    return U.getAndAddLong(this, VALUE, 1L) + 1L;
}

其中 U 是一个 Unsafe 实例。

private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();

本文使用的源码是 JDK 11,其 getAndAddLong 源码为:

@HotSpotIntrinsicCandidate
public final long getAndAddLong(Object o, long offset, long delta) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!weakCompareAndSetLong(o, offset, v, v + delta));
    return v;
}

可以看到里面是一个 while 循环,如果不成功就一直循环,是一个乐观锁,坚信自己能成功,一直 CAS 直到成功。最终调用了 native 方法:

@HotSpotIntrinsicCandidate
public final native boolean compareAndSetLong(Object o, long offset,
                                                long expected,
                                                long x);

处理器实现原子操作

从上面可以看到,CAS 是调用处理器底层的指令来实现原子操作,那么处理器底层是如何实现原子操作的呢?

处理器的处理速度>>处理器与物理内存的通信速度,所以在处理器内部有 L1、L2 和 L3 的高速缓存,可以加快读取的速度。

单核处理器能够保存内存操作是原子性的,当一个线程读取一个字节,所以进程和线程看到的都是同一个缓存里的字节。但是多核处理器里,每个处理器都维护了一块字节的内存,每个内核都维护了一个字节的缓存,多线程并发会存在缓存不一致的问题。

那处理器如何保证内存操作的原子性呢?

  • 总线锁定:当处理器要操作共享变量时,会在总线上发出 Lock 信号,其他处理器就不能操作这个共享变量了。
  • 缓存锁定:某个处理器对缓存中的共享变量操作后,就通知其他处理器重新读取该共享资源。

LongAdder vs AtomicLong

本文分析的 AtomicLong 源码,其实是在循环不断尝试 CAS 操作,如果长时间不成功,就会给 CPU 带来很大开销。JDK 1.8 中新增了原子类 LongAdder,能够更好应用于高并发场景。

LongAdder 的原理就是降低操作共享变量的并发数,也就是将对单一共享变量的操作压力分散到多个变量值上,将竞争的每个写线程的 value 值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的 value 值进行 CAS 操作,最后在读取值的时候会将原子操作的共享变量与各个分散在数组的 value 值相加,返回一个近似准确的数值。

LongAdder 内部由一个base变量和一个 cell[] 数组组成。当只有一个写线程,没有竞争的情况下,LongAdder 会直接使用 base 变量作为原子操作变量,通过 CAS 操作修改变量;当有多个写线程竞争的情况下,除了占用 base 变量的一个写线程之外,其它各个线程会将修改的变量写入到自己的槽 cell[] 数组中。

一个测试用例:

@Test
public void testLongAdder() {
    LongAdder longAdder = new LongAdder();
    longAdder.add(1);
    System.out.println(longAdder.longValue());
}

先看里面的 longAdder.longValue() 代码:

public long longValue() {
    return sum();
}

最终是调用了 sum() 方法,是对里面的 cells 数组每项加起来求和。这个值在读取的时候并不准,因为这期间可能有其他线程在并发修改 cells 中某个项的值:

public long sum() {
    Cell[] cs = cells;
    long sum = base;
    if (cs != null) {
        for (Cell c : cs)
            if (c != null)
                sum += c.value;
    }
    return sum;
}

add() 方法源码:

public void add(long x) {
    Cell[] cs; long b, v; int m; Cell c;
    if ((cs = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (cs == null || (m = cs.length - 1) < 0 ||
            (c = cs[getProbe() & m]) == null ||
            !(uncontended = c.cas(v = c.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}

add 具体的代码本篇文章就不详细叙述了~

代码

代码和思维导图在 GitHub 项目中,欢迎大家 star!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容