java 乐观锁 实例_深入分析 Java 乐观锁

本文深入解析了 Java 乐观锁的概念,指出其避免线程阻塞的优点,并通过 CAS(Compare And Swap)原子操作举例说明。文章详细介绍了 CAS 在 AtomicLong 中的应用,探讨了处理器如何保证原子操作,并对比了 AtomicLong 与 LongAdder 的性能差异,揭示了 LongAdder 在高并发场景下的优势。
摘要由CSDN通过智能技术生成

a18bf380a61e514e17923ae365987385.png

前言

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

乐观锁是什么?

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

优点:不会死锁

不会饥饿

不会因竞争造成系统开销

乐观锁的实现

CAS 原子操作

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

954370a0edd5f6730dbe58b76cd09f2a.png

以 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 的高速缓存,可以加快读取的速度。

fd72f018baa8e14011d55a792131251d.png

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

那处理器如何保证内存操作的原子性呢?总线锁定:当处理器要操作共享变量时,会在总线上发出 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 具体的代码本篇文章就不详细叙述了~

公众号

coding 笔记、点滴记录,以后的文章也会同步到公众号(Coding Insight)中,希望大家关注^_^

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值