Java8对CAS做了哪些优化? CAS如何保证操作的原子性
如果想了解Java中的并发包,首先了解CAS的机制是必不可少的,因为CAS可以说是实现并发包的原理。
今天给大家介绍一下CAS是如何保证操作的原子性以及Java8对CAS做了哪些优化。
同步:重载和未充分利用
我们先看几行代码:
public class CASTest {
static int i = 0;
public static void increment() {
i++;
}
}
复制代码
如果有100个线程同时调用increment()方法来增加i,i的结果会是100吗?
学过多线程的同学应该知道,这种方法不是线程安全的。由于i++不是原子操作,所以很难得到100。
这里简单解释一下为什么不能得到100(如果你知道,可以直接跳过)。 i++操作需要计算机分三步完成。 1. 读取i的值。 2. i 加 1。 3. 将 i 的最终结果写入内存。因此,当线程 A 读取 i 的值为 i = 0 时,此时线程 B 也读取 i 的值为 i = 0。然后 A 将 i 加 1 并将其写入内存。此时,i = 1。紧接着,B也将i加1。此时,在线程i中,B = 1。然后线程B将i写入内存。此时内存中i = 1,也就是说,线程A和B都对i进行了自增,但最终的结果是1,而不是2。
怎么办?解决的策略一般是给这个方法加一把锁,如下
public class CASTest {
static int i = 0;
public synchronized static void increment() {
i++;
}
}
复制代码
同步加法后,最多有一个线程可以进入increment()方法。这样就没有线程安全了。如果不懂sync,可以看我的文章:深入理解Sync(从偏向锁到重量级锁)
但是自增这样简单的操作,为sync添加sync,似乎有点大材小用了。 ,加上synchronized关键字后,当有很多线程竞争increment方法时,无法加锁的方法就会被阻塞在方法之外,最后被唤醒,被阻塞/唤醒这些操作非常耗时。
这里可能有人会说,从JDK1.6开始sync不是做了很多优化吗?是的,做了很多优化,增加了偏向锁、轻型锁等。但即使添加它们,当许多线程竞争时,开销仍然很高。不信,看我另一篇文章的介绍:深入理解synchronized(偏向锁到重量级锁)
CAS:这么小事就交给我吧
有没有其他方法可以替代锁同步方法并确保方法increment()是线程安全的?毛呢布料?
我们看看如果我使用下面的方法,是否可以保证增量是防线程的?步骤如下:
1.线程从内存中读取 i 的值。如果当前 i 的值为 0,我们将这个值称为 k,即 k = 0。
2。设 j = k + 1。
3。将 k 的值与内存中 i 的值进行比较。如果它们相等,则意味着没有其他线程更改了 i 的值,我们将 j 的值(当前为 1)写入内存;如果不等于(意味着i的值已被其他线程修改),那么我们不将j的值写入内存,而是跳回步骤1继续这三个操作。
翻译成代码,就是:
public static void increment() {
do{
int k = i;
int j = k + 1;
}while (compareAndSet(i, k, j))
}
复制代码
如果你模拟一下这个,你会发现这样写是线程安全的。
这里可能有人会说,第三步中的comparisonAndSet不仅仅只是读取内存,还可以进行比较、写入内存等操作。这一步本身是线程安全的吗?
如果你能思考一下,说明你真的思考过这个过程,并且模拟过。但我想告诉大家的是,这个compareAndSet操作实际上只对应了操作系统硬件操作指令,虽然看起来操作很多,但是操作系统可以保证它是原子执行的。
对于英文单词较长的指令,我们都喜欢用缩写来命名,所以我们调用AndSetCAS来进行比较。
因此,使用CAS机制的write方法也是线程安全的。所以可以说不存在锁的竞争,不存在阻塞等情况,可以让程序更好的运行。
Java也提供了这样的CAS原子类,例如:
- AtomicBoolean
- AtomicInteger
- AtomicLong♓♓♿♿♿♿我回顾一下上面的例子,代码如下:
- CAS:谁偷偷改变了我的值
这种CAS机制虽然可以保证increment()方法,但是还是存在问题,比如当线程A开始执行第三步时,线程B将i的值加1然后立即将 i 的值减 1 。然后线程A执行第三步。目前,线程A认为没有人改变它。 i 的值,因为 i 的值没有改变。这就是我们通常所说的ABA问题。
对于基本类型值,将数字 更改回原始值 不会产生太大影响,但当它是引用类型时,就会产生太大影响。
我们来做版本控制吧
为了解决这个ABA问题,我们可以引入版本控制。例如,每次线程更改引用值时,版本都会更新,即使两个线程具有相同的引用但版本不同,因此我们可以避免 ABA 问题。 Java 提供了一个 AtomicStampedReference 类来启用版本控制。
CAS Java8 优化。
因为这种CAS机制不会对方法加锁,所以所有线程都可以进入increment()方法。如果太多线程进入这个方法,就会出现一个问题:每次线程开始执行第三步时,i的值总是会改变,所以线程返回到第一步并重新开始。
这就导致了一个问题:由于线程太密集,太多人想要改变i的值,而大多数人无法改变它,白白浪费资源。
为了解决这个问题,Java8引入了cell[]数组。它是这样工作的:如果有5个线程想要自增i,就会产生冲突,因为5个线程很少。概率低了,就让他们像以前一样用CAS来增加吧。
但是,如果有100个线程想要用i自增,那么此时冲突就会显着增加,系统会将这些线程分配到元胞数组的不同元素上。如果cell[10]中有10个元素,且该元素的初始值为0,则系统将100个线程分为10组,每组对cell数组的一个元素进行增量操作,这样最后元胞数组的10个元素的值都是10。系统将这10个元素的值相加,得到100。这相当于100个线程对i执行100次增量操作。
当然,我只是举一个例子来说明Java 8中CAS优化的大致原理。如果您对具体内容感兴趣,可以阅读源代码或搜索相关文章。
总结
了解CAS的原理非常重要。它是AQS的基石,AQS是并发框架的基石。等我有时间了,我会写一篇关于AQS的文章。
作者:帅迪
链接:https://juejin.im/post/5cd4e7996fb9a0323e3ad6ff
来源:掘金。版权所有
商业转载请联系作者授权。非商业转载请注明出处。
- CAS:谁偷偷改变了我的值
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。