欢迎关注我的公众号:石杉的架构笔记(ID:shishan100)
java
周一至周五早8点半!精品技术文章准时送上!面试
上篇文章给你们聊了一下volatile的原理,具体参见:大白话聊聊Java并发面试问题之volatile究竟是什么?。算法
这篇文章给你们聊一下java并发包下的CAS相关的原子操做,以及Java 8如何改进和优化CAS操做的性能。编程
由于Atomic系列的原子类,不管在并发编程、JDK源码、仍是各类开源项目中,都常常用到。并且在Java并发面试中,这一块也属于比较高频的考点,因此仍是值得给你们聊一聊。数组
好,咱们正式开始!假设多个线程须要对一个变量不停的累加1,好比说下面这段代码:安全
实际上,上面那段代码是不ok的,由于多个线程直接这样并发的对一个data变量进行修改,是线程不安全性的行为,会致使data值的变化不遵守预期的值来改变。性能优化
举个例子,好比说20个线程分别对data执行一次data++操做,咱们觉得最后data的值会变成20,其实不是。多线程
最后可能data的值是18,或者是19,都有可能,由于多线程并发操做下,就是会有这种安全问题,致使数据结果不许确。架构
至于为何会不许确?那不在本文讨论的范围里,由于这个通常只要是学过java的同窗,确定都了解过多线程并发问题。并发
因此,对于上面的代码,通常咱们会改造一下,让他经过加锁的方式变成线程安全的:
这个时候,代码就是线程安全的了,由于咱们加了synchronized,也就是让每一个线程要进入increment()方法以前先得尝试加锁,同一时间只有一个线程能加锁,其余线程须要等待锁。
经过这样处理,就能够保证换个data每次都会累加1,不会出现数据错乱的问题。
老规矩!咱们来看看下面的图,感觉一下synchronized加锁下的效果和氛围,至关于N个线程一个一个的排队在更新那个数值。
可是,如此简单的data++操做,都要加一个重磅的synchronized锁来解决多线程并发问题,就有点杀鸡用牛刀,大材小用了。
虽然随着Java版本更新,也对synchronized作了不少优化,可是处理这种简单的累加操做,仍然显得“过重了”。人家synchronized是能够解决更加复杂的并发编程场景和问题的。
并且,在这个场景下,你要是用synchronized,不就至关于让各个线程串行化了么?一个接一个的排队,加锁,处理数据,释放锁,下一个再进来。
对于这种简单的data++类的操做,其实咱们彻底能够换一种作法,java并发包下面提供了一系列的Atomic原子类,好比说AtomicInteger。
他能够保证多线程并发安全的状况下,高性能的并发更新一个数值。咱们来看下面的代码:
你们看上面的代码,是否是很简单!多个线程能够并发的执行AtomicInteger的incrementAndGet()方法,意思就是给我把data的值累加1,接着返回累加后最新的值。
这个代码里,就没有看到加锁和释放锁这一说了吧!
实际上,Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,经过CAS机制保证多线程修改一个数值的安全性
那什么是CAS呢?他的全称是:Compare and Set,也就是先比较再设置的意思。
话很少说,先上图!
咱们来看上面的图,假如说有3个线程并发的要修改一个AtomicInteger的值,他们底层的机制以下:
首先,每一个线程都会先获取当前的值,接着走一个原子的CAS操做,原子的意思就是这个CAS操做必定是本身完整执行完的,不会被别人打断。
而后CAS操做里,会比较一下说,唉!大兄弟!如今你的值是否是刚才我获取到的那个值啊?
若是是的话,bingo!说明没人改过这个值,那你给我设置成累加1以后的一个值好了!
同理,若是有人在执行CAS的时候,发现本身以前获取的值跟当前的值不同,会致使CAS失败,失败以后,进入一个无限循环,再次获取值,接着执行CAS操做!
好!如今咱们对照着上面的图,来看一下这整个过程:
上述整个过程,就是所谓Atomic原子类的原理,没有基于加锁机制串行化,而是基于CAS机制:先获取一个值,而后发起CAS,比较这个值被人改过没?若是没有,就更改值!这个CAS是原子的,别人不会打断你!
经过这个机制,不须要加锁这么重量级的机制,也能够用轻量级的方式实现多个线程安全的并发的修改某个数值。
可是这个CAS有没有问题呢?确定是有的。好比说大量的线程同时并发修改一个AtomicInteger,可能有不少线程会不停的自旋,进入一个无限重复的循环中。
这些线程不停地获取值,而后发起CAS操做,可是发现这个值被别人改过了,因而再次进入下一个循环,获取值,发起CAS操做又失败了,再次进入下一个循环。
在大量线程高并发更新AtomicInteger的时候,这种问题可能会比较明显,致使大量线程空循环,自旋转,性能和效率都不是特别好。
因而,当当当当,Java 8推出了一个新的类,LongAdder,他就是尝试使用分段CAS以及自动分段迁移的方式来大幅度提高多线程高并发执行CAS操做的性能!
在LongAdder的底层实现中,首先有一个base值,刚开始多线程来不停的累加数值,都是对base进行累加的,好比刚开始累加成了base = 5。
接着若是发现并发更新的线程数量过多,就会开始施行分段CAS的机制,也就是内部会搞一个Cell数组,每一个数组是一个数值分段。
这时,让大量的线程分别去对不一样Cell内部的value值进行CAS累加操做,这样就把CAS计算压力分散到了不一样的Cell分段数值中了!
这样就能够大幅度的下降多线程并发更新同一个数值时出现的无限循环的问题,大幅度提高了多线程并发更新数值的性能和效率!
并且他内部实现了自动分段迁移的机制,也就是若是某个Cell的value执行CAS失败了,那么就会自动去找另一个Cell分段内的value值进行CAS操做。
这样也解决了线程空旋转、自旋不停等待执行CAS操做的问题,让一个线程过来执行CAS时能够尽快的完成这个操做。
最后,若是你要从LongAdder中获取当前累加的总值,就会把base值和全部Cell分段数值加起来返回给你。
不知道你们有没有发现这种高并发访问下的分段处理机制,在不少地方都有相似的思想体现!由于高并发中的分段处理机制其实是一个很常见和经常使用的并发优化手段。
在咱们以前的一篇讲分布式锁的文章:(每秒上千订单场景下的分布式锁高并发优化实践!),也是用到了分段加锁以及自动分段迁移/合并加锁的一套机制,来大幅度几十倍的提高分布式锁的并发性能。
因此其实不少技术,思想都是有殊途同归之妙的。
END
若有收获,请帮忙转发,您的鼓励是做者最大的动力,谢谢!
一大波微服务、分布式、高并发、高可用的原创系列文章正在路上
欢迎扫描下方二维码,持续关注:
石杉的架构笔记(id:shishan100)
十余年BAT架构经验倾囊相授