一个经典问题,i初始值为0,开启10个线程,每个线程对i循环1000次进行++操作,结果却小于等于10000。
出现这种问题的原因是,当多个线程对同一个共享变量进行操作时,由于操作系统底层高速缓存的存在,会出现缓存不一致,即一个线程对变量操作后,没有立即同步到主存,其他线程从自己的工作缓存取值进行操作,就会导致最终结果小于等于预期值。
可以使用 synchronized关键字,每次只有一个线程获取锁执行i++操作,但是这种相当于串行,性能较低。
juc并发包下的原子类
每次执行结果都是10000。
atomic包下的原子类,底层使用了Unsafe类,其内存语义是,对变量的++操作是原子性的,不可分割的,且value是volatile修饰的,实现一个线程对变量++操作后,会将其他线程的工作内存的共享变量的缓存行失效,且立即同步到主存,其他线程再取值就会从主存去取了。需要注意的是,volatile本身并不能实现原子性。
atomic底层使用了CAS操作,比较并替换,类似乐观锁机制,每次先查询,然后更新的时候会比较当前值是不是自己当初查询的那个值,如果不是,就再次查询,再次更新。
atomic原子类出现的问题
1.ABA问题
初始值是0,线程1先查询,值为0,然后CPU上下文切换,线程2这时也查询值为0,更新为1,然后又更新为初始值0,此时线程1再CAS更新,先查询发现值等于当初自己查询的值0,所以就更新成功了,但是其实中间已经被线程1更新过一次了。
对于ABA问题,可以使用AtomicStampedReference类来解决,每次修改记录一个版本号。
2.并发量高的情况下会影响性能
因为CAS操作每次都是先查询再更新,如果并发量比较高,可能会导致某个线程一直CAS失败,就一直重试,从而影响性能。
LongAdder类
类似于ConcurrentHashMap类的思想,分段加锁,有一个base初始值,如果并发量不高的情况下,直接对base进行CAS操作,如果失败,会初始化一个Cell数组,之后每个线程维护了一个本地变量probe,与Cell数组的大小进行哈希取模,这样一个线程就映射到一个元素,对数组内元素CAS操作失败的话,可以重新对probe赋值,如果还是失败,会对Cell数组扩容,每次扩容为原来的2倍大小。
最后需要对base值和Cell数组的每个元素相加,得到最终结果。
注意,这里base和Cell数组都是volatile修饰的,也就是一旦一个线程对其修改后,对其他线程立即可见。