atomiclong 初始化_2020-10-18:java中LongAdder和AtomicLong有什么区别?

性能对比:

测试程序,对比同步锁,Atomic还有LongAdder:

public class StressTest {

static long syncTest = 0;

static AtomicLong atomicTest = new AtomicLong(0);

static LongAdder longAdderTest = new LongAdder();

static int currentThreadNum;

public static void main(String[] args) throws InterruptedException {

test(1);

test(5);

test(10);

test(50);

test(100);

}

static void test(int threadNum) throws InterruptedException {

currentThreadNum = threadNum;

SyncIncrement[] syncIncrements = new SyncIncrement[threadNum];

AtomicIncrement[] atomicIncrements = new AtomicIncrement[threadNum];

LongAdderIncrement[] longAdderIncrements = new LongAdderIncrement[threadNum];

for (int i = 0; i < threadNum; i++) {

syncIncrements[i] = new SyncIncrement();

atomicIncrements[i] = new AtomicIncrement();

longAdderIncrements[i] = new LongAdderIncrement();

}

System.out.println("---------Thread Number: " + currentThreadNum + "---------");

long start = System.currentTimeMillis();

for (int i = 0; i < threadNum; i++) {

syncIncrements[i].start();

}

for (int i = 0; i < threadNum; i++) {

syncIncrements[i].join();

}

System.out.println("Synchronized Lock time elapsed: " + (System.currentTimeMillis() - start) + "ms");

start = System.currentTimeMillis();

for (int i = 0; i < threadNum; i++) {

atomicIncrements[i].start();

}

for (int i = 0; i < threadNum; i++) {

atomicIncrements[i].join();

}

System.out.println("Atomic Lock time elapsed: " + (System.currentTimeMillis() - start) + "ms");

start = System.currentTimeMillis();

for (int i = 0; i < threadNum; i++) {

longAdderIncrements[i].start();

}

for (int i = 0; i < threadNum; i++) {

longAdderIncrements[i].join();

}

System.out.println("Long adder time elapsed: " + (System.currentTimeMillis() - start) + "ms");

}

static class SyncIncrement extends Thread {

@Override

public void run() {

for (int i = 0; i < 10000000/currentThreadNum; i++) {

synchronized (SyncIncrement.class) {

syncTest++;

}

}

}

}

static class AtomicIncrement extends Thread {

@Override

public void run() {

for (int i = 0; i < 10000000/currentThreadNum; i++) {

atomicTest.incrementAndGet();

}

}

}

static class LongAdderIncrement extends Thread {

@Override

public void run() {

for (int i = 0; i < 10000000/currentThreadNum; i++) {

longAdderTest.increment();

}

}

}

}

输出:

---------Thread Number: 1---------

Synchronized Lock time elapsed: 205ms

Atomic Lock time elapsed: 133ms

Long adder time elapsed: 126ms

---------Thread Number: 5---------

Synchronized Lock time elapsed: 1906ms

Atomic Lock time elapsed: 648ms

Long adder time elapsed: 65ms

---------Thread Number: 10---------

Synchronized Lock time elapsed: 1964ms

Atomic Lock time elapsed: 635ms

Long adder time elapsed: 53ms

---------Thread Number: 50---------

Synchronized Lock time elapsed: 2157ms

Atomic Lock time elapsed: 682ms

Long adder time elapsed: 52ms

---------Thread Number: 100---------

Synchronized Lock time elapsed: 2057ms

Atomic Lock time elapsed: 602ms

Long adder time elapsed: 71ms

AtomicLong的提速思路

假设我们要统计接口调用次数,一般我们会用AtomicLong

每次接口被调用,我们调用

AtomicLong.incrementAndGet()

想看当前调用次数,就直接调用

AtomicLong.get()

统计类应用一般是,写入多,读取少,读取可能远小于写入

我们觉得AtomicLong还不够好,想进一步提高性能,尤其是写入性能

可能我们会想到空间换时间,一个AtomicLong性能不够,我们用多个。

假设一共N个AtomicLong,代码变成:

接口被调用时:AtomicLong[随机数(或者递增数)%N].incrementAndGet()

获取统计总数:

for(int i=0;i

count += AtomicLong[i].get();

}

这样在获取统计总数时,如果有其他线程写入,可能统计结果不准确,但这对于统计来说其实可以忽略

写入的时候有取余运算,取余运算太低效,我们利用取余的特性:

对于2的n次方取余相当于对2的n次方减一取与运算。

我们规定N必须为2的n次方

这时我们的计数代码就变成了:

AtomicLong[随机数(或者递增数)&(2^n-1)].incrementAndGet()

然后我们想到,我们用的是数组,内存上是连续的,有可能会发生什么?

那就是falseSharing!这货严重影响我们的性能!

可以参考我的另一篇文章:

怎么解决FalseSharing?

Disruptor框架用了long填充

Java 8之后有@Contended注解

我们重写一个AtomicInteger类,增加一个value,在这个value上面打上@Contended注解就行啦

至此,我们就把LongAdder给实现了

LongAdder源代码分析

Cell类

我们先来看被改写的基础AtomicLong类,就是Cell。

//Contended注解代表需要缓存行填充,会对于value前后进行缓存行填充,防止falseSharing导致的性能下降

@sun.misc.Contended

static final class Cell {

//代表原来的AtomicLong中的记录值

volatile long value;

Cell(long x) { value = x; }

//CAS更新,原来AtomicLong中的CAS调用的也是UNSAFE.compareAndSwapLong

final boolean cas(long cmp, long val) {

return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);

}

// Unsafe类, 用于内存操作

private static final sun.misc.Unsafe UNSAFE;

// value在这个类中的偏移量,用于cas更新使用

private static final long valueOffset;

static {

try {

UNSAFE = sun.misc.Unsafe.getUnsafe();

Class> ak = Striped64.Cell.class;

//通过Unsafe类确认value的偏移

valueOffset = UNSAFE.objectFieldOffset

(ak.getDeclaredField("value"));

} catch (Exception e) {

throw new Error(e);

}

}

}

说一说@Contended注解

@Contended注解可以用于class上还有字段上。

用于class上,则在类中所有域前后加上缓存行填充,例如:

@Contended

public static class ContendedTest2 {

private Object plainField1;

private Object plainField2;

private Object plainField3;

private Object plainField4;

}

假设使用的是128bytes的填充(2倍于大多数硬件缓存行的大小 – 来避免相邻扇区预取导致的伪共享冲突。),在内存中的分布就是(@140表示字段在类中的地址偏移):

TestContended$ContendedTest2: field layout

Entire class is marked contended

@140 --- instance fields start ---

@140 "plainField1" Ljava.lang.Object;

@144 "plainField2" Ljava.lang.Object;

@148 "plainField3" Ljava.lang.Object;

@152 "plainField4" Ljava.lang.Object;

@288 --- instance fields end ---

@288 --- instance ends ---

为什么是从140开始?首先对象头在64位虚拟机中,如果启用压缩对象头的话,占用12位。之后,我们使用的是128bytes的填充,所以偏移128bytes。12bytes+128bytes=140bytes。之后四个Object指针每个占用4bytes,最后由于我们使用的是128bytes的填充所以需要128bytes的偏移,到现在一共是284bytes。JVM虚拟机内存分布是8bytes对齐,所以这里一共需要288bytes来满足8bytes对齐(这里涉及到的概念可以参考我的另一篇文章:https://blog.csdn.net/zhxdick/article/details/61916359)。

用于字段上,被注释的字段将和其他字段隔离开来,会被加载在独立的缓存行上。在字段级别上,@Contended还支持一个“contention group”属性(Class-Level不支持),同一group的字段们在内存上将是连续,但和其他他字段隔离开来。例如:

public static class ContendedTest1 {

@Contended

private Object contendedField1;

private Object plainField1;

private Object plainField2;

private Object plainField3;

private Object plainField4;

}

在内存中的分布是:

TestContended$ContendedTest1: field layout

@ 12 --- instance fields start ---

@ 12 "plainField1" Ljava.lang.Object;

@ 16 "plainField2" Ljava.lang.Object;

@ 20 "plainField3" Ljava.lang.Object;

@ 24 "plainField4" Ljava.lang.Object;

@156 "contendedField1" Ljava.lang.Object; (contended, group = 0)

@288 --- instance fields end ---

@288 --- instance ends ---

12bytes的对象头,在所有字段后跟着由@Contended注解修饰的字段,由于使用的是128bytes的填充,开始位置是24+4+128=156.最后由于我们使用的是128bytes的填充所以需要128bytes的偏移,到现在一共是284bytes。JVM虚拟机内存分布是8bytes对齐,所以这里一共需要288bytes来满足8bytes对齐。

如果注解多个字段,则分别被填充:

public static class ContendedTest4 {

@Contended

private Object contendedField1;

@Contended

private Object contendedField2;

private Object plainField3;

private Object plainField4;

}

内存分布:

TestContended$ContendedTest4: field layout

@ 12 --- instance fields start ---

@ 12 "plainField3" Ljava.lang.Object;

@ 16 "plainField4" Ljava.lang.Object;

@148 "contendedField1" Ljava.lang.Object; (contended, group = 0)

@280 "contendedField2" Ljava.lang.Object; (contended, group = 0)

@416 --- instance fields end ---

@416 --- instance ends ---

在某些情况,你会想对字段进行分组,同一组的字段会和其他字段有访问冲突,但是和同一组的没有。例如,(同一个线程的)代码同时更新2个字段是很常见的情况。如果同时把2个字段都添加@Contended注解是足够的(翻译注:但是太足够了),但我们可以通过去掉他们之间的填充,来优化它们的内存空间占用。为了区分组,我们有一个参数“contention group”来描述:

public static class ContendedTest5 {

@Contended("updater1")

private Object contendedField1;

@Contended("updater1")

private Object contendedField2;

@Contended("updater2")

private Object contendedField3;

private Object plainField5;

private Object plainField6;

}

内存分布是:

TestContended$ContendedTest5: field layout

@ 12 --- instance fields start ---

@ 12 "plainField5" Ljava.lang.Object;

@ 16 "plainField6" Ljava.lang.Object;

@148 "contendedField1" Ljava.lang.Object; (contended, group = 12)

@152 "contendedField2" Ljava.lang.Object; (contended, group = 12)

@284 "contendedField3" Ljava.lang.Object; (contended, group = 15)

@416 --- instance fields end ---

@416 --- instance ends ---

LongAdder类的核心方法add

public static class LongAdder extends Striped64 implements Serializable {

private static final long serialVersionUID = 7249069246863182397L;

public LongAdder() {

}

/**

* 核心方法,加x

* @param x 加数

*/

public void add(long x) {

Cell[] as; long b, v; int m; Cell a;

//cells不为null,代表初始化结束,已经进入cell更新逻辑,证明已经有过锁争用情况,之后就一致通过cell更新

//如果cell是null,就通过base更新。如果对于base cas更新失败,才会进入Cell更新的逻辑

//也就是在没有争用的情况下,只会对于base进行更新,不会进入后面cell复杂的更新逻辑

if ((as = cells) != null || !casBase(b = base, b + x)) {

boolean uncontended = true;

//如果as是null,代表第一次进入cell,调用longAccumulate进行cell更新

//如果as不是null,as的长度不大于零,证明没有初始化完,调用longAccumulate进行cell更新

//如果as不是null且as的长度大于零,通过当前线程标识对于m取与运算(利用对于2^n取余相当于对于2^n-1取与运算)获取对应的cell,如果这个cell是null证明没有初始化,调用longAccumulate进行cell更新

//如果初始化了,就cas更新这个cell,更新失败的话,调用longAccumulate进行cell更新

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);

}

}

/**

* 加一

*/

public void increment() {

add(1L);

}

/**

* 减一(就是加-1)

*/

public void decrement() {

add(-1L);

}

/**

* 返回当前值,将每个cell加在一起,不加任何锁,所以可能会有并发统计问题

* @return the sum

*/

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;

}

/**

* 重置为零,同样没锁,同样会有并发竞争问题

*/

public void reset() {

Cell[] as = cells; Cell a;

base = 0L;

if (as != null) {

for (int i = 0; i < as.length; ++i) {

if ((a = as[i]) != null)

a.value = 0L;

}

}

}

/**

* 先统计后重置,同样没锁,同样会有并发竞争问题

*/

public long sumThenReset() {

Cell[] as = cells; Cell a;

long sum = base;

base = 0L;

if (as != null) {

for (int i = 0; i < as.length; ++i) {

if ((a = as[i]) != null) {

sum += a.value;

a.value = 0L;

}

}

}

return sum;

}

}

这里我们弄明白了更新的逻辑,那么细节的核心方法longAccumulate是怎么回事呢?来看Striped64类

Striped64类

abstract static class Striped64 extends Number {

//CPU个数,限制cell数组最大数量

static final int NCPU = Runtime.getRuntime().availableProcessors();

// cell数组,长度一样要是2^n

//原因呢,因为对于2^n取余相当于对2^n-1取与运算,提高代码性能

transient volatile Cell[] cells;

// 累积器的基本值,在两种情况下会使用:

// 1、没有遇到并发的情况,直接使用base,速度更快;

// 2、多线程并发初始化table数组时,必须要保证table数组只被初始化一次,因此只有一个线程能够竞争成功,这种情况下竞争失败的线程会尝试在base上进行一次累积操作

// 注意,累加值是base加上每个cell的值

transient volatile long base;

// 自旋标识,在对cells进行初始化,或者后续扩容时,

// 需要通过CAS操作把此标识设置为1(busy,忙标识,相当于加锁),

// 取消busy时可以直接使用cellsBusy = 0,相当于释放锁

transient volatile int cellsBusy;

Striped64() {

}

// 使用CAS更新base的值,其实还是用的unsafe类,可以把base理解为一个基础的AtomicLong

final boolean casBase(long cmp, long val) {

return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);

}

// 使用CAS将cells自旋标识更新为1,相当于加锁

// 更新为0时可以不用CAS,直接使用cellsBusy = 0,相当于释放锁

final boolean casCellsBusy() {

return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);

}

// probe是ThreadLocalRandom里面的一个属性,通过ThreadLocalRandom.current()可以初始化这个属性

// 可以认为probe是线程标识

static final int getProbe() {

return UNSAFE.getInt(Thread.currentThread(), PROBE);

}

// 相当于rehash,重新算一遍线程的hash值,用于标识线程

static final int advanceProbe(int probe) {

probe ^= probe << 13; // xorshift

probe ^= probe >>> 17;

probe ^= probe << 5;

UNSAFE.putInt(Thread.currentThread(), PROBE, probe);

return probe;

}

final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {

int h;

// 这个if相当于给线程生成一个非0的hash值

if ((h = getProbe()) == 0) {

//如果为零,调用ThreadLocalRandom.current()初始化

ThreadLocalRandom.current();

//之后就能获取到正常的线程标识

h = getProbe();

//进入到这里肯定是第一次进入,未初始化过,设置wasUncontended为true

wasUncontended = true;

}

//标识上次循环获取到的cell是不是null,可以理解为是否需要扩容

boolean collide = false;

for (;;) {

Cell[] as; Cell a; int n; long v;

//如果cells不为null并且长度大于0,代表已经初始化了

if ((as = cells) != null && (n = as.length) > 0) {

//取余,获取当前线程对应的cell,如果还为null

if ((a = as[(n - 1) & h]) == null) {

//如果cellsBusy标记为零,代表未上锁(也就是没有其他线程在执行扩容)

if (cellsBusy == 0) {

//初始化这个槽的cell,用需要加的数x初始化,如果加入槽成功相当于就已经加了x

Cell r = new Cell(x);

//尝试加锁

if (cellsBusy == 0 && casCellsBusy()) {

boolean created = false;

try {

Cell[] rs; int m, j;

//获取锁之后,还要判断一次,考虑别的线程可能执行了扩容,这里重新赋值重新判断

if ((rs = cells) != null && (m = rs.length) > 0 && rs[j = (m - 1) & h] == null) {

//赋值,相当于就已经加了x

rs[j] = r;

created = true;

}

} finally {

//无论如何都要释放锁

cellsBusy = 0;

}

//如果加入槽成功,证明已经加上了x,可以退出了

if (created)

break;

//加入槽失败,证明不是自己加上的x,失败重试

continue;

}

}

//只是获取锁失败,并且为了减少冲突,先不考虑扩容

collide = false;

}

//wasUncontended代表的是外部cas更新对应槽位是否成功,如果是失败,并且该槽位不为空,则考虑重新给线程生成唯一标识避免冲突

//设置wasUncontended为true之后会走到h = advanceProbe(h)重新生成唯一标识

else if (!wasUncontended)

wasUncontended = true;

//尝试CAS更新槽内cell的值

else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))

//成功则退出

break;

else if (n >= NCPU || cells != as) // cell数组已经是最大的了,或者中途发生了扩容操作。因为NCPU不一定是2^n,所以这里用 >=

//长度n是递增的,执行到了这个分支,说明n >= NCPU会永远为true,下面两个else if就永远不会被执行了,也就永远不会再进行扩容

// CPU能够并行的CAS操作的最大数量是它的核心数(CAS在x86中对应的指令是cmpxchg,多核需要通过锁缓存来保证整体原子性),当n >= NCPU时,再出现几个线程映射到同一个Cell导致CAS竞争的情况,那就真不关扩容的事了,完全是hash值的锅了

collide = false;

// 映射到的Cell单元不是null,并且尝试对它进行累积时,CAS竞争失败了,这时候把扩容意向设置为true

// 下一次循环如果还是跟这一次一样,说明竞争很严重,那么就真正扩容

// 把扩容意向设置为true,只有这里才会给collide赋值为true,也只有执行了这一句,才可能执行后面一个else if进行扩容

else if (!collide)

collide = true;

// 最后再考虑扩容,能到这一步说明竞争很激烈,尝试加锁进行扩容 -

else if (cellsBusy == 0 && casCellsBusy()) { ----- 标记为分支B

try {

//检查下是否被别的线程扩容了(CAS更新锁标识,处理不了ABA问题,这里再检查一遍)

if (cells == as) {

// 执行2倍扩容

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;

}

// 重新给线程生成一个hash值,降低hash冲突,

h = advanceProbe(h);

}

// cells没有被加锁,并且它没有被初始化,那么就尝试对它进行加锁,加锁成功进入这个else if

else if (cellsBusy == 0 && cells == as && casCellsBusy()) {

boolean init = false;

try {

if (cells == as) {

// 初始化时只创建两个单元

Cell[] rs = new Cell[2];

// 对其中一个单元进行累积操作,另一个不管,继续为null

rs[h & 1] = new Cell(x);

cells = rs;

init = true;

}

} finally {

cellsBusy = 0;

}

if (init)

break;

}

else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))

break;

}

}

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AtomicLong是一个Java原子类,用于对长整型数据进行原子操作。它提供了一系列方法,可以实现原子性的增加、减少、设置、获取等操作。与AtomicInteger类似,AtomicLong的使用方式也类似。 AtomicLong是线程安全的,它使用了CAS(Compare and Swap)机制来保证操作的原子性。CAS是一种乐观锁的实现方式,它通过比较内存的值与期望值来确定是否修改,如果相等则修改,否则重新尝试。这种方式可以避免使用锁,提高了并发性能。 在编译成字节码时,编译器会判断当前JVM或机器硬件是否支持8字节的CAS操作。如果支持,则使用无锁的方式实现原子操作;如果不支持,则会使用加锁处理。这样可以保证在不同的环境下都能正常使用AtomicLong。 总结来说,AtomicLong是一个用于对长整型数据进行原子操作的Java类,它提供了一系列方法,使用CAS机制来保证操作的原子性。它的使用方式与AtomicInteger类似,并且在编译器会根据环境选择合适的实现方式。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Java concurrency之AtomicLong原子类_动力节点Java学院整理](https://download.csdn.net/download/weixin_38747818/12778861)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [并发编程 — 原子类 AtomicLong 详解](https://blog.csdn.net/small_love/article/details/111057686)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值