第四章 Java并发包中原子操作类原理剖析
第四章 Java并发包中原子操作类原理剖析
JUC包提供一系列的原子性操作类,这些类都是使用非阻塞算法CAS实现的,比使用锁实现原子性操作在性能上有很大提升。
1. 原子变量操作类AtomicLong
AtomicLong
是原子性递增递减类,其内部使用Unsafe来实现
public class AtomicLong extends Number implements java.io.Serializable{
//获取Unsafe类实例 setup to use Unsafe.compareAndSwapLong for updates
private static final Unsafe unsafe=Unsafe.getUnsafe();
private static final valueOffset;
//声明为volatile,在多线程下保持内存可见性,value是具体存放计数的变量,对其进行更新操作都通过CAS进行
private volatile long value;
static{
try{
valueOffset=unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value"));
}catche(Exception e){
throw new Error(e);
}
}
public AtomicLong(long initialValue){
value=initialValue;
}
//初始化值value为0
public AtomicLong(){
}
public final long get(){
return value;
}
//通过volatile关键字可以保证内存可见性,这种单纯更新操作(写入操作不依赖当前值)能够保证原子性
//但是写入操作依赖当前值(“ 读取--计算---写入”) 则无法保证原子性,必须通过Unsafe的CAS操作才能保持原子性
public final void set(long newValue){
value=newValue;
}
}
1.1 递增递减操作
递增抵减操作都不是原子操作,是“读取—计算—写入”三个步骤,写入操作依赖当前值,仅通过volatile关键字无法保证原子性,只能保证内存可见性,通过Unsafe类提供的CAS操作可以保证
//如果当前值等于expect,则设定当前值为update,并返回true,否则返回false
public final boolean compareAndSet(long expect,long update){
return unsafe.compareAndSwapLong(this,valueOffset,expect,update);
}
public final boolean weakCompareAndSet(long expect,long update){
return unsafe.compareAndSwapLong(this,valueOffset,expect,update);
}
//调用unsafe方法,原子性设置value值为原始值+1,返回值为原始值
public final long getAndIncrement(){
return unsafe.getAndAddLong(this,valueOffset,1L);
}
//调用unsafe方法,原子性设置value值为原始值-1,返回值为原始值
public final long getAndDecrement(){
return unsafe.getAndAddLong(this,valueOffset,-1L);
}
//调用unsafe方法,原子性设置value值为原始值+1,返回值为递增后的值
public final long incrementAndGet(){
return unsafe.getAndAddLong(this,valueOffset,1L)+1L;
}
//调用unsafe方法,原子性设置value值为原始值-1,返回值为递减后的值
public final long decrementAndGet(){
return unsafe.getAndAddLong(this,valueOffset,-1L)-1L;
}
//设定当前值为给定值并返回旧值,相当于 “读取---写入”操作
public final long getAndSet(long newValue){
return unsafe.getAndSetLong(this,valueOffset,newValue);
}
public final long getAndAdd(long delta){
return unsafe.getAndAddLong(this,valueOffset,delta);
}
public final long addAndGet(long delta){
return unsafe.getAndAddLong(this,valueOffset,delta)+delta;
}
//一元运算符操作更新,返回原始值 JDK1.8
public final long getAndUpdate(LongUnaryOperator updateFunction){
long prev,next;
do{
prev=get();
next=updateFunction.applyAsLong(prev);
}while(!compareAndSet(prev,next));
return prev;
}
//一元运算符操作更新,返回更新后的值 JDK1.8
public final long updateAndGet(LongUnaryOperator updateFunction){
long prev,next;
do{
prev=get();
next=updateFunction.applyAsLong(prev);
}while(!compareAndSet(prev,next));
return next;
}
//二元运算符操作更新,返回原始值 JDK1.8
ppublic final long getAndAccumulate(long x,LongBinaryOperator accumulatorFunction){
long prev,next;
do{
pre=get();
next=accumulatorFunction.applyAsLong(prev,x);
}while(!compareAndSet(prev,next));
return prev;
}
//二元运算符操作更新,返回更新后的值 JDK1.8
ppublic final long accumulateAndGet(long x,LongBinaryOperator accumulatorFunction){
long prev,next;
do{
pre=get();
next=accumulatorFunction.applyAsLong(prev,x);
}while(!compareAndSet(prev,next));
return next;
}
JDK8中Unsafe类中的getAndAddLong方法
public final long getAndAddLong(Object obj,long offset,long gamma){
long l;
do{
l=getLongVolatile(obj,offset);
}while(!compareAndSwapLong(obj,offset,l,l+gamma));
return l;
}
public final native boolean compareAndSwapLong(Object var1,long var2,long var3,long var4);
下面通过一个多线程使用AtomicLong统计数组中0的个数的例子加深理解
package com.chapter3;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
/**
* @CreateTime: 2021-09-21 19:04
* @Description: 多线程使用AtomicLong统计0的个数
*/
public class AtomicLongTest {
//使用原子类自增和自减
private static AtomicLong atomicLong=new AtomicLong();
//使用普通多线程会统计错误值
private static long count;
private static final int SIZE=2<<15;
private static final int SEED=1;
private static Integer[] arrayOne=new Integer[SIZE];
private static Integer[] arrayTwo=new Integer[SIZE];
private static Integer[] arrayThree=new Integer[SIZE];
private static Random random=new Random();
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<SIZE;++i){
arrayOne[i]=random.nextInt(4);
arrayTwo[i]=random.nextInt(4);
arrayThree[i]=random.nextInt(4);
}
System.out.println(Arrays.toString(arrayOne));
System.out.println(Arrays.toString(arrayTwo));
System.out.println(Arrays.toString(arrayThree));
Thread t1=new Thread(()->{
for (int i=0;i<SIZE;++i){
if(arrayOne[i]==SEED) {
atomicLong.incrementAndGet();
++count;
}
}
});
Thread t2=new Thread(()->{
for (int i=0;i<SIZE;++i){
if(arrayTwo[i]==SEED){
atomicLong.incrementAndGet();
++count;
}
}
});
Thread t3=new Thread(()->{
for (int i=0;i<SIZE;++i){
if(arrayThree[i]==SEED){
atomicLong.incrementAndGet();
++count;
}
}
});
long startTime=System.currentTimeMillis();
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
long endTime=System.currentTimeMillis();
long spinsh=endTime-startTime;
System.out.println("count 0:"+atomicLong.get());
System.out.println("count 0:"+count); //随着数组大小不断加大,以及线程数量增加,统计结果出现不一致,印证了多线程下使用普通变量进行操作会造成脏数据或意想不到的后果,如果换成synchronized块会发现性能降低,耗时加长了
System.out.println("cost time :"+spinsh); //单纯 count 7ms atomicLong 13ms
}
}
2. JDK8新增原子操作类LongAdder
2.1 简单介绍
AtomicLong通过CAS提供的非阻塞原子性操作,相比使用阻塞算法的同步器来说性能提升了很多。但是其缺点是高并发下大量线程会同时竞争更新同一个原子变量,但是同时只有一个线程能更新成功,其他线程都是失败后自旋重试,浪费了CPU时间。
JDK8新增的LongAdder用来克服高并发下使用AtomicLong的缺点。解决思路是把一个变量分解成多个变量,让同样多的线程去竞争多个资源。
使用LongAdder时,在内部维护多个Cell变量,每个Cell里面有一个初始值为0的long型变量。带来的改变有三处:
- 在同等并发情况下,争夺单个变量更新操作的线程量减少,变相减少了争夺共享资源的并发量
- 多个线程在争夺Cell原子变量时如果失败了,并不是在当前Cell变量上一直自旋CAS重试,而是尝试在其他Cell变量上进行CAS尝试,增加了当前线程重试CAS成功的可能性
- 获取LongAdder当前值时,是把所有Cell变量的value值累加后在加上base返回
LongAdder维护一个延迟初始化原子性更新数组Cells和一个基值变量base。由于Cells占用内存较大,一开始并不创建它,按需创建,惰性加载。
一开始Cell为null且并发量较少,都是对基值变量base进行累加。保持Cell数组大小为2的N次方,初始化Cell数组时Cell数组元素个数为2,数组元素类型为Cell类型。Cell类型是AtomicLong类型的一个改进,用来减少缓存竞争,即解决伪共享问题。
对大多数孤立的多个原子操作进行字节填充是浪费的,因为原子性操作都是无序分散在内存中(即多个原子性变量的内存地址不连续),多个原子变量被放到同一个Cache行可能性很小。但是原子性数组元素内存地址是连续的,所以数组内的多个元素能经常共享Cache行,因此这里使用@sun.misc.Contended
注解对Cell类型进行字节填充,防止数组中多个元素共享一个Cache行,性能上是一个提升
2.2 LongAdder代码分析
Long+Add +后缀er
,类名可以看出是专门处理long型变量相加
的类
2.2.1 Cell类
Cell类是AtomicLong类的一个改进,体现在如下方面
- value变量volatile关键字修饰,保证内存可见性
- Cell类由注解
@sun.misc.Contended
修饰,解决伪共享 - cas函数通过CAS更新操作,保证当前线程更新时被分配的Cell元素中value值的原子性
@sun.misc.Contended
static final Class Cell{
volatile long value;
Cell(long x){value=x;}
final boolean cas(long cmp,long val){
return UNSAFE.compareAndSwapLong(this,valueOffset,cmp,val);
}
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static{
try{
UNSAFE=sun.misc.Unsafe.getUnsafe();
Class<?> ak=Cell.class;
valueOffset=UNSAFE.objectFieldOffset(ak.getDeclaredField("value"));
}catch(Exception e){
throw new Error(e);
}
}
}
2.2.2 Striped64类
在Striped64类内部维护三个变量:
base
变量,基础值volatile,默认为0,当线程量不多时,只对此变量进行操作cells
数组变量,惰性加载,延迟初始化,数组元素类型为Cell
类cellsBusy
变量用来实现自旋锁,状态值只有0或1 ,当创建Cell元素、扩容Cell数组、初始化Cell数组时,使用CAS操作该变量来保证同时只有一个线程可以进行其中之一的操作
2.2.3 LongAdder类
LongAdder
类继承自Striped64类.
long sum()
函数
返回当前的值,内部操作是累加所以Cell内部的value值后在加上base.由于计算总和时没有对Cell数组加锁,所以累计过程中可能有其他线程对Cell中的值做出修改,也可能对Cell数组扩容,因此sum函数返回的值并不是非常精确
,其返回值并不是一个调用sum方法时的原子快照值
。
public long sum(){
Cell[] as=cells;
Cell a;
long sum=base;
if(as!=null){
for(int i=0;i<as.length;i++){
a=as[i];
if(a!=null){
sum+=a.value;
}
}
}
return sum;
}
void reset()
重置函数
把base置0,如果Cell数组有元素,则元素值被重置为0
public void reset(){
Cell[] as=cells;
Cell a;
base=0L;
if(as!=null){
for(int i=0;i<as.length;i++){
a=as[i];
if(a!=null){
a.value=0L;
}
}
}
}
long sumThenReset()
是sum改造版本,累加后并重置。当多线程调用该方法会出问题,比如考虑第一个调用线程清空Cell值,则后一个线程调用累加的都是0值。
public long sumThenReset(){
Cell[] as=cells;
Cell a;
long sum=base;
base=0L;
if(as!=null){
for(int i=0;i<as.length;++i){
a=as[i];
if(a!=null){
sum+=a.value;
a.value=0L;
}
}
}
return sum;
}
long longValue()
等价于sum()void add(long x)
方法
public void add(long x){
Cell[] as;
long b,v;
int m;
Cell a;
//(1)
if((as=cells)!=null || !casBase(b=base,b+x)){
//是否非竞争 ,默认为是
boolean uncontended=true;
if(as==null || (m=as.length-1)<0 || //(2)
(a=as[getProbe() & m])==null || //(3)
!(uncontended=a.cas(v=a.value,v+x))){ //(4) 执行cas失败则说明当前线程对应的Cell元素的value执行cas失败,有其他线程也在使用该Cell元素,因此是竞争性的
longAccumulate(x,null,uncontended); //(5)
}
}
}
final boolean casBase(long cmp,long val){
return UNSAFE.compareAndSwapLong(this,BASE,cmp,val)
}
public void increment(){
add(1L);
}
public void decrement(){
add(-1L);
}
- 代码(1)首先看cells是否为null,如果为null,则在基础变量base上进行累加操作,这时候类似于AtomicLong的操作
如果cells不为null或cells为null但是在进行cas更新时失败了,则继续往下执行代码。 - 代码(2)(3)决定当前线程应该访问cells数组里面的哪个Cell元素,如果当前线程映射的元素Cell存在,则执行代码(4),使用CAS操作去更新分配的Cell元素的value值
如果当前线程映射的元素不存在或存在但执行CAS更新操作失败了,则执行代码(5) - 其实代码(2)(3)(4)合起来就是获取当前线程应该访问的cells数组的Cell元素,然后进行CAS更新操作,只是在此过程中有条件不满足则会跳转执行代码(5)。
当前线程应该访问cells数组的哪个Cell元素是通过getProbe()&m进行计算得到的,m=cells.length-1,getProbe()则是获取当前线程中变量threadLocalRandomProbe的值,这个值一开始是0,在代码(5)里面会进行初始化。
- 当前线程通过分配的Cell元素的cas函数来保证对Cell元素value值更新的原子性。
整个add函数内部逻辑流程图如下:
longAccumulate
函数
public void longAccumulate(long x,LongBinaryOperator fn,boolean wasUncontended){
//(6) 初始化当前线程的变量threadLocalRandomProbe的值
int h;
if((h=getProbe())==0){
ThreadLocalRandom.current(); //因为在此需要计算当前线程对应Cell元素在cells数组中的位置,所以强迫Thread类进行随机随机数种子初始化【惰性加载】
h=getProbe();
wasUncontended=true; //是否非竞争
}
boolean collide=false; //是否碰撞
for(;;){
Cell[] as;
Cell a;
int n;
long v;
if((as=cells)!=null && (n=as.length)>0){ //(7)
if((a=as[h&(n-1)])==null){ //(8)
if(cellsBusy==0){ // try to attach new Cell
Cell r=new Cell(x); // Optimistically create
if(cellsBusy==0 && casCellsBusy()){ //Recheck under lock
boolean created=false;
try{
Cell[] rs;
int m,j;
if((rs==cells)!=null && (m=rs.length)>0 && rs[j=h&(m-1)]==null){
rs[j]=r;
created=true;
}
}finally{
cellsBusy=0;
}
if(created){
break;
}
continue; //slot is now non-empty
}
}
collide=false;
}else if(!wasUncontended){ //CAS already known to fail
wasUncontended=true;
//(9)当前Cell元素存在,则执行cas操作
}else if(a.cas(v=a.value,(fn==null)?v+x:fn.applyAsLong(v,x))){
break;
//(10) 当前Cell数组元素个数大于CPU个数
}else if(n>=NCPU || cells!=as){
collide=false; //At max size or stale
//(11)是否有冲突
}else if(!collide){
collide=true;
//(12)如果当前元素个数没有达到CPU个数并且有冲突则扩容
}else if(cellsBusy==0 && casCellsBusy()){
try{
if(cells==as){
Cell[] rs=new Cell[n<<1];
for(int i=0;i<n;++i){
rs[i]=as[i];
}
cells=rs;
}
}finally{
cellsBusy=0;
}
collide=false;
continue;
}
//(13)为了能够找到一个空闲的Cell,重新计算hash值,xorshift算法生成随机数
h=advancedProbe(h);
//(14)初始化Cell数组
}else if(cellsBusy==0 && cells==as && casCellsBusy()){
boolean init=false;
try{
if(cells==as){
//(14.1)
Cell[] rs=new Cell[2];
//(14.2)
rs[h&1] = new Cell(x);
cells=rs;
init=true;
}
} finally{
//(14.3)
cellsBusy=0;
}
if(init){
break;
}
}else if(casBase(v=base,((fn==null)?v+x:fn.applyAsLong(v,x)))){
break; //Fall back on using base
}
}
}
cells数组初始化在代码(14)处进行,其中cellsBusy是一个标志,为0说明当前cells数组没有在被初始化或者扩容,也没有在新建Cell元素,为1说明cells数组在被初始化或扩容,或者当前在创建新的Cell元素、通过CAS操作来进行0或1状态的切换,这里使用cellsBusy函数。假设当前线程通过CAS设置cellsBusy为1,则当前线程开始初始化操作,那么这时候其他线程就无法进行扩容,如代码(14.1)初始化cells数组元素个数为2,代码(14.2)使用h&1计算当前线程应该访问cells数组的哪个位置【当前线程的threadLocalRandomProbe变量值&(cells数组元素个数-1)】,然后标志cells数组已经被初始化,最后代码(14.3)重置了cellsBusy标志,显然这里没有用到CAS操作,却是线程安全的,因为cellsBusy为volatile类型,保证变量内存可见性,另外此时其他地方的代码没有机会修改cellsBusy的值。在这里初始化的cells数组里面的两个元素值目前还是null。此处知道cells数组如何被初始化。
cells数组扩容是在代码(12)中进行的,对cells扩容是有条件的:
- 当前cells的元素个数小于当前及其CPU的个数 代码(10)
- 当前多个线程访问了cells中同一个元素,从而导致冲突使其中一个线程CAS失败 代码(11)
只有当每个CPU都运行一个线程时才会使多线程得到最佳,即当CPU数等于cells数组元素个数时,每个Cell都用一个CPU处理,这时性能才是最佳的。
代码(12)扩容也是先通过CAS设置cellsBusy为1,然后才进行扩容。假设CAS成功则将执行代码(12.1),容量扩充为之前2倍,并复制Cell元素到扩容数组。另外,扩容后cells数组里面除了包含复制过来的元素外,还包含其他新元素,这些元素的值目前还是null。
在代码(7)(8)中,当前线程调用add方法并根据当前线程的随机数threadLocalRandomProbe和cells元素个数计算要访问的Cell元素下标,然后如果发现对应下标元素值为null,则新增一个元素Cell到cells数组中,并且在将其添加到cells数组之前要竞争设置cellsBusy为1.
代码(13)对CAS失败的线程重新计算当前线程的随机数值threadLocalRandomProbe,以减少下次访问cells元素时的冲突机会。
通读以上可以回答以下几个问题:
- LongAdder类的结构
其继承Striped64
类,Striped64类内部维护三个变量:基值base
、Cell类的数组cells
、原子性变量cellsBusy
- 当前线程应该访问Cell数组的哪一个元素,或者说当前线程应该访问数组元素的下标如何计算得出
通过当前线程对象的变量threadLocalRandomProbe
&(cells.length-1)计算得出 - 如何初始化Cell数组
初始化数组大小为2,且当前线程所在cells数组的位置创建Cell元素
if(cellsBusy==0 && cells==as && casCellsBusy()){
boolean init=false;
try{
if(cells==as){
//(14.1)
Cell[] rs=new Cell[2];
//(14.2)
rs[h&1] = new Cell(x);
cells=rs;
init=true;
}
} finally{
//(14.3)
cellsBusy=0;
}
if(init){
break;
}
}
- Cell数组如何扩容
容量扩充为之前2倍,并复制Cell元素到扩容数组。另外,扩容后cells数组里面除了包含复制过来的元素外,还包含其他新元素,这些元素的值目前还是null。
//(12)如果当前元素个数没有达到CPU个数并且有冲突则扩容
if(cellsBusy==0 && casCellsBusy()){
try{
if(cells==as){
Cell[] rs=new Cell[n<<1];
for(int i=0;i<n;++i){
rs[i]=as[i];
}
cells=rs;
}
}finally{
cellsBusy=0;
}
collide=false;
continue;
}
- 线程访问分配的Cell元素有冲突如何处理
//(13)为了能够找到一个空闲的Cell,重新计算hash值,xorshift算法生成随机数
h=advancedProbe(h);
- 如何保证线程操作被分配的Cell元素的原子性
当前线程通过分配的Cell元素的cas函数来保证对Cell元素value值更新的原子性
3. 通用的LongAccumulator类
LongAdder类是LongAccumulator类的一个特例。
public longAccumulator(LongBinaryOperator accumulatorFunction,long identity){
this.function=accumulatorFunction;
base=this.identity=identity; //初始值
}
public interface LongBinaryOperator{
//根据两个参数计算并返回一个值
long applyAsLong(long left,long right);
}
调用LongAdder相当于使用以下方式调用LongAccumulator:
LongAdder adder=new LongAdder();
longAccumulator accumulator=new longAccumulator((long x,long y)-> x+y,0);
longAccumulator相比LongAdder:
- 可以为累加器提供非0的初始值,后者只能提供默认初始值0;
- LongAccumulator可以指定累加规则,比如不累加计算而是累乘计算,后者则内置累加计算