并发的核心:CAS 与synchronized, Java8是如何优化 CAS 的?

 

你们可能都据说说 Java 中的并发包,若是想要读懂 Java 中的并发包,其核心就是要先读懂 CAS 机制,由于 CAS 能够说是并发包的底层实现原理。html

今天就带你们读懂 CAS 是如何保证操做的原子性的,以及 Java8 对 CAS 进行了哪些优化。java

synchronized:大材小用

咱们先来看几行代码:数组

public class CASTest { static int i = 0; public static void increment() { i++; } }

假若有100个线程同时调用 increment() 方法对 i 进行自增操做,i 的结果会是 100 吗?安全

学会多线程的同窗应该都知道,这个方法是线程不安全的,因为 i++ 不是一个原子操做,因此是很可贵到 100 的。markdown

这里稍微解释下为啥会得不到 100(知道的可直接跳过), i++ 这个操做,计算机须要分红三步来执行。
一、读取 i 的值。
二、把 i 加 1.
三、把 最终 i 的结果写入内存之中。因此,假如线程 A 读取了 i 的值为 i = 0,这个时候线程 B 也读取了 i 的值 i = 0。接着 A把 i 加 1,而后写入内存,此时 i = 1。紧接着,B也把 i 加 1,此时线程B中的 i = 1,而后线程 B 把 i 写入内存,此时内存中的 i = 1。也就是说,线程 A, B 都对 i 进行了自增,但最终的结果倒是 1,不是 2.多线程

那该怎么办呢?解决的策略通常都是给这个方法加个锁,以下并发

public class CASTest { static int i = 0; public synchronized static void increment() { i++; } }

加了 synchronized 以后,就最多只能有一个线程可以进入这个 increment() 方法了。这样,就不会出现线程不安全了。不懂 synchronized 的能够看我这篇文章:完全搞懂synchronized(从偏向锁到重量级锁)框架

然而,一个简简单单的自增操做,就加了 synchronized 进行同步,好像有点大材小用的感受,加了 synchronized 关键词以后,当有不少线程去竞争 increment 这个方法的时候,拿不到锁的方法是会被阻塞在方法外面的,最后再来唤醒他们,而阻塞/唤醒这些操做,是很是消耗时间的。ide

这里可能有人会说,synchronized 到了JDK1.6以后不是作了不少优化吗?是的,确实作了不少优化,增长了偏向锁、轻量级锁等, 可是,就算增长了这些,当不少线程来竞争的时候,开销依然不少,不信你看我另一篇文章的介绍:完全搞懂synchronized(从偏向锁到重量级锁)post

CAS :这种小事交给我

那有没有其余方法来代替 synchronized 对方法的加锁,而且保证 increment() 方法是线程安全呢?

你们看一下,若是我采用下面这种方式,可否保证 increment 是线程安全的呢?步骤以下:

一、线程从内存中读取 i 的值,假如此时 i 的值为 0,咱们把这个值称为 k 吧,即此时 k = 0。

二、令 j = k + 1。

三、用 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)) }

若是你去模拟一下,就会发现,这样写是线程安全的。

这里可能有人会说,第三步的 compareAndSet 这个操做不只要读取内存,还干了比较、写入内存等操做,,,这一步自己就是线程不安全的啊?

若是你能想到这个,说明你是真的有去思考、模拟这个过程,不过我想要告诉你的是,这个 compareAndSet 操做,他其实只对应操做系统的一条硬件操做指令,尽管看似有不少操做在里面,但操做系统可以保证他是原子执行的。

对于一条英文单词很长的指令,咱们都喜欢用它的简称来称呼他,因此,咱们就把 compareAndSet 称为 CAS 吧。

因此,采用 CAS 这种机制的写法也是线程安全的,经过这种方式,能够说是不存在锁的竞争,也不存在阻塞等事情的发生,可让程序执行的更好。

在 Java 中,也是提供了这种 CAS 的原子类,例如:

  1. AtomicBoolean
  2. AtomicInteger
  3. AtomicLong
  4. AtomicReference

具体如何使用呢?我就以上面那个例子进行改版吧,代码以下:

public class CASTest { static AtomicInteger i = new AtomicInteger(0); public static void increment() { // 自增 1并返回以后的结果 i.incrementAndGet(); } }

CAS:谁偷偷更改了个人值

虽然这种 CAS 的机制可以保证increment() 方法,但依然有一些问题,例如,当线程A即将要执行第三步的时候,线程 B 把 i 的值加1,以后又立刻把 i 的值减 1,而后,线程 A 执行第三步,这个时候线程 A 是认为并无人修改过 i 的值,由于 i 的值并无发生改变。而这,就是咱们日常说的ABA问题

对于基本类型的值来讲,这种把数字改变了在改回原来的值是没有太大影响的,但若是是对于引用类型的话,就会产生很大的影响了。

来个版本控制吧

为了解决这个 ABA 的问题,咱们能够引入版本控制,例如,每次有线程修改了引用的值,就会进行版本的更新,虽然两个线程持有相同的引用,但他们的版本不一样,这样,咱们就能够预防 ABA 问题了。Java 中提供了 AtomicStampedReference 这个类,就能够进行版本控制了。

Java8 对 CAS 的优化。

因为采用这种 CAS 机制是没有对方法进行加锁的,因此,全部的线程均可以进入 increment() 这个方法,假如进入这个方法的线程太多,就会出现一个问题:每次有线程要执行第三个步骤的时候,i 的值总是被修改了,因此线程又到回到第一步继续重头再来。

而这就会致使一个问题:因为线程太密集了,太多人想要修改 i 的值了,进而大部分人都会修改不成功,白白着在那里循环消耗资源。

为了解决这个问题,Java8 引入了一个 cell[] 数组,它的工做机制是这样的:假若有 5 个线程要对 i 进行自增操做,因为 5 个线程的话,不是不少,起冲突的概率较小,那就让他们按照以往正常的那样,采用 CAS 来自增吧。

可是,若是有 100 个线程要对 i 进行自增操做的话,这个时候,冲突就会大大增长,系统就会把这些线程分配到不一样的 cell 数组元素去,假如 cell[10] 有 10 个元素吧,且元素的初始化值为 0,那么系统就会把 100 个线程分红 10 组,每一组对 cell 数组其中的一个元素作自增操做,这样到最后,cell 数组 10 个元素的值都为 10,系统在把这 10 个元素的值进行汇总,进而获得 100,二这,就等价于 100 个线程对 i 进行了 100 次自增操做。

固然,我这里只是举个例子来讲明 Java8 对 CAS 优化的大体原理,具体的你们有兴趣能够去看源码,或者去搜索对应的文章哦。

总结

理解 CAS 的原理仍是很是重要的,它是 AQS 的基石,而 AQS 又是并发框架的基石

 

转自:https://www.cnblogs.com/kubidemanong/p/10681550.html

相关文章
相关标签/搜索