学习《Java并发编程之美》第4章后,简要记录一下几个原子操作类的语法和原理。
各种原子操作类内部通过Unsafe使用CAS实现,保证多线程变量的安全性,相比使用锁实现原子性操作,在性能上有很大提高。常用类有AtomicLong、AtomicInteger、AtomicBoolean等
一、AtomicLong
1.初始化
public AtomicLong(long initialValue);若不设值,默认为0
2.自增自减
先增减后返回:incrementAndGet()和decrementAndGet()
先返回值后增减:getAndIncrement()和getAndDecrement()
3.增量超过1的,减法将参数变为负数即可
public final long addAndGet(long delta)
public final long getAndAdd(long delta)
4.乘除
atomicLong.set(atomicLong.get()*6);
看起来不像是原子操作,调用get()前的一瞬间被改动的话,会覆盖掉其他线程的数值
5.其他的几个类例如AtomicInteger、AtomicBoolean使用方法都差不多
二、LongAdder
AtomicLong通过CAS提供的非阻塞的原子性操作,但是大量线程去争抢更新同一个原子变量,只有一个线程成功,其他线程会无限循环不断进行自旋尝试,浪费CPU资源。
JDK8新增的原子性递增或者递减类LongAdder类就是处理这个问题的,将一个原子变量分解成多个变量,线程获取其中一个即可增减,减少争夺共享资源的并发量。
1.LongAdder的结构
Cell数组就是分解后的多个变量,供线程操作;
base相当于原来的原子变量,初始为0,由Cell数组累加而成;
cellsBusy用来实现自旋锁,状态值为0或1;
2.当前线程会访问数组哪个变量?
源码中有as[getProbe() & m]获取下标,m=数组长度-1,getProbe()用于获取当前线程中变量threadLocalRandomProbe的值,这个值叫探针变量,不同线程不同;
3.如何初始化Cell数组?
并发量较少时,Cell数组没有初始化,只调用casBase函数对base变量进行CAS累加;
并发量逐渐增多时,casBase函数会失败,如果Cell数组为null或者为空,调用longAccumlate函数进行初始化;
4.如何保证被分配到的Cell元素的原子性?
用@sun.misc.Contended注解修饰变量避免伪共享
(1)什么是伪共享?
高速缓冲存储器是为了解决主内存和CPU运行速度差的问题而存在的中介桥梁,一般被继承道CPU内部,也叫CPU Cache。内部按行存储,行是Cache与主内存进行数据交换的单位,大小为2的幂次方字节。
同一行有多个变量的话,线程会先找缓存再找主内存。例如缓存行存有x和y两个变量,线程A和B分别从主内存拿到x和y到自己的缓存区,A对x做出修改,y没变。B只需要y但是因为x被改动了,B要重新去主存读取数据。对主存造成压力。
(2)如何避免伪共享?
一个缓存行只存一个变量,行内其他字节用其他变量填充;或者用@sun.misc.Contended注解修饰变量,自动填充;
5.常用方法
(1)long sum(); 计算LongAdder对象的值,base+Cell数组总和。计算过程没有对Cell数组加锁,期间可能有其他线程进行修改或者扩容,求和结果不精准;
(2)void reset(); base和Cell数组全部置为0;
(3)sumThenReset(); sum()+reset(),求和过程中置空;
(4)add(long x); 加一个值x,想要减值则将x设置为负数;