原子操作类之18罗汉增强
是什么
Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰
Atomic相关类
参考阿里巴巴操作手册
17.【参考】volatile解决多线程内存不可见问题对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题
说明:如果是count++操作,使用如下类实现:
AtomicInteger count = new AtomicInteger();
count.addAndGet(1);
如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)
再分类
基本类型原子类
- AtomicInteger:整型原子类
- AtomicBoolean:布尔型原子类
- AtomicLong:长整型原子类
常用API简介
public final int get() //获取当前的值
public final int getAndSet(int newValue) //获取当前的值,并设置新的值
public final int getAndIncrement() //获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue) //最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值
Case
CountDownLatch代替sleep
package com.bilibili.juc.atomic;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerDemo {
public static final int SIZE = 50;
public static void main(String[] args) throws InterruptedException {
MyNumber myNumber = new MyNumber();
CountDownLatch countDownLatch = new CountDownLatch(SIZE);
for (int i = 0; i < SIZE; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
myNumber.addPlusPlus();
}
} finally {
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
// 等待上面50个线程全部计算完成后,再去获得最终结果
// 暂停2秒(低级解决方法,实际工作中不可这样使用)
// try {
// TimeUnit.SECONDS.sleep(2);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t" + "result:" + myNumber.atomicInteger.get());
}
}
class MyNumber {
AtomicInteger atomicInteger = new AtomicInteger();
public void addPlusPlus() {
atomicInteger.getAndIncrement();
}
}
输出结果:
main result:50000
数组类型原子类
- AtomicIntegerArray:整型数组原子类
- AtomicLongrArray:长整型数组原子类
- AtomicReferenceArray:用类型数组原子类
常用API简介
public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值
Case
package com.bilibili.juc.atomic;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class AtomicIntegerArrayDemo {
public static void main(String[] args) {
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[5]);
// AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(5);
// AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(new int[]{1,2,3,4,5});
for (int i = 0; i < atomicIntegerArray.length(); i++) {
System.out.println(atomicIntegerArray.get(i));
}
int tmpInt = 0;
tmpInt = atomicIntegerArray.getAndSet(0, 1122);
System.out.println(tmpInt + "\t" + atomicIntegerArray.get(0));
tmpInt = atomicIntegerArray.getAndIncrement(0);
System.out.println(tmpInt + "\t" + atomicIntegerArray.get(0));
}
}
输出结果:
0
0
0
0
0
0 1122
1122 1123
引用类型原子类
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题
- 解决修改过几次
- AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来
- 解决是否修改过,它的定义就是将标记戳简化为true/false,类似于一次性筷子
Case
package com.bilibili.juc.atomic;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicMarkableReference;
public class AtomicMarkableReferenceDemo {
static AtomicMarkableReference atomicMarkableReference = new AtomicMarkableReference(100, false);
public static void main(String[] args) {
new Thread(() -> {
boolean marked = atomicMarkableReference.isMarked();
System.out.println(Thread.currentThread().getName() + "\t" + "默认标识:" + marked);
// 暂停1s,等待后面的t2线程和我拿到一样的默认marked标识,都是false
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicMarkableReference.compareAndSet(100, 1000, marked, !marked);
System.out.println(Thread.currentThread().getName() + "\t" + "线程CAS结果:" + result);
System.out.println(Thread.currentThread().getName() + "\t" + atomicMarkableReference.isMarked());
System.out.println(Thread.currentThread().getName() + "\t" + atomicMarkableReference.getReference());
}, "t1").start();
new Thread(() -> {
boolean marked = atomicMarkableReference.isMarked();
System.out.println(Thread.currentThread().getName() + "\t" + "默认标识:" + marked);
// 暂停2s
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicMarkableReference.compareAndSet(100, 2000, marked, !marked);
System.out.println(Thread.currentThread().getName() + "\t" + "线程CAS结果:" + result);
System.out.println(Thread.currentThread().getName() + "\t" + atomicMarkableReference.isMarked());
System.out.println(Thread.currentThread().getName() + "\t" + atomicMarkableReference.getReference());
}, "t2").start();
}
}
输出结果:
t1 默认标识:false
t2 默认标识:false
t1 线程CAS结果:true
t1 true
t1 1000
t2 线程CAS结果:false
t2 true
t2 1000
对象的属性修改原子类
- AtomicIntegerFieldUpdater:原子更新对象中int类型字段的值;基于反射的实用程序,可对指定类的指定volatile int字段进行原子更新
- AtomicLongFieldUpdater:原子更新对象中Long类型字段的值;基于反射的实用程序,可对指定类的指定volatile long字段进行原子更新
- AtomicReferenceFieldUpdater:原子更新对象中引用类型字段的值;基于反射的实用程序,可对指定类的指定volatile 引用字段进行原子更新
使用目的
以一种线程安全的方式操作非线程安全对象内的某些字段
使用要求
更新的对象属性必须使用public volatile修饰符
因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性
面试官问你:你在哪里使用了volatile
回答:在使用高并发JUC编程时,使用AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater类原子更新对象中int、long、引用类型字段的值,更新的对象属性使用了public volatile修饰符
Case
AtomicIntegerFieldUpdater使用案例
package com.bilibili.juc.atomic;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* 需求:10个线程每个线程累加1000(不使用synchronized,尝试使用AtomicIntegerFieldUpdater实现)
*/
public class AtomicIntegerFieldUpdaterDemo {
public static void main(String[] args) throws InterruptedException {
BankAccount bankAccount = new BankAccount();
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
bankAccount.transferMoney(bankAccount);
}
} finally {
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "\t" + "result: " + bankAccount.money);
}
}
class BankAccount {
public volatile int money = 0; //钱数
static final AtomicIntegerFieldUpdater<BankAccount> ATOMIC_INTEGER_FIELD_UPDATER = AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");
// 之前使用synchronized实现的方式
public synchronized void add() {
money++;
}
// 不使用synchronized,保证高性能原子性,局部微创小手术,使用AtomicIntegerFieldUpdater实现
public void transferMoney(BankAccount bankAccount) {
ATOMIC_INTEGER_FIELD_UPDATER.getAndIncrement(bankAccount);
}
}
输出结果:
main result: 10000
AtomicReferenceFieldUpdater案例演示
package com.bilibili.juc.atomic;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
/**
* 需求:多线程并发调用一个类的初始化方法,如果未被初始化过,将执行初始化操作,需要保证只能初始化一次,只有一个线程初始化成功
*/
public class AtomicReferenceFieldUpdaterDemo {
public static void main(String[] args) {
MyVar myVar = new MyVar();
for (int i = 0; i < 5; i++) {
new Thread(() -> myVar.init(myVar), String.valueOf(i)).start();
}
}
}
class MyVar {
public volatile Boolean isInit = Boolean.FALSE;
static final AtomicReferenceFieldUpdater<MyVar, Boolean> REFERENCE_FIELD_UPDATER = AtomicReferenceFieldUpdater.newUpdater(MyVar.class, Boolean.class, "isInit");
public void init(MyVar myVar) {
if (REFERENCE_FIELD_UPDATER.compareAndSet(myVar, Boolean.FALSE, Boolean.TRUE)) {
System.out.println(Thread.currentThread().getName() + "\t" + "--------------start init,need 2 seconds");
// 暂停2s,模拟初始化的业务操作
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t" + "--------------over init");
} else {
System.out.println(Thread.currentThread().getName() + "\t" + "--------------已经有线程进行初始化工作了。。。。。");
}
}
}
输出结果:
0 --------------已经有线程进行初始化工作了。。。。。
2 --------------已经有线程进行初始化工作了。。。。。
1 --------------start init,need 2 seconds
3 --------------已经有线程进行初始化工作了。。。。。
4 --------------已经有线程进行初始化工作了。。。。。
1 --------------over init
原子操作增强类原理深度解析
- DoubleAccumulator:一个或多个变量共同维护使用提供的函数更新的运行的double值
- DoubleAdder:一个或多个变量共同维持最初的零和double总和
- LongAccumulator:一个或多个变量共同维护使用提供的函数更新的运行的long值
- LongAdder:一个或多个变量共同维持最初的零和long总和
阿里要命面试题
- 热点商品点赞计数器,点赞数加加统计,不要求实时精确
- 一个很大的List,里面都是int类型,如何实现加加,说说思路
点赞计数器,看看性能
LongAdder
java doc文档解释:当多个线程更新用于收集统计信息但不用于细粒度同步控制的目的的公共和时,此类通常优于AtomicLong。在低更新争用下,这两个类具有相似的特征,但在高争用的情况下,这一类的预期吞吐量明显更高,但代价是空间消耗更高
常用API
Code
package com.bilibili.juc.atomic;
import java.util.concurrent.atomic.LongAdder;
public class LongAdderAPIDemo {
public static void main(String[] args) {
LongAdder longAdder = new LongAdder();
longAdder.increment();
longAdder.increment();
longAdder.increment();
System.out.println(longAdder.sum());
}
}
输出结果:
3
LongAccumulator
LongAdder只能用来计算加法,且从零开始计算。LongAccumulator提供了自定义函数的操作
// LongAccumulator的构造方法,LongBinaryOperator代表自定义函数
public LongAccumulator(LongBinaryOperator accumulatorFunction,
long identity) {
this.function = accumulatorFunction;
base = this.identity = identity;
}
// 函数式接口
@FunctionalInterface
public interface LongBinaryOperator {
long applyAsLong(long left, long right);
}
常用API
Code
package com.bilibili.juc.atomic;
import java.util.concurrent.atomic.LongAccumulator;
public class LongAccumulatorAPIDemo {
public static void main(String[] args) {
LongAccumulator longAccumulator = new LongAccumulator(Long::sum, 0);
longAccumulator.accumulate(1);
longAccumulator.accumulate(3);
System.out.println(longAccumulator.get());
}
}
输出结果:
4
LongAdder高性能对比Code演示
package com.bilibili.juc.atomic;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
/**
* @ClassName: AccumulatorCompareDemo
* @Description: 需求:50个线程,每个线程100w次,求总点赞树
* @Author: zhangjin
* @Date: 2023/12/15
*/
public class AccumulatorCompareDemo {
public static final int TIME = 100 * 10000;
public static final int THREAD_NUMBER = 50;
public static void main(String[] args) throws InterruptedException {
ClickNumber clickNumber = new ClickNumber();
long startTime;
long endTime;
CountDownLatch countDownLatch1 = new CountDownLatch(THREAD_NUMBER);
startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
try {
for (int j = 0; j < TIME; j++) {
clickNumber.clickBySynchronized();
}
} finally {
countDownLatch1.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch1.await();
endTime = System.currentTimeMillis();
System.out.println("clickBySynchronized方法耗时:" + (endTime - startTime) + "毫秒,clickBySynchronized方法结果:" + clickNumber.number);
CountDownLatch countDownLatch2 = new CountDownLatch(THREAD_NUMBER);
startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
try {
for (int j = 0; j < TIME; j++) {
clickNumber.clickByAtomicLong();
}
} finally {
countDownLatch2.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch2.await();
endTime = System.currentTimeMillis();
System.out.println("clickByAtomicLong方法耗时:" + (endTime - startTime) + "毫秒,clickByAtomicLong方法结果:" + clickNumber.atomicLong.get());
CountDownLatch countDownLatch3 = new CountDownLatch(THREAD_NUMBER);
startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
try {
for (int j = 0; j < TIME; j++) {
clickNumber.clickByLongAdder();
}
} finally {
countDownLatch3.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
System.out.println("clickByLongAdder方法耗时:" + (endTime - startTime) + "毫秒,clickByLongAdder方法结果:" + clickNumber.longAdder.sum());
CountDownLatch countDownLatch4 = new CountDownLatch(THREAD_NUMBER);
startTime = System.currentTimeMillis();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
try {
for (int j = 0; j < TIME; j++) {
clickNumber.clickByLongAccumulator();
}
} finally {
countDownLatch4.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch4.await();
endTime = System.currentTimeMillis();
System.out.println("clickByLongAccumulator方法耗时:" + (endTime - startTime) + "毫秒,clickByLongAccumulator方法结果:" + clickNumber.longAccumulator.get());
}
}
class ClickNumber {
int number = 0;
public synchronized void clickBySynchronized() {
number++;
}
AtomicLong atomicLong = new AtomicLong(0);
public void clickByAtomicLong() {
atomicLong.getAndIncrement();
}
LongAdder longAdder = new LongAdder();
public void clickByLongAdder() {
longAdder.increment();
}
LongAccumulator longAccumulator = new LongAccumulator(Long::sum, 0);
public void clickByLongAccumulator() {
longAccumulator.accumulate(1);
}
}
输出结果:
clickBySynchronized方法耗时:3060毫秒,clickBySynchronized方法结果:50000000
clickByAtomicLong方法耗时:554毫秒,clickByAtomicLong方法结果:50000000
clickByLongAdder方法耗时:111毫秒,clickByLongAdder方法结果:50000000
clickByLongAccumulator方法耗时:100毫秒,clickByLongAccumulator方法结果:50000000
源码、原理分析
架构
ps:LongAdder类继承Striped64类,Striped64类继承Number类,之前的16个加上Striped64类和Number类即18罗汉
原理(LongAdder为什么这么快)
官网说明和阿里要求
官方java doc文档解释:当多个线程更新用于收集统计信息但不用于细粒度同步控制的目的的公共和时,此类通常优于AtomicLong。在低更新争用下,这两个类具有相似的特征,但在高争用的情况下,这一类的预期吞吐量明显更高,但代价是空间消耗更高
阿里Java开发手册:
17.【参考】volatile解决多线程内存不可见问题对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题
说明:如果是count++操作,使用如下类实现:
AtomicInteger count = new AtomicInteger();
count.addAndGet(1);
如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)
○如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)
LongAdder的父类Striped64类
Striped64类有几个比较重要的成员函数
/** CPU数量,即cells数组的最大长度 */
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* cells数组,为2的幂,2、4、8、16...,方便以后位运算——重要
*/
transient volatile Cell[] cells;
/**
* 基础value值,当并发较低时,只累加该值主要用于没有竞争的情况,通过CAS更新——重要
*/
transient volatile long base;
/**
* 创建或扩容Cells数组时使用的自旋锁变量调整单元格大小(扩容),创建单元格时使用的锁
*/
transient volatile int cellsBusy;
STRIPED64类中一些变量或者方法的定义
base:类似于AtomicLong中全局的value值。在没有竞争情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
collide:表示扩容意向,false一定不会扩容,true可能会扩容
cellsBusy:初始化cells或者扩容cells需要获取锁,0:表示无锁状态,1:表示其他线程已经持有了锁
casCellsBusy():通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
NCPU:当前计算机CPU数量,Cell数组扩容时会使用到
getProbe():获取当前线程的hash值
advanceProbe():重置当前线程的hash值
Cell类是java.util.concurrent.atomic下Striped64的一个内部类
LongAdder为什么这么快
AtomicLong的弊端
AtomicLong的底层原理就是CAS,CAS是轻量级的,不加锁,假设有一个共享变量A,外面存在很多线程争抢这个A,按照CPU底层Unsafe类原语级别,同一时间段,有且只有一个线程能够获取这个共享变量A,其它线程都在自旋。此时如果是线程少,数据量小的前提,没问题!!!但是涉及高并发、大数据,自旋的线程数量就会特别多,系统的CPU就会增加,给整体的系统性能造成负担
例如:日常生活中,春节,大家都需要买火车票回家,但是整个北京市只有北京火车南站一个网点在售卖火车票,同一个售卖窗口只能一个人在买票,其他人只能等待、排队
解决思路:化整为零 ——> 分散热点!!!
LongAdder的解决思路
LongAdder的基本思路就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多,如果要获取真正的long值,只要将各个槽中的变量值累加返回
sum()方法会将所有的Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点
数学表达【总结】
内部有一个base变量,一个Cell[]数组
- base变量:低并发,直接累加到该变量上
- Cell[]数组:高并发,累加进各个线程自己的槽Cell[i]中
源码解读深度分析
LongAdder原理小总结
LongAdder在无竞争的情况下,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零分散热点的做法,用空间换时间,用一个数组cells,将一个value值拆分进这个数组cells。多个线程需要同时对value进行操作的时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果
longAdder.increment()源码分析
add()
public void add(long x) {
// as表示cells数组的引用
// b表示获取的base属性值
// v表示当前线程hash到Cell中存储的值,也表示CAS的期望值
// m表示cells数组的长度减1
// a表示当前线程hash到的cell单元格
Cell[] as; long b, v; int m; Cell a;
// 首次首线程(as = cells) != null一定是false,此时走casBase()方法,以CAS的方式更新base值,且只有当CAS失败时,才会走到if中
// 条件一:cells数组不为空
// 条件二:CAS操作base失败,说明其它线程先一步修改了base,出现了竞争
if ((as = cells) != null || !casBase(b = base, b + x)) {
// true:无竞争,false:竞争激烈,多个线程hash到同一个Cell,可能要扩容
boolean uncontended = true;
// 条件一:cells数组为空
// 条件二:应该不会出现
// 条件三:当前线程所在的Cell为空,说明当前线程还没有更新过Cell,应该初始化一个Cell
// 条件四:更新当前线程所在的Cell失败,说明现在竞争很激烈,多个线程hash到了同一个Cell,应该扩容
if (as == null || (m = as.length - 1) < 0 ||
// getProbe()方法返回的是线程中的ThreadLocalRandomProbe字段,它是通过随机数生成的一个值,对于一个确定的线程这个值是固定的(除非刻意修改它)
// as[getProbe() & m]表示定位到cells数组中某一个Cell
(a = as[getProbe() & m]) == null ||
// a.cas(v = a.value, v + x)表示对a这个Cell中的数据执行cas操作+x
// a.cas(v = a.value, v + x)等于false代表高并发导致现有的Cell顶不住了
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
小总结
- 如果Cells表为空,尝试用CAS更新base字段,成功则退出
- 如果Cells表为空,CAS更新base字段失败,出现竞争,uncontended为true,调用longAccumulate(新建数组)
- 如果Cells表非空,但当前线程映射的槽为空,uncontended为true,调用longAccumulate(初始化)
- 如果Cells表非空,且当前线程映射的槽非空,CAS更新Cell的值,成功则返回,否则,uncontended设为false,调用longAccumulate(扩容)
longAccumulate()
final void longAccumulate(long x, LongBinaryOperator fn,
boolean wasUncontended) {
// 存储线程的probe值
int h;
// 如果getProbe()方法返回0,说明随机数未初始化
if ((h = getProbe()) == 0) {
// 使用ThreadLocalRandom为当前线程重新计算一个hash值,强制初始化
ThreadLocalRandom.current(); // force initialization
// 重新获取probe值,hash值被重置就好比一个全新的线程一样,所以设置了wasUncontended竞争状态为true
h = getProbe();
// 重新计算了当前线程的hash值后认为此次不算一次竞争,都未初始化,肯定还不存在竞争激烈,wasUncontended竞争状态为true
wasUncontended = true;
}
// 如果hash取模映射得到的Cell单元不是null,则为true,此值也可以看作是扩容意向
boolean collide = false; // True if last slot nonempty
for (;;) {
Cell[] as; Cell a; int n; long v;
// CASE1:cells数组已经被初始化了
if ((as = cells) != null && (n = as.length) > 0) {
// 1.当前线程的hash值运算后映射得到的Cell单元为null,说明该Cell没有被使用
if ((a = as[(n - 1) & h]) == null) {
// cells数组没有正在扩容
if (cellsBusy == 0) { // Try to attach new Cell
// 新建一个Cell单元
Cell r = new Cell(x); // Optimistically create
// 尝试加锁,成功后cellsBusy == 1
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
try { // Recheck under lock
Cell[] rs; int m, j;
// 在有锁得情况下再检测一边之前的判断
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
// 将Cell单元r附到cells数组上
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
// 2.wasUncontended表示cells数组初始化后,当前线程竞争修改失败
// wasUncontended = false,这里只是重置设置了这个值为true,紧接着执行advanceProbe(h)重置当前线程的prob,重新循环
// 当a.cas(v = a.value, v + x)等于false时,wasUncontended = false,代表当前线程在之前那个Cell执行CAS操作竞争失败,这里允许当前线程重置hash值,重置获取新的Cell进行竞争
else if (!wasUncontended) // CAS already known to fail
wasUncontended = true; // Continue after rehash
// 3.前面1、2都不符合,说明当前线程的hash值运算后映射得到的Cell单元不为null,也重置过hash值
// 这时通过CAS操作尝试对当前线程对应的Cell单元中的value值进行累加x操作,x默认为1,如果CAS成功则直接跳出循环
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
// 4.如果n大于CPU最大数量,不可扩容,并通过下面h = advanceProbe(h)方法修改线程的probe再重新循环
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
// 5.如果扩容意向collide是false则修改它为true,并通过下面h = advanceProbe(h)方法修改线程的probe再重新循环
// 如果当前数据的长度已经大于了CPU核数,就会再次设置扩容意向collide = false(见4)
else if (!collide)
collide = true;
// 6.以上条件都不满足,且尝试加锁并成功
else if (cellsBusy == 0 && casCellsBusy()) {
try {
// 当前的cells数组和最先赋值的as是同一个,代表没有被其它线程扩容过
if (cells == as) { // Expand table unless stale
// 按位左移1位来操作,扩容大小为之前容量的两倍
Cell[] rs = new Cell[n << 1];
// 扩容后再将之前数组的元素拷贝到新数组中
for (int i = 0; i < n; ++i)
rs[i] = as[i];
cells = rs;
}
} finally {
// 释放锁设置cellsBusy = 0
cellsBusy = 0;
}
// 设置扩容状态
collide = false;
// 继续循环
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
// CASE2:cells数组没有加锁且未初始化,则尝试对它进行加锁,并初始化cells数组(首次新建)
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
// 初始化rs数组长度为2
Cell[] rs = new Cell[2];
// 给rs数组中(h & 1)计算的结果对应的Cell设置为1
rs[h & 1] = new Cell(x);
// 将cells指向rs数组引用
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
// CASE3:cells数组正在进行初始化,则尝试直接在基数base上进行累加操作
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
}
}
longAccumulate()方法入参说明
- long x:需要增加的值,一般默认都是1
- LongBinaryOperator fn:默认传递的是null
- boolean wasUncontended:竞争标识,如果是false则代表有竞争,只有cells初始化之后,并且当前线程CAS竞争修改失败,才会是false
步骤
线程hash值:probe
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long BASE;
private static final long CELLSBUSY;
private static final long PROBE;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> sk = Striped64.class;
BASE = UNSAFE.objectFieldOffset
(sk.getDeclaredField("base"));
CELLSBUSY = UNSAFE.objectFieldOffset
(sk.getDeclaredField("cellsBusy"));
Class<?> tk = Thread.class;
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
} catch (Exception e) {
throw new Error(e);
}
}
/**
* Returns the probe value for the current thread.
* Duplicated from ThreadLocalRandom because of packaging restrictions.
*/
static final int getProbe() {
return UNSAFE.getInt(Thread.currentThread(), PROBE);
}
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
CASE2:未初始化过Cell[]数组,尝试占有锁并首次初始化cells数组
如果上面条件都执行成功就会执行数组的初始化及赋值操作, Cell rs = new Cell[2]表示数组的长度为2
rs[h & 1]= new Cell(x)表示创建一个新的Cell元素,value是x值,默认为1
h & 1类似于我们之前HashMap常用到的计算散列桶index的算法,通常都是hash & (table.en -1)。同hashmap-个意思
CASE3:兜底,多个线程尝试CAS修改失败的线程会走到这个分支
该分支实现直接操作base基数,将值累加到base上,也即其它线程正在初始化cells数组,多个线程正在更新base的值
CASE1:cells数组不再为空且可能存在cells数组扩容
具体分为上面代码注释的1、2、3、4、5、6,6个步骤
上6步骤总结
sum()
sum()会将Cell数组中的所有value和base累加作为返回值。核心思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
/**
* 返回当前和。返回值是一个原子快照;在没有并发更新的情况下调用会返回一个准确的结果,但是在计算总和时发生的并发更新可能不会被合并。
*/
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
为什么在并发情况下sum的值不精确?
sum()方法执行时,并没有限制对base和cells的更新,所以LongAdder不是强一致性的,它是最终一致性的
首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了,而此时局部变量sum不会更新,造成不一致。其次,这里对cell的读取无法保证是最后一次写入的值,所以,sum()方法在没有并发的场景下,可以获得正确的结果。
使用总结
AtomicLong
- 线程安全,可允许一些性能损耗,要求高精度时,可使用
- 保证精度,性能代价
- AtomicLong是多个线程对单个热点值value进行原子操作
LongAdder
- 当需要在高并发场景下有较好的性能表现,且对值得精确度要求不高时,可使用
- 保证性能,精度代价
- LongAdder时每个线程拥有自己的槽,各个线程一般只对自己槽中的值进行CAS操作
总结
AtomicLong
- 原理:CAS+自旋
- 场景:低并发下的全局计算,AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性问题
- 缺陷:高并发后性能急剧下降——AtomicLong的自旋会成为瓶颈(N个线程CAS操作修改线程的值,每次只有一个成功过,其他N-1失败,失败的不停自旋直至成功,这样大量失败自旋的情况,一下子cpu就打高了)
LongAdder
- 原理:CAS+Base+Cell数组分散——空间换时间并分散了热点数据
- 场景:高并发下的全局计算
- 缺陷:sum求和后还有计算线程修改结果的话,最后结果不够准确