java高阶面试问题java8中的CAS讲解

createh51周前 (03-06)技术教程2

一、前情回顾

上次给大家讲解了volatile的原理,这次给大家聊一下cas的相关的原子操作,及在java8中如何优化操作性能的。

Atomic相关的原子类,在并发编程、JDK底层源码、还有其他的中间中项目中,都经会经常碰到的。并且在java高级面试中,面试频率还是比较高的。所以还是值得给大家讲一下。

二、为什么要用CAS,如不用会遇到什么问题

我们看一下以下代码,多个线程对一个数进行递加


一般程序员觉得这段代码是没问题的,但是水平稍微好的程序员,就知道这段代码是有问题的,那这段代码到底有什么问题了,我们继续向下讲解。

原因是多个线程直接这样并发对一个变量进行修改,会出现线程不安全性行为,会导致num不能按我们期望的预期值的结果。比如现在要求多个线程进行每次递加操作,每次+1,加到100停止,而我们的期望的预期值肯定是100,而结果可能是99、98等结果,因为多线程并发操作下,就会出现这种安全问题,导致数据不准确。

三、如何能唯是我们的预期值了

稍微有点经验的程序员,第一感觉就是使用synchronized

没错,synchronized确实能保证得到的值是我们预期的值,我们继续讲下去,看看synchronized是不是我们最优的选择。

对上面的代码进行优化一下,通过加synchronized让他变成线程安全


加了synchronized以后,代码就是线程安全了,也是让每个线程addNUM方法之前进行加锁,并且同一时间只有一个线程进行加锁、解锁,其他线程只能处于等待状态。这个就可以保证num的最终的值同,是我们想要的预期值了,不会出现数据错乱的问题。

我们看一张执行的示意图


大家也看到,这段代码并复杂,这段代码如此简单,就要加一个很重的synchronized,就有点大材小用的感觉,还有细心的人也发现,有一个问题就是多线程都是在排队状态,那如果100-200个线程对num加到100000了,怎么办,虽然能保证结果是正确的,但是会影响到性能。

四、有其它的办法吗?这时我们想到Atomic原子类及其底层原理

对于简单的num++这类的操作,可以换成其它性能更高的做法,java并发包里提供了一系列的Atomic原子类,比如AtomicInteger类。

他可以在多线程下操作,能保证线程安全,看看下面的代码



看以上代码,是不是简洁好多,是不是也很简单,多个线程并发的执行AtomicInteger中的incrementAndGet方法,就是把num值累加1操作,返回累加后的最新值。

以上代码中,并没有使用加锁机制,但能保证结果是我们预期的值,Atomic原子类底层用的不是锁的概念,而是通过无锁化CAS机制,通过CAS机制能保证多个线程操作修改的值,能达到我们预期的值。

我们先看一下一张CAS的运行图


我通过以上图,再来分析一下,目前有三个线程并发的要修改AtomicInteger的值,运行原理如下:

首先,每个线程都会获取当前的值,然后进行判断,我拿到值和原始值是否一致,如果一致进行递增操作

如果此时有线程在执行递增或递减操作,发现自己获取的值与原始值不一致,会导致CAS失败,失败以后,会重新再次获取最新值,再进行操作。

我们来一步一步说一下,上面的图,其实我图中已经说的比较明确,但怕大家不太理解,我再以文字描述一下:

案例背景:

本案例中三个线程,对一个变量进行并发递增操作,线程1、线程2、线程3

第一步:假如三个线程现在并发的操作,而线程1抢先了一步把当前值的值拿到了,当前值为0,拿到以后,对其进行CAS操作,也就是递增操作,进行递增操作时,会判断变量原始值是否与拿到的值是否相同,如果相同则进行加1操作。判断结果为是0,则正常加1操作,这时,变量的最新值为1.

第二步:线程2、线程3这时同时把变量的最新值给拿到了,这时最新值被线程+1以后,值为1,所以线程2、线程3拿到的最新值为1,这时线程2抢先一步,进行CAS操作,判断当前值是否是1,结果是1,则进行+1操作。

第三步:接着线程3进行CAS操作,同线程逻辑一样,也要判断的拿到的值是否与变量的最新值一致,而最新值被线程2进行CAS操作,不是1,而是2。但是线程3当时拿到的值是1,这时CAS判断失败,无法进行CAS操作。

第四步:线程3判断失败以后,怎么操作了,接着线程3会从新获取变量的最新值,最新值是2,拿到新值以后,再进行CAS操作,经过CAS判断以后,当前值与最新值是一致的,CAS操作是成功的。

以上整个过程,就是Atomic原子类的原理,没有基本锁机制串行化,而是基于CAS判断来完成的,通过这个机制,不需要加synchronized这类的重量级锁,同时能保证多个线程对一个变量操作,线程安全。

五、java8对CAS的优化

那我们来看看这样做是不是觉得很好,但是大家有没有认真考虑,这个有没有问题了,我肯定的说,是有问题,这样的使用,只能说对于线程比较少的情况下,如果线程多了,可能我个线程不停的获取、判断,值,会进行一个循环状态。

线程会不停的获取,然后发起CAS操作,值又被修改过,又重新获取值,然后再次进入循环,这样会导致大量线程空循环,损耗机器性能,影响系统性能。

但有没有办法了,有,java8给我们提供了另一个数据结构LongAddr,他的原理是分段CAS,意思是分成多个CAS操作,最终将多个CAS的值累加,返回最新的值。


在LongAdder底层实现中,有一个base值,刚开始线程不多的时候,都是对base进行累加的。

如果线程多的时候,就会进行分段CAS机制,也就是内部有多个cell,每个数组是一个数值分段。

这时,大量的线程分别去不同的cell内部的value进行修改操作,最后运行完将,cell的值累加,累加完返回base值,最终返回新的值,

cell内部实现了自动分段迁移的机制,也就是如果某个cell的value失败了,那么就会自动去找另一个cell分段内的value值进行CAS操作。

这样也解决了线程空旋转、自旋不停等待执行CAS操作的问题,让一个线程过来执行CAS时可以尽快完成操作。

六、总结

这种高并发访问下的机制分段处理,在很多地方都有类似的思想体现,尤其是一线中间件中大量没被使用,因为高并发中的分段处理机制实际上是一个很常和常用的并发优化手段。

好了今天这篇先写到这,后期我再讲一上什么是AQS等并发技术

相关文章

Java 判断对象是否所有属性为空,大家觉得这样写可以吗?

序言:在开发Excel数据导入的时候,后台拿到Excel中的数据并接收到List泛型集合中,发现有很多对象的属性全部为null,想通过代码将这些无效的数据给过滤掉,下面是过滤的具体操作。ObjectU...

java8之Optional 判空,简化判空操作

导语在没有用Optional判空之前,你是否也像下面的代码一样判空呢?如果是,请往下看,Optional 相对传统判空的优势。传统阶层判空为什么要用Optional,它到底是什么东西你也看到了上面的那...

Java中的空指针怎么处理?

#暑期创作大赛#Java程序员工作中遇到最多的错误就是空指针异常,无论你多么细心,一不留神就从代码的某个地方冒出NullPointerException,令人头疼。1. 对象设置默认值Object o...

java catch 空指针异常_关于Java:捕获空指针异常

我想问的是有关Java的多数知识,但我想它适用于许多语言。考虑,if(myVariable==null){doSomethingAboutIt();}else carryOn(myVariable);...

java 中如何避免空指针

在Java中,空指针异常(NullPointerException)是常见的运行时异常,通常是因为在对一个空对象(null)进行方法调用、字段访问等操作时引起的。为了避免空指针异常,可以采取以下几种方...

工作5年总结9种方式,帮你减少Java程序中80%的空指针异常

Java程序员工作中遇到最多的错误就是空指针异常,无论你多么细心,一不留神就从代码的某个地方冒出NullPointerException,真是令人头疼。到底怎么避免空指针异常?下面的方法能够帮助你。1...