1.回顾上一篇文章,给大家讲了volatile的原理。详见:两个月自研和非外包入坑的创业公司,居然让我明白了volatile这篇文章会讲java并发契约下CAS相关的原子操作,以及Java8如何改进并优化CAS操作的性能。因为Atomic系列的原子类在并发编程、JDK源码或者各种开源项目中经常用到。而且在Java并发面试中,这个也是比较高频的考点,所以很值得大家聊一聊。2、场景介绍,突出问题。好吧,让我们开始吧!假设多个线程需要不断地给一个变量加1,比如下面的代码:其实上面的代码是不行的,因为多个线程直接并发地通过这种方式修改一个数据变量,是一个线程会不安全的行为导致数据值的变化不按预期值变化。比如20个线程分别对data进行data++操作,我们以为data的最终值会变成20,其实不是。最后数据的值可能是18或者19,这是有可能的,因为在多线程并发操作下,会存在这样的安全问题,导致数据结果不准确。至于为什么会不准确?那不在本文的讨论范围之内,因为这个一般只要学过java的同学肯定了解多线程并发的问题。3、初步解决方案:synchronized因此,对于上面的代码,一般我们会通过加锁的方式对其进行修改,使其成为线程安全的:这时候代码是线程安全的,因为我们加了synchronized,也就是让每个线程尝试在进入increment()方法之前锁定。同一时间只能有一个线程加锁,其他线程需要等待加锁。通过这个过程,可以保证数据每变化一次就加1,不会出现数据混乱的问题。老规矩!我们看下图感受下synchronizedlocking的效果和氛围,相当于N个线程一个一个排队更新值。然而,这么简单的data++操作,却需要重度的synchronized锁来解决多线程并发的问题,有点大材小用了。虽然做了很多优化来与Java版本的更新同步,但是处理这种简单的累加操作还是显得“太重”了。人们同步可以解决更复杂的并发编程场景和问题。而且,这种场景下,如果使用synchronized,不就相当于把每个线程都序列化了吗?一个一个排队,加锁,处理数据,释放锁,下一个进来。4.更高效的解决方案:Atomic原子类及其底层原理对于这个简单的data++类的操作,我们其实可以换一种方式来实现。java并发下提供了一系列Atomic原子类,比如AtomicInteger。可以在多线程并发安全的情况下保证一个值的高性能并发更新。我们来看看下面的代码:看看上面的代码,是不是很简单!多个线程可以并发执行AtomicInteger的incrementAndGet()方法,也就是帮我把data的值加1,累加后返回最新的值。在这段代码中,你看不到锁和释放锁!其实Atomic原子类底层并没有使用传统的锁机制,而是使用了无锁的CAS机制。CAS机制保证了多线程修改一个值的安全性。那么什么是CAS?他的全称是:CompareandSet,意思是先比较再设置。话不多说,先上图吧!让我们看看上图。假设有3个线程想要同时修改AtomicInteger的值。它们的底层机制如下:首先,每个线程都会先获取当前值,然后进行原子CAS操作。意味着这个CAS操作必须完全由自己执行,不会被别人打断。然后在CAS操作中,会对比说,唉!大哥!你现在的值是我刚才得到的值吗?如果是,宾果游戏!说明这个值没有人改过,所以可以设置成累加1后的值!同样,如果有人在执行CAS时发现之前得到的值与当前值不一样,也会导致CAS失败。失败后会进入死循环,重新获取值,然后执行CAS操作!好的!现在我们对照上图看一下整个过程:首先假设线程点击结束,然后对AtomicInteger执行incrementAndGet()操作,底层会先获取AtomicInteger的当前值,this的值为0,此时没有其他线程可以和他竞争!他不管那么多,直接执行原子CAS操作,问人家:大哥,你的值现在还是0吗?如果是,说明没有人修改过!太好了,加我1,设置为1。所以AtomicInteger的值变成了1!然后线程2和线程3同时跑过来了,因为底层不是基于锁机制,而是无锁的CAS机制,所以它们可能并发执行incrementAndGet()操作。然后他们都拿到了AtomicInteger的当前值,也就是1然后线程2先发起了原子CAS操作!注意CAS是原子的,此时只有一个线程在执行!然后线程2问:兄弟,你的值现在还是1吗?如果是,很好,说明没有人改,我改成2,AtomicInteger的值也改成2了。关键点来了:现在线程3发起了CAS操作,但是他还持有之前得到的1!这个时候线程3会问:兄弟,你的值现在还是1吗?坏消息来了!!!此时的值为2!线程3哭了,他说这期间居然有人改了这个值。算了,那我还是再取值吧,于是取了最新的值,取值为2。然后再次发起CAS操作,问一下,现在取值是2了吗?是的!太好了,没人改,我赶紧改了,AtomicInteger的值改成了3!上述整个过程就是所谓Atomic原子类的原理。不是基于锁机制序列化,而是基于CAS机制:先获取一个值,然后发起CAS,比较这个值是否被人改过?如果不是,只需更改值!这个CAS是原子的,别人不会打扰你的!通过这种机制,不需要加锁这种重量级的机制,也可以轻量级的实现对某个值的多个线程安全的并发修改。5、Java8优化了CAS机制但是这个CAS有什么问题吗?必须有。比如大量线程并发修改一个AtomicInteger,可能会有很多线程不断自旋,进入无限循环。这些线程不停的获取值,然后发起CAS操作,但是发现这个值被别人改了,于是又进入下一个循环,获取值,发起CAS操作又失败,进入下一个循环再次。当大量线程并发更新AtomicInteger时,这个问题可能会更加明显,导致大量线程空循环,自转,性能和效率都不是特别好。于是,当当当当当,Java8推出了一个新类LongAdder,它正在尝试使用分段CAS和自动分段迁移来大幅提升多线程高并发执行CAS操作的性能!在LongAdder的底层实现中,首先有一个基值。一开始,多线程不停地累加值,都是基于base的累加。比如一开始累加就变成base=5。然后,如果发现并发更新线程数过多,就会实现分段CAS机制,即在内部构建一个Cell数组,每个数组是一个数值段。这个时候让大量线程对不同Cell内部的value值进行CAS累加操作,让CAS计算压力分散到不同的Cell段值上!这样可以大大减少多线程并发更新同一个值时出现的死循环问题,大大提高多线程并发更新值的性能和效率!并且其内部实现了自动段迁移的机制,即如果某个cell的值无法进行CAS,则会自动寻找另一个cell段中的值进行CAS操作。这样也解决了线程白纺,一直纺等待执行CAS操作的问题。当一个线程来执行CAS操作时,可以尽快完成操作。最后,如果你想从LongAdder中得到当前累计的总值,它会将基值和所有Cell段值相加返回给你。6.总结&思考不知道大家有没有发现这种高并发访问下的分段处理机制,很多地方都有类似的思路!因为高并发下的分段处理机制其实是一种非常普遍和常用的并发优化方法。在我们之前关于分布式锁的文章中:你知道公司为什么规定所有接口必须加分布式锁吗?,还使用了一套段锁和自动段迁移/合并锁的机制,将分布式锁的并发性能大幅提升数十倍。所以其实很多技术和想法都是大同小异的。
