原子类位于java.util.concurrent.atomic
包下
-
AtomicBoolean
-
AtomicInteger
-
AtomicIntegerArray
-
AtomicIntegerFieldUpdater
-
AtomicLong
-
AtomicL ongArray
-
AtomicL ongFieldUpdater
-
AtomicMarkableReference
-
AtomicReference
-
AtomicReferenceArray
-
AtomicReferenceFieldUpdater
-
AtomicStampedReference
-
DoubleAccumulator
-
DoubleAdder
-
LongAccumulator
-
LongAdder
基本类型原子类
处理的数据类型不同,API 都相同
- AtomicInteger
- AtomicBoolean
- AtomicLong
构造器,以 AtomicInteger 为例,另外俩个也一样:
Constructor and Description |
---|
AtomicInteger() 创建一个新的AtomicInteger,初始值为 0 。 |
AtomicInteger(int initialValue) 用给定的初始值创建一个新的AtomicInteger。 |
常用的 API :
public final int get() | 获取当前值 |
public final int getAndSet(int new Value) | 获取当前值,并设置新值 |
public final int getAndIncrement() | 获取当前值,并自增 |
public final int getAndDecrement() | 获取当前值,并自减 |
public final int getAndAdd(int delta) | 获取当前值,并加上预期的值 |
public comapreAndSet(int expect,int update) | 如果内存中的数值等于预期值,则以原子的方式将该值设置为输入值(update) |
案例一:
创建 50 个线程,每个线程执行 1000 次自增+1操作,预计结果为 50000
public class AtomicTest01 {
private static final int SIZE = 50;
public static void main(String[] args) {
Number number = new Number();
for (int i = 1; i <= SIZE; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 1000; i1++) {
number.add();
}
}, String.valueOf(i
)).start();
}
// 获取最终值
System.out.println(Thread.currentThread().getName() + "\t" + number.atomicInteger.get());
}
}
class Number{
// 默认值为0
AtomicInteger atomicInteger = new AtomicInteger();
public void add() {
// 自增+1
atomicInteger.getAndIncrement();
}
}
输出结果:
main 47693
与我们预期的结果并不一样,造成这样的原因是: main主线程执行的太快,其他线程还没有运算完,main线程就获取计算结果。导致与预计结果不一样。
解决方法:
- 可以在主线程中 sleep ,等待其他线程计算完。但是这种方式很傻,并且在工作中如果这样用,那绝对是人人喊打。。。。。
- 使用 CountDownLatch 计数器
- 不是很了解的可以去看我博客:JUC基础
public class AtomicTest01 {
private static final int SIZE = 50;
public static void main(String[] args) {
// 创建 50 个线程,每个线程执行 1000 次自增操作
Number number = new Number();
// 创建 CountDownLatch 计数器
CountDownLatch latch = new CountDownLatch(SIZE);
for (int i = 1; i <= SIZE; i++) {
new Thread(() -> {
try {
for (int i1 = 0; i1 < 1000; i1++) {
number.add();
}
} finally {
// 每执行完一个线程就将计数器-1
latch.countDown();
}
}, String.valueOf(i
)).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取最终值
System.out.println(Thread.currentThread().getName() + "\t" + number.atomicInteger.get());
}
}
class Number{
// 默认值为0
AtomicInteger atomicInteger = new AtomicInteger();
public void add() {
// 自增+1
atomicInteger.getAndIncrement();
}
}
数组类型原子类
- AtomicIntegerArray
- AtomicLongArray
- AtomicRreferenceArray
构造器,以 AtomicIntegerArray 为例:
Constructor and Description |
---|
AtomicIntegerArray(int length) 创建给定长度的新 AtomicIntegerArray,所有元素最初为零。 |
AtomicIntegerArray(int[] array) 创建一个新的 AtomicIntegerArray,其长度与从给定数组复制的所有元素相同。 |
常用的 API :
int | get(int i) 获取索引为 i 的当前值。 |
int | addAndGet(int i, int delta) 将索引为 i 的元素加上指定值(delta) |
boolean | compareAndSet(int i, int expect, int update) 将索引为 i 的元素与预期值(expect)作比较,相等则更新为新值(update) |
int | get(int i) 获取索引为 i 的元素 |
int | getAndIncrement(int i) 获取索引为 i 的元素,并加1 |
int | getAndDecrement(int i) 获取索引为 i 的元素,并减1 |
public class AtomicArrayTest {
public static void main(String[] args) {
// 创建一个长度为5的原子数组,默认值为0
// AtomicIntegerArray integerArray = new AtomicIntegerArray(5);
// 指定创建的原子数组
AtomicIntegerArray integerArray = new AtomicIntegerArray(new int[]{1,2,3,4});
// 将索引为0的值加2
int num = integerArray.addAndGet(0, 2);
System.out.println(num);
// 将索引为0的元素与3作比较,相等更新值为5
boolean result = integerArray.compareAndSet(0, 3, 5);
System.out.println(result);
// 获取索引为3的元素
int num_3 = integerArray.get(3);
System.out.println(num_3);
// 将索引为 1 的元素增1
int andIncrement = integerArray.getAndIncrement(1);
System.out.println(andIncrement);
System.out.println(integerArray.toString());
}
}
引用类型原子类
- AtomicReference
- AtomicStampedReference
- AtomicMarkableReference
AtomicReference:在上一章已经讲过,它可以将我们指定的类型纳入原子类中。
AtomicStampedReference:带版本戳
以防CAS中的ABA问题(上一章讲过),可以解决多次修改
AtomicMarkableReference : 类似 AtomicStampedReference,使用标记记录内存值是否被修改,但是它使用的是 true、false 标记。只能使用一次,只能判断是否修改过。并不能记录修改几次。
案例演示:
/**
*
* Author: YZG
* Date: 2022/11/26 21:04
* Description:
*/
public class AtomicMarkableReferenceDemo {
public static void main(String[] args) {
AtomicMarkableReference<Object> markableReference = new AtomicMarkableReference<>(100,false);
new Thread(()->{
boolean marked = markableReference.isMarked();
System.out.println(Thread.currentThread().getName()+"\t"+"默认标识"+marked);
//暂停1秒钟线程,等待后面的T2线程和我拿到一样的模式flag标识,都是false
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
markableReference.compareAndSet(100, 1000, marked, !marked);
},"t1").start();
new Thread(()->{
boolean marked = markableReference.isMarked();
System.out.println(Thread.currentThread().getName()+"\t"+"默认标识"+marked);
//这里停2秒,让t1先修改,然后t2试着修改
try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
boolean t2Result = markableReference.compareAndSet(100, 1000, marked, !marked);
System.out.println(Thread.currentThread().getName()+"\t"+"t2线程result--"+t2Result);
System.out.println(Thread.currentThread().getName()+"\t"+markableReference.isMarked());
System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference());
},"t2").start();
}
}
输出结果:
t1 默认标识false
t2 默认标识false
t2 t2线程result--false
t2 true
t2 1000
Process finished with exit code 0
对象的属性修改原子类
- AtomicIntegerFieldUpdater :原子更新对象中int类型字段的值
- AtomicLongFieldUpdater:原子更新对象中Long类型字段的值
- AtomicReferenceFieldUpdater:原子更新引用类型字段的值
更加细粒度范围内的原子更新
使用目的:以一种线程安全带 方式操作非线程安全对象内的某些字段
比如:我们一个银行账户的类,希望只对money字段保证其原子性,其中银行名称,银行账号和姓名都属于公开信息。以前我们使用 synchronized 关键字锁住了整个对象,这种方式锁的范围过大,效率低。
因此我们可以使用 FieldUpdater 原子类进行操作,只保证对象中的某个字段的原子性。
使用要求
- 更新的对象属性必须使用public volatile修饰符
- 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法
newUpdater()
创建一个更新器,并且需要设置想要更新的类和属性。
AtomicIntegerFieldUpdater 案例演示:
创建10个线程,每个线程转账 1000,不使用 synchronized
public class AtomicIntegerFieldUpdaterTest {
public static void main(String[] args) throws InterruptedException {
Account account = new Account();
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
for (int i1 = 0; i1 < 1000; i1++) {
account.addByFieldUpdate();
}
} finally {
latch.countDown();
}
}, String.valueOf(i)).start();
}
latch.await();
System.out.println(Thread.currentThread().getName()+ "\t" + account.money);
}
}
class Account{
String bankName = "CCB";
// 更新的对象属性必须使用==public volatile==修饰符
public volatile int money ;
public void add() {
money++;
}
// 因为对象的属性修改类型原子类都是**抽象类**,所以每次使用都必须使用静态方法`newUpdater()`创建一个更新器,并且需要设置想要更新的类和属性。
AtomicIntegerFieldUpdater<Account> integerFieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(Account.class,"money");
public void addByFieldUpdate() {
integerFieldUpdater.getAndIncrement(this);
}
}
AtomicReferenceFieldUpdater 案例演示:
AtomicReferenceFieldUpdater 可以为指定字段类型保证原子性
//比如这个案例中是针对boolean类型的
class MyVar{
public volatile Boolean isInit = Boolean.FALSE;
AtomicReferenceFieldUpdater<MyVar,Boolean> referenceFieldUpdater =
AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");
public void init(MyVar myVar){
if(referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE)){
System.out.println(Thread.currentThread().getName()+"\t"+"-----start init,needs 3 seconds");
try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"\t"+"-----over init");
}else{
System.out.println(Thread.currentThread().getName()+"\t"+"抱歉,已经有其他线程进行了初始化");
}
}
}
public class AtomicReferenceFieldUpdaterDemo {
public static void main(String[] args) {
MyVar myVar = new MyVar();
for(int i = 1;i <= 5;i ++){
new Thread(()->{
myVar.init(myVar);
},String.valueOf(i)).start();
}
}
}
//1 -----start init,needs 3 seconds
//5 抱歉,已经有其他线程进行了初始化
//4 抱歉,已经有其他线程进行了初始化
//2 抱歉,已经有其他线程进行了初始化
//3 抱歉,已经有其他线程进行了初始化
//1 -----over init
原子操作增强类原理深度解析
- DoubleAccumulator、
- DoubleAdder
- LongAccumulator
- LongAdder
这四个类都是 jdk8 新增的。下面以LongAdder和LongAccumulator进行演示,DoubleAdder和 DoubleAccumulator 仅仅是操作类型不同
LongAdder
jdk 中的描述:
这个类是通常优选
AtomicLong
当多个线程更新时使用,用于诸如收集统计信息,不用于细粒度同步控制的共同总和。在低更新争议下,这两类具有相似的特征。但是,在高度争议的情况下,这一类的预期吞吐量明显高于牺牲更高的空间消耗。在低并发下,LongAdder 和 AtomicLong 类似,但是在高并发下 LongAdder 比 后者具有更高的吞吐量、性能
构造器
Constructor and Description |
---|
LongAdder() 创建一个新的加法器,初始和为零。 |
常用 API
代码演示:
public class LongAdderTest {
public static void main(String[] args) {
LongAdder longAdder = new LongAdder();
longAdder.increment(); // +1
longAdder.add(2); // +2
longAdder.decrement(); // -1
System.out.println(longAdder.sum()); // 2
}
}
LongAccumulator
jdk中这样描述:
当多线程更新用于收集统计信息的公共值时,此类通常优于
AtomicLong
,而不是细粒度的同步控制。 在低更新争议下,这两类具有相似的特征。 但是,在高度争议的情况下,这一类的预期吞吐量明显高于牺牲更高的空间消耗。
构造器
Constructor and Description |
---|
LongAccumulator(LongBinaryOperator accumulatorFunction, long identity) 使用给定的累加器函数和identity元素创建一个新的实例。 |
常用API
Modifier and Type | Method and Description |
---|---|
void | accumulate(long x) 具有给定值的更新。 |
long | get() 返回当前值。 |
LongAdder和 LongAccumulator 的区别:
- LongAdder 只能从默认值 0 开始计算,并且只能进行加法运算。
- LongAccumulator 支持自定义函数计算。并且可以指定初始值。
代码演示:
// 初始值为1,计算函数为:x+y,初始值相当于 y,accumulate传入的值为x
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 1);
longAccumulator.accumulate(2); // +2 ,相当于参数 x
System.out.println(longAccumulator.get());
演示高并发下 LongAdder和 LongAccumulator 的性能
前面我们说过,在高并发下,LongAdder和 LongAccumulator 的性能优于 AtomicLong。
下面就实现一个点赞功能,演示: synchronized 、Atomic、LongAdder和 LongAccumulator 的性能对比。
代码演示:
/**
*
* Author: YZG
* Date: 2022/11/27 20:26
* Description:
*/
public class ClickDemo {
public static void main(String[] args) throws InterruptedException {
Resource resource = new Resource();
CountDownLatch latch1 = new CountDownLatch(50);
CountDownLatch latch2 = new CountDownLatch(50);
CountDownLatch latch3 = new CountDownLatch(50);
CountDownLatch latch4 = new CountDownLatch(50);
long begin;
long end;
begin = System.currentTimeMillis();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
try {
for (int i1 = 0; i1 < 1000000; i1++) {
resource.clickBySynchronized();
}
} finally {
latch1.countDown();
}
}, String.valueOf(i)).start();
}
latch1.await();
end = System.currentTimeMillis();
System.out.println("clickBySynchronized 耗时: " + (end - begin) + "毫秒" + " , 点赞结果: " + resource.number);
// ==========================================================================================================
begin = System.currentTimeMillis();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
try {
for (int i1 = 0; i1 < 1000000; i1++) {
resource.clickByAtomicLong();
}
} finally {
latch2.countDown();
}
}, String.valueOf(i)).start();
}
latch2.await();
end = System.currentTimeMillis();
System.out.println("clickByAtomicLong 耗时: " + (end - begin) + "毫秒" + " , 点赞结果: " + resource.number);
// ==========================================================================================================
begin = System.currentTimeMillis();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
try {
for (int i1 = 0; i1 < 1000000; i1++) {
resource.clickByLongAdder();
}
} finally {
latch3.countDown();
}
}, String.valueOf(i)).start();
}
latch3.await();
end = System.currentTimeMillis();
System.out.println("clickByLongAdder 耗时: " + (end - begin) + "毫秒" + " , 点赞结果: " + resource.number);
// ==========================================================================================================
begin = System.currentTimeMillis();
for (int i = 0; i < 50; i++) {
new Thread(() -> {
try {
for (int i1 = 0; i1 < 1000000; i1++) {
resource.clickByLongAccumulator();
}
} finally {
latch4.countDown();
}
}, String.valueOf(i)).start();
}
latch4.await();
end = System.currentTimeMillis();
System.out.println("clickByLongAccumulator 耗时: " + (end - begin) + "毫秒" + " , 点赞结果: " + resource.number);
}
}
// 资源类
class Resource {
// 点赞数
public long number ;
// synchronized方式
public synchronized void clickBySynchronized() {
number++;
}
// AtomicLong方式
AtomicLong atomicLong = new AtomicLong(0);
public void clickByAtomicLong() {
atomicLong.incrementAndGet();
}
// LongAdder方式
LongAdder longAdder = new LongAdder();
public void clickByLongAdder() {
longAdder.increment();
}
结果:
clickBySynchronized 耗时: 2958毫秒 , 点赞结果: 50000000
clickByAtomicLong 耗时: 1335毫秒 , 点赞结果: 50000000
clickByLongAdder 耗时: 219毫秒 , 点赞结果: 50000000
clickByLongAccumulator 耗时: 158毫秒 , 点赞结果: 50000000
通过测试可以看出,LongAdder 和 LongAccumulato消耗的时间比Synchronized降低了将近十几倍,因此也可以说明在高并发下 LongAdder 和 LongAccumulato性能确实优秀!
LongAdder 原理分析
从阿里和官网上就已经说明了 LongAdder 性能之高,并推荐在高并发下使用。
阿里手册:
官网:
那么LongAdder 为什么会这么快呢?底层又是什么样呢?一起研究一下。
LongAdder 的继承关系图
首先看 LongAdder 的继承关系:
从继承关系图中得知,LongAdder 继承了 Striped64 ,Striped64 继承了 Number类,我们就先从 Striped64 这个类入手
Striped64
类中有着四个属性,这四个属性的含义如下:
其中最重要的是 cells 和 base ,后面我们会用到。
/** Number of CPUS, to place bound on table size */
// CPU数量,即cells数组的最大长度
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* Table of cells. When non-null, size is a power of 2.
*/
// 单元格数组|cells数组,为2的幂,2,4,8,16.....,方便以后位运算
transient volatile Cell[] cells;
/**
* Base value, used mainly when there is no contention, but also as
* a fallback during table initialization races. Updated via CAS.
*/
// 基础value值,当并发较低时,只累加该值主要用于没有竞争的情况,通过CAS更新。
transient volatile long base ;
/**
* Spinlock (locked via CAS) used when resizing and/or creating Cells.
*/
// 创建或者扩容Cells数组时使用的自旋锁变量调整单元格大小(扩容),创建单元格时使用的锁。
transient volatile int cellsBusy;
类中的方法和变量:
Cell 类 是 Striped64 的一个静态内部类:
/**
* Padded variant of AtomicLong supporting only raw accesses plus CAS.
*
* JVM intrinsics note: It would be possible to use a release-only
* form of CAS here, if it were provided.
*/
@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);
}
// Unsafe mechanics
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);
}
}
}
LongAdder 为什么这么快?
在高并发下LongAdder替换掉 AtomicLong,为什么要替换呢?
AtomicLong 底层采用的 CAS,靠硬件实现原子性和可见性,保证在内存中只有一个线程能够获取到数据,那么其他线程就必须在主内存外进行自旋, 这种方式在低并发的情况下还可以忍受,我们可以想想,如果在高并发线程下,几千个 ,几万个线程同时在自旋,那么这种情况对于 CPU 的消耗是非常严重的。性能会急剧下降
LongAdder 可以理解为 AtomicLong 的升级版,那么他是如何解决 CPU 的空转呢?
LongAdder 在低并发情况下其实和 AtomicLong一样,使用 base 记录 value值,但是在高并发下就不一样,在高并发下,LongAdder 采用分散热点 的方式,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
举个例子:火车站买火车票,
AtomicLong
只要一个窗口,其他人都在排队;而LongAdder
利用cell
开了多个卖票窗口,所以效率高了很多。
sum() 会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点 。
LongAdder 源码分析
LongAdder 在无竞争的情况,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系时则是采用化整为零的做法,从空间换时间,用一个数组 cell ,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。
在源码中,LongAdder 是如何计算 hash 值 并 映射到 cell 数组下标的呢?
add 方法
首先,先看 LongAdder 中的 自增 方法:
public void increment() {
add(1L);
}
调用了 add 方法:
/**
* Adds the given value.
*
* @param x the value to add
*/
public void add(long x) {
// as 表示 cells 引用
// b 表示获取的base值
// v 表示期望值
// m 表示 cells 数组的长度
// a 表示当前线程命中的 cell 单元格
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
// uncontended代表没有冲突。
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
casBase 方法里调用的就是 UNSAFE 类中的 compareAndSwapLong ,这个方法大家都熟悉,就是比较并交换,将期望值与内存中的值作比较,相等则交换新值。
因此 casBase 这个方法就是 判断 base 和内存中的值,如果相等就交换 b+x 。
final boolean casBase(long cmp, long val) {
// this 表示要操作的对象,BASE 内存地址偏移量,cmp 期望值 val 交换的值
return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
当一个线程调用 add 方法,(as = cells) != null = false
。base初始值为0,执行 casBase方法,会将 b+x 与 内存中的值交换,此时内存中的值为1,返回 true,取反等于 false,因此 跳出 if 语句 结束。
因此在线程少的情况下,会一直操作 base 值,并不会采用 cells 数组分散热点。
但是当高并发时,在执行 !casBase(b = base, b + x)
这段代码时,肯定会有某个线程的 base 值与内存中的值不一样,交换失败,返回 false。就会执行 if 语句里面的代码。
as == null = true
会执行 longAccumulate
方法, longAccumulate 方法里就是 as 数组进行初始化,也就是将 base 的值分散到 cells 数组中:
我们可以看到在 longAccumulate方法里,将 cells 数组进行初始化,容量为 2 ,也就是此时 base、cells[0]、cells[1] 都可以供线程获取值。
当有线程再执行 add() 方法时,其中 (as = cells) != null = true
又会跳出 if 语句,结束执行。
因此,在低并发下,只需要执行 casBase 方法进行比较交换就行,不会涉及到cells 分散热点。
但是在高并发下会执行 longAccumulate 方法, 对 cells 数组进行初始化或者扩容。将base的值分散到 cells 数组中供其他线程访问。
总结一下 add方法:
longAccumulate方法
上面中说道 longAccumulate 方法对 cells 数组进行扩容和初始化,那么在该方法中具体怎么做呢?
清楚 longAccumulate 方法的参数:
longAccumulate 方法在Striped64类中,看一下Striped64类中需要用的方法以及属性:
首先看 longAccumulate 的第一部分:获取线程的 probe 值
调用 getProbe() 方法获取当前线程的 探测值(hash值),探测值你可以理解为某个工厂中工人的工号
, 每个线程对应一个值,通过这个值才会找到对应 cells 数组的槽位。
如果在一些特殊情况下,获取不到 (h = getProbe()) == 0 = true
, 会强制获取一个随机值。并且将此线程的竞争状态设置为 true, 表示不参与竞争。
/**
* 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);
}
更详细的说明:
longAccumulate 的第二部分: 是一个 大的 for 自旋,for 循环里面又分为三部分。
在第一部分中 首先给当前线程分配一个hash值,然后进入一个for(;;)自旋,这个自旋分为三个分支:
- CASE1:Cell[]数组已经初始化
- CASE2:Cell[]数组未初始化(首次新建)
- CASE3:Cell[]数组正在初始化中
首先,在 base 转换使用 cells 数组来分散热点时,需要初始化 cells 数组,也就是 CASE2,第二部分:
先看一下整体的思路:
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
boolean init = false;
try { // Initialize table
if (cells == as) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
}
} finally {
cellsBusy = 0;
}
if (init)
break;
}
cellsBusy 是锁的状态,0 表示没有线程占用,1 表示有线程占用。
在初始化cells 数组的时候只需要有一个线程来初始化,因此在某个线程进来后 会先拿到锁,并且调用 casCellsBusy
方法将锁的状态修改成 1
final boolean casCellsBusy() {
// CAS: CELLSBUSY 默认值为0,
return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}
获取锁后,就是初始化 cells 的步骤,创建一个大小容量为 2 的 Cell 数组。
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
init = true;
在初始化完毕之后,执行 finally 语句块释放锁。
当某个线程在初始化的时候,其他线程怎么办呢?因此就需要了解第三步部分 CASE3 了。第三部分就是其他线程在等待某一个线程初始化要执行的。
在第三部分中,其他等待的线程会直接操作 base进行 CAS。
// 在上面得知
else if (casBase(v = base, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break; // Fall back on using base
当 初始化 Cells 数组完毕之后,就执行 第一部分 CASE1了,将 CASE1 分为6小部分,分析:
if ((as = cells) != null && (n = as.length) > 0) {
// 当线程的 hash 值运算后映射得到的 Cell单元为null,说明该Cell 没有被使用
if ((a = as[(n - 1) & h]) == null) {
// Cell[] 数组没有正在扩容
if (cellsBusy == 0) { // Try to attach new Cell
// 创建一个 Cell 单元
Cell r = new Cell(x); // Optimistically create
// 尝试加锁,成功后 casCellsBusy = 1
if (cellsBusy == 0 && casCellsBusy()) {
boolean created = false;
// 在有锁的情况下,在检查一下之前的判断
try { // Recheck under lock
Cell[] rs; int m, j;
// 将 Cell 单元附到Cell 数组上
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
created = true;
}
} finally {
cellsBusy = 0;
}
if (created)
break;
continue; // Slot is now non-empty
}
}
collide = false;
}
/*
CASE1.1 上面代码判断当前线程hash后指向的数据位置元素是否为空,
如果为空则将Cell数据放入数组中,跳出循环。
如果不空则继续循环
*/
else if (!wasUncontended)
wasUncontended = true;
/*
CASE1.2 wasUncontended表示cells初始化后,当前线程竞争修改失败
wasUncontended =false,这里只是重新设置了这个值为true,紧
接着执行advanceProbe(h)重置当前线程的hash,重新循环。
*/
else if (a.cas(v = a.value, ((fn == null) ? v + x :
fn.applyAsLong(v, x))))
break;
/*
CASE1.3 说明当前线程对应的数组中有了数据,也重置过hash值,
这时通过CAS操作尝试对当前数中的value值进行累加x操作,x默认为1,如果CAS 成功则直接跳出循环。
*/
else if (n >= NCPU || cells != as)
collide = false; // At max size or stale
/*
CASE1.4 如果n大于CPU最大数量,不可扩容,
并通过下面的h = advanceProbe(h)方法修
改线程的probe再重新尝试
*/
else if (!collide)
collide = true;
/*
CASE1.5 如果扩容意向callide是false则修改它为true,然后重新计算当前线
程的hash值继续循环,
如果当前数组的长度已经大于了CPU的核数,就会再次设置扩容意
向collide=false (见上一 步)
*/
// CASE1.6:
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 {
// 释放锁,将状态改为0
cellsBusy = 0;
}
collide = false;
continue; // Retry with expanded table
}
h = advanceProbe(h);
}
对CASE1 的总结 :
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进行原子操作
原理:
CAS+自旋
应用场景:
- 低并发下的全局计算
- AtomicLong能保证并发情况下计数的准确性,其内部通过CAS来解决并发安全性的问题。
缺陷 :
高并发后性能急剧下降
N个线程CAS操作修改线程的值,每次只有一个成功过,其它N - 1失败,失败的不停的自旋直到成功,这样大量失败自旋的情况,一下子cpu就打高了。
LongAdder
- 当需要在高并发下有较好的性能表现,且对值的精确度要求不高时,可以使用
- 保证性能,精度代价
- LongAdder是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作
原理
- CAS+Base+Cell数组分散
- 空间换时间并分散了热点数据
应用场景:
高并发下的全局计算
缺陷:
sum求和后还有计算线程修改结果的话,最后结果不够准确
各位彭于晏,如有收获点个赞不过分吧…✌✌✌
gongzhonghao 回复 [JUC] 获取MarkDown笔记