前言

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

乐观锁是什么?

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

优点:

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

乐观锁的实现

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 具体的代码本篇文章就不详细叙述了~

公众号

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

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

深入分析 Java 乐观锁的更多相关文章

  1. JAVA乐观锁实现-CAS

    是什么 全称compare and swap,一个CPU原子指令,在硬件层面实现的机制,体现了乐观锁的思想. JVM用C语言封装了汇编调用.Java的基础库中有很多类就是基于JNI调用C接口实现了多线 ...

  2. Java乐观锁实现之CAS操作

    介绍CAS操作前,我们先简单看一下乐观锁 与 悲观锁这两个常见的锁概念. 悲观锁: 从Java多线程角度,存在着“可见性.原子性.有序性”三个问题,悲观锁就是假设在实际情况中存在着多线程对同一共享的竞 ...

  3. Java乐观锁的实现原理(案例)

    简要说明: 表设计时,需要往表里加一个version字段.每次查询时,查出带有version的数据记录,更新数据时,判断数据库里对应id的记录的version是否和查出的version相同.若相同,则 ...

  4. JAVA乐观锁、悲观锁实现

    一.名词解释 1.悲观锁:认为每次对数据库的操作(查询.修改)都是不安全的,因此每次操作都会把这条数据锁掉,直到本次操作完毕释放该锁 2.乐观锁:查询数据的时候总是认为是安全的,不会锁数据:等到更新数 ...

  5. Java乐观锁、悲观锁

    乐观锁 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号 ...

  6. java 乐观锁 vs 悲观锁

    在数据库的锁机制中介绍过,数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性. 悲观锁其实就是 完全同步 比如 sync ...

  7. java 乐观锁CAS

    乐观锁是一种思想,本身代码里并没有lock或synchronized关键字进行修饰.而是采用一种version. 即先从数据库中查询一条记录得到version值,在更新这条记录时在where条件中对这 ...

  8. [数据库事务与锁]详解八:底理解数据库事务乐观锁的一种实现方式——CAS

    注明: 本文转载自http://www.hollischuang.com/archives/1537 在深入理解乐观锁与悲观锁一文中我们介绍过锁.本文在这篇文章的基础上,深入分析一下乐观锁的实现机制, ...

  9. [数据库锁机制] 深入理解乐观锁、悲观锁以及CAS乐观锁的实现机制原理分析

    前言: 在并发访问情况下,可能会出现脏读.不可重复读和幻读等读现象,为了应对这些问题,主流数据库都提供了锁机制,并引入了事务隔离级别的概念.数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务 ...

随机推荐

  1. 基于FFmpeg的Dxva2硬解码及Direct3D显示(五)

    解码及显示 目录 解码及显示 解码 显示 资源清理 解码 循环读取视频帧 AVPacket packet = { 0 }; while (av_read_frame(m_pFmtCtx, &p ...

  2. linux绑定盘符

    [root@centos6 ~]# udevadm info -q path -n /dev/sdb [root@centos6 ~]# udevadm info -q path -n /dev/sd ...

  3. 微服务下的持续集成-Jenkins自动化部署GitHub项目

    @ 目录 一.前言 二.DevOps概念 三.为什么要做持续集成 四.常见云服务 五.手动部署Jenkins 5.1 准备工作 5.2 下载 5.3 启动 5.4 配置 5.5 Jenkins 首页 ...

  4. 用JavaScript做精灵图

    用JavaScript做精灵图 精灵图可以不用在给每一个小块一 一的修改位置.主要原理是找到整张的背景图与li的下标的数学关系. 这是一大张背景图,这个背景图的位置其实是有规律的,每两张之间间隔一个固 ...

  5. DDD(领域驱动设计)--战略设计

    领域 领域是一个组织所做的事情以及其中所包含的一切.商业机构通常会确定一个市场,然后在这个市场中销售产品和服务.每个组织都有它自己的业务范围和做事方式. 领域就是解决一个特定范围内的业务问题. 如何分 ...

  6. 原生sql查询返回结果集处理方法

    今天博主用原生写查询的时候发现,查询出来的居然不是我数据表里的数据,而是一个对象 object(mysqli_result)#2 (5) { ["current_field"]=& ...

  7. 直面秋招!非科班生背水一战,最终拿下阿里等大厂offer!

    前言 2020年已经接近到9月份了,很多粉丝朋友都对金九银十雀雀欲试了吧!也有很多朋友向我求教经验,因为我自己工作相对于稳定,在这里给大家分享一个粉丝朋友的经历,他作为一个曾经的菜鸡面试者,在不断的失 ...

  8. linux中的fork炸弹

    学习bash脚本看到一段代码(老鸟应该知道)挺有意思,一时看不懂.该命令不需要管理员即可运行,请不要在你的机器上使用以下脚本,否则你知道你在干什么 :() { :|: & };: 参考链接:h ...

  9. ABBYY FineReader如何将图片转换为Excel

    ABBYY FineReader的OCR文字识别功能很强大,不但可以将文件转换为文本文档或Word文档,也可以识别PDF文件或者图片上的表格,并且转换为Excel文件.下面我就为大家演示一下怎么用AB ...

  10. FL Studio中如何制作和混音Dutch Lead(上)

    Dutch Lead是电子音乐类型Dutch House以及Bigroom House中常用的Lead音色,这一篇文章中我将从制作和混音两方面来讲解Dutch Lead. (一).Dutch Lead ...