彻底理解Java并发:乐观锁与CAS
创始人
2024-04-01 03:03:52

本篇内容包括:悲观锁与乐观锁的概述、CAS(Compare And Swap)比较并交换的介绍、非阻塞算法与ABA问题,以及对 Java 中 CAS 的实现解读(AtomicInteger 对 CAS 的实现,Unsafe 类简介)。

一、悲观锁与乐观锁

锁的一种宏观分类方式是悲观锁和乐观锁。悲观锁与乐观锁并不是特指某个锁(Java 中没有哪个 Lock 实现类就叫 PessimisticLock 或 OptimisticLock),而是在并发情况下的两种不同策略。

1、乐观锁(Optimistic Lock)

乐观锁认为自己在使用数据的时候,不会有别的线程修改数据,所以不会加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据

锁实现:CAS 算法,例如 Java 原子类 AtomicInteger 的自增是通过 CAS自 旋实现

使用场景:读操作较多,不加锁的特点能使其读操作的性能大幅提升

2、悲观锁(Pessimistic Lock)

悲观锁认为自己在使用数据时一定有别的线程来修改数据,在获取数据时会先加锁,确保数据不会被其他线程修改

锁实现:关键字 syncchronized 、接口 lock 的实现类

使用场景:写操作较多,先加锁可以保证写操作时的数据正确

悲观锁阻塞事务,乐观锁回滚重试,它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。

Java 中的并发锁大致分为隐式锁和显式锁两种。隐式锁就是我们最常使用的 synchronized 关键字,显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为 ReentrantLock 和 ReentrantReadWriteLock,这两个类都是基于 AQS(AbstractQueuedSynchronizer) 实现的。还有的地方将 CAS 也称为一种锁,在包括 AQS 在内的很多并发相关类中,CAS 都扮演了比较重要的角色。


二、CAS(Compare And Swap)

1、比较并交换

CAS,即「比较并交换」。java.util.concurrent 包中借助 CAS 实现了区别于 synchronouse 同步锁的一种乐观锁。CAS 是解决多线程并行情况下使用锁造成性能损耗的一种机制。

CAS 操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了:“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可“。

在 JAVA 中,sun.misc.Unsafe 类提供了硬件级别的原子操作来实现这个 CAS。java.util.concurrent 包下的大量类都使用了这个 Unsafe.java 类的 CAS 操作。

2、非阻塞算法

一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法,下面来看一下原子操作(AtomicInteger Jdk1.5)的 ++i 是如何借助 CAS 实现的:

public final int incrementAndGet() {for (;;) {int current = get();int next = current + 1;if (compareAndSet(current, next))return next;}
}
public final boolean compareAndSet(int expect, int update) {   return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

其中,compareAndSwapInt 是借助 C 来调用 CPU 底层指令实现的。

3、ABA问题

因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A

AtomicStampedReference 里面增加了一个时间戳,也就是说每一次修改只需要设置不同的版本号以解决 ABA 问题。

此外CAS自旋时,如果长时间不成功,就会带来循环时间长,开销大的问题。


三、对 Java 中 CAS 的实现解读

1、AtomicInteger 对 CAS 的实现

AtomicInteger 是一个支持原子操作的 Integer 类,就是保证对 AtomicInteger 类型变量的增加和减少操作是原子性的,不会出现多个线程下的数据不一致问题。如果不使用 AtomicInteger,要实现一个按顺序获取的 ID,就必须在每次获取时进行加锁操作,以避免出现并发时获取到同样的 ID 的现象。

接下来通过源代码来看 Jdk8 中 AtomicInteger 中 incrementAndGet() 方法的实现,下面是具体的代码。

// JDK 8
public final int incrementAndGet() {return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

在 incrementAndGet 里,我们可以看到使用了 Unsafe 类,下面是 Unsafe 里提供的 getAndAddInt 方法:

public final int getAndAddInt(Object o, long offset, int delta) {int v;do {v = getIntVolatile(o, offset);} while (!compareAndSwapInt(o, offset, v, v + delta));return v;
}

通过一个 do while 语句来做一个主体实现的在 while 语句里核心调了 compareAndSwapInt() 方法:

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

此方法为 native 方法,compareAndSwapInt 基于的是 CPU 的 CAS 指令来实现的。所以基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。并且由于 CAS 操作是 CPU 原语,所以性能比较好。

回到 incrementAndGet 中:我们传过来的第一个值是当前的对象,第二个值是我们当前的值(比如如果我们要实现2+1)那么 offset 就是 2 delta 就是1,这里的 v,它是我们调用底层的方法v v = this.getIntVolatile(o, offset); 获取底层当前的值。如果没有其他线程来处理 o 这个变量的时候,它的正常返回值应该是 2,因此传到 compareAndSwapInt 的参数就是(o,2,2,2+1),这个方法想达到的目标就是对于 o 这个对象,如果当前的这个值和底层的这个值相等的情况下,就把它更新成后面那个值 v + delta。

当我们一个方法进来的时候,我们 offset 的值是2,我们第一次取出来 v 的值也等于 2,但是当我们在执行更新成 3 的时候 也就是这句代码 while (!compareAndSwapInt(o, offset, v, v + delta));可能会被其它线程更改,所以我们要判断 offset 是否与 v 是相同的,只有是相同的,才允许它更新为 3。通过这样不停的循环来判断。就能保证期望的值和底层的值相同。

CAS比较与交换的伪代码可以表示为:
do{备份旧数据;基于旧数据构造新数据;
}while(!CAS( 内存地址,备份的旧数据,新数据 ))

Java中的乐观锁大部分都是基于CAS(Compare And Swap,比较和交换)操作实现的,CAS设一种原子操作,在对数据操作之前,首先会比较当前值跟传入值是否一样,如果一样咋更新,否则不执行更新操作直接返回失败状态。compareAndSwapInt 也是 CAS 的核心。

2、Unsafe 类简介

Unsafe 类和 C++ 有点类似,在 Java 中是没有办法直接操作内存的,但是 Unsafe 类却可以间接的让程序员操作内存区域。

Unsafe 是位于 sun.misc 包下的一个类。Unsafe 提供的 API 大致可分为内存操作、CAS、Class 相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。由于并发相关的源码很多用到了 CAS,比如 java.util.concurrent.atomic 相关类、AQS、CurrentHashMap 等相关类。

CAS 主要相关源码:

    /*** 参数说明* @param o             包含要修改field的对象* @param offset        对象中某个参数field的偏移量,该偏移量不会改变* @param expected      期望该偏移量对应的field值* @param x             更新值* @return              true|false*/public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...