不过这样的代码相信你可能看出其中的蹊跷了,这个我们后面来分析,下面来看看 Java 里是怎么用 CAS 的。
Java 里的 CAS
===========
还是前面的问题,如果让你用 Java 的 API 来实现你可能会想到两种方式,一种是加锁(可能是 synchronized 或者其他种类的锁),另一种是使用 atomic
类,如 AtomicInteger
,这一系列类是在 JDK1.5 的时候出现的,在我们常用的 java.util.concurrent.atomic
包下,我们来看个例子:
ExecutorService executorService = Executors.newCachedThreadPool();
AtomicInteger atomicInteger = new AtomicInteger(0);
for (int i = 0; i < 5000; i++) {
executorService.execute(atomicInteger::incrementAndGet);
}
System.out.println(atomicInteger.get());
executorService.shutdown();
这个例子开启了 5000 个线程去进行累加操作,不管你执行多少次答案都是 5000。这么神奇的操作是如何实现的呢?就是依靠 CAS 这种技术来完成的,我们揭开 AtomicInteger
的老底看看它的代码:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField(“value”));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
-
Creates a new AtomicInteger with the given initial value.
-
@param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
-
Gets the current value.
-
@return the current value
*/
public final int get() {
return value;
}
/**
-
Atomically increments by one the current value.
-
@return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
这里我只帖出了我们前面例子相关的代码,其他都是类似的,可以看到 incrementAndGet
调用了 unsafe.getAndAddInt
方法。Unsafe
这个类是 JDK 提供的一个比较底层的类,它不让我们程序员直接使用,主要是怕操作不当把机器玩坏了。。。(其实可以通过反射的方式获取到这个类的实例)你会在 JDK 源码的很多地方看到这家伙,我们先说说它有什么能力:
-
内存管理:包括分配内存、释放内存
-
操作类、对象、变量:通过获取对象和变量偏移量直接修改数据
-
挂起与恢复:将线程阻塞或者恢复阻塞状态
-
CAS:调用 CPU 的 CAS 指令进行比较和交换
-
内存屏障:定义内存屏障,避免指令重排序
这里只是大致提一下常用的操作,具体细节可以在文末的参考链接中查看。下面我们继续看 unsafe
的 getAndAddInt
在做什么。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public native int getIntVolatile(Object var1, long var2);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
其实很简单,先通过 getIntVolatile
获取到内存的当前值,然后进行比较,展开 compareAndSwapInt
方法的几个参数:
-
var1
: 当前要操作的对象(其实就是AtomicInteger
实例) -
var2
: 当前要操作的变量偏移量(可以理解为 CAS 中的内存当前值) -
var4
: 期望内存中的值 -
var5
: 要修改的新值
所以 this.compareAndSwapInt(var1, var2, var5, var5 + var4)
的意思就是,比较一下 var2
和内存当前值 var5
是否相等,如果相等那我就将内存值 var5
修改为 var5 + var4
(var4
就是 1,也可以是其他数)。
这里我们还需要解释一下 偏移量 是个啥?你在前面的代码中可能看到这么一段:
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField(“value”));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
可以看出在静态代码块执行的时候将 AtomicInteger
类的 value
这个字段的偏移量获取出来,拿这个 long 数据干嘛呢?在 Unsafe
类里很多地方都需要传入 obj
和偏移量,结合我们说 Unsafe
的诸多能力,其实就是直接通过更底层的方式将对象字段在内存的数据修改掉。
使用上面的方式就可以很好的解决多线程下的原子性和可见性问题。由于代码里使用了 do while
这种循环结构,所以 CPU 不会被挂起,比较失败后重试,就不存在上下文切换了,实现了无锁并发编程。
CAS 存在的问题
=========
自旋的劣势
你留意上面的代码会发现一个问题,while
循环如果在最坏情况下总是失败怎么办?会导致 CPU 在不断处理。像这种 while(!compareAndSwapInt)
的操作我们称之为自旋,CAS 是乐观的,认为大家来并不都是修改数据的,现实可能出现非常多的线程过来都要修改这个数据,此时随着并发量的增加会导致 CAS 操作长时间不成功,CPU 也会有很大的开销。所以我们要清楚,如果是读多写少的情况也就满足乐观,性能是非常好的。
ABA 问题
提到 CAS 不得不说 ABA 问题,它是说假如内存的值原来是 A,被一个线程修改为了 B,此时又有一个线程把它修改为了 A,那么 CAS 肯定是操作成功的。真的这样做的话代码可能就有 bug 了,对于修改数据为 B 的那个线程它应该读取到 B 而不是 A,如果你做过数据库相关的乐观锁机制可能会想到我们在比较的时候使用一个版本号 version
来进行判断就可以搞定。在 JDK 里提供了一个 AtomicStampedReference
类来解决这个问题,来看一个例子:
int stamp = 10001;
AtomicStampedReference stampedReference = new AtomicStampedReference<>(0, stamp);
stampedReference.compareAndSet(0, 10, stamp, stamp + 1);
System.out.println("value: " + stampedReference.getReference());
System.out.println("stamp: " + stampedReference.getStamp());
它的构造函数是 2 个参数,多传入了一个初始 时间戳,用这个戳来给数据加了一个版本,这样的话多个线程来修改如果提供的戳不同。在修改数据的时候除了提供一个新的值之外还要提供一个新的戳,这样在多线程情况下只要数据被修改了那么戳一定会发生改变,另一个线程拿到的是旧的戳所以会修改失败。
尝试应用
====
既然 CAS 提供了这么好的 API,我们不妨用它来实现一个简易版的独占锁。思路是当某个线程进入 lock
方法就比较锁对象的内存值是否是 false,如果是则代表这把锁它可以获取,获取后将内存之修改为 true,获取不到就自旋。在 unlock
的时候将内存值再修改为 false 即可,代码如下:
public class SpinLock {
private AtomicBoolean mutex = new AtomicBoolean(false);
public void lock() {
while (!mutex.compareAndSet(false, true)) {
// System.out.println(Thread.currentThread().getName()+ " wait lock release");
}
}
public void unlock() {
while (!mutex.compareAndSet(true, false)) {
// System.out.println(Thread.currentThread().getName()+ " wait lock release");
}
}
}
这里使用了 AtomicBoolean
这个类,当然用 AtomicInteger
也是可以的,因为我们只保存一个状态 boolean
占用比较小就用它了。这个锁的实现比较简单,缺点非常明显,由于 while
循环导致的自旋会让其他线程都在占用 CPU,但是也可以使用,关于锁的优化版本实现我会在后续的文章中进行改进和说明,正因为这些问题我们也会在后续研究 AQS
这把利器的优点。
CAS 源码
======
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
最后
针对最近很多人都在面试,我这边也整理了相当多的面试专题资料,也有其他大厂的面经。希望可以帮助到大家。
最新整理面试题
上述的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题
最新整理电子书
最新整理大厂面试文档
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
料,也有其他大厂的面经。希望可以帮助到大家。
最新整理面试题
[外链图片转存中…(img-0arkgZpf-1713433756489)]
上述的面试题答案都整理成文档笔记。也还整理了一些面试资料&最新2021收集的一些大厂的面试真题
最新整理电子书
[外链图片转存中…(img-fEWroS4W-1713433756489)]
最新整理大厂面试文档
[外链图片转存中…(img-RrlnGNUj-1713433756490)]
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!