JUC-原子类-0-介绍及基本使用

在并发编程中,原子类也是经常使用的一个工具,利用原子类,可以把一些操作变成一个原子操作,在多线程的情况下不需要加锁也可以保证线程安全

原子类的作用及优势

原子类的作用跟锁是类似的,都是为了保证在并发环境下的线程安全,原子类相比于锁,有一定的优势

  1. 锁的粒度更细: 原子类可以把竞争范围缩小到变量级别,通常我们手动加锁的粒度都会大于原子变量的粒度
  2. 效率更高: 一般情况下,原子类的效率会比使用锁的效率高,除了高度竞争的情况

常用原子类

常用的原子类有以下几种

基本原子类型

  • Atmoic*: AtmoicInteger,AtmoicLong,AtmoicBoolean

作用: 对基本类型的操作变为原子操作

原子数组类型

  • Atmoic*Array: AtmoicIntegerArray, AtmoicLongArray, AtmoicReferenceArray

作用: 对数组的操作为原子操作

引用类型原子类

  • Atmoic*Reference: AtomicReference, AtomicStampedReference, AtomicMarkableReference

作用: 将一个对象转为原子操作

升级类型原子类

  • Atomic*FieldUpdater: AtomicIntegerfieldupdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater
    作用: 将一个非原子变量转换为原子变量

累加器

  • Adder: LongAdder, DoubleAdder
  • Accumulator: LongAccumulator ,DoubleAccumulator

作用: 高效累加操作

原子基本类型使用

AtmoicInteger 为例,了解一下基本的用法,以及常用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class AtomicIntegerTest {

public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);

// 获取当前值
int i = atomicInteger.get();

// 获取当前值并设置新的值
int j = atomicInteger.getAndSet(1);

// 获取当前值并自增
int k = atomicInteger.getAndIncrement();

// 获取当前值并自减
int l = atomicInteger.getAndDecrement();

// 获取当前值,并加上指定的值
int m = atomicInteger.getAndAdd(5);

// 对比并设置值,如果当前的值为预期的值,则以原子的方式将该值设置为输入值
boolean b = atomicInteger.compareAndSet(5, 6);


}

}

原子数组这里就不再介绍了

引用原子类型介绍

AtomicReference 这个类的作用,和 AtmoicInteger 本质上没有却别, AtmoicInteger 是让一个整形变量保证原子性, AtomicReference 可以让一个对象保证原子性

在之前锁的章节,有一个自旋锁的案例,已经用到这个类型了, 代码再贴过来回顾一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SpinLock {

private AtomicReference<Thread> sign = new AtomicReference<>();

private void lock(){
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
}
}

private void unLock(){
Thread current = Thread.currentThread();
sign.compareAndSet(current, null);
}

}

这里就用到了一个原子类的 compareAndSet 这个CAS操作,对比并赋值

升级类型原子类的使用

升级类型原子类,功能是把一个普通的变量升级为原子类,主要的使用场景是当一个普通变量偶尔需要原子操作的时候使用

AtomicIntegerfieldupdater 为例,就是将一个整形升级为原子整形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AtomicIntegerFieldUpdaterTest implements Runnable{

static Candidate testCandidate;

public static AtomicIntegerFieldUpdater<Candidate> scoreUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");

@Override
public void run() {
for (int i = 0; i < 1000; i++) {
scoreUpdater.getAndIncrement(testCandidate);
}
}

public static class Candidate{
volatile int score;
}
}

这里就会把 testCandidate 这个对象的 score 变为原子操作

Adder累加器

在高并发下 LongAdder 的效率比 AtomicLong 要高

使用方式就是 new LongAdder(); 然后调用 add() 方法就可以了

那么为什么 AtomicLong 的性能不如 LongAdder 呢?

AtomicLong 每次执行修改操作,都要做一次 flushrefresh 操作,在高并发的情况下效率就很低了,而 LongAdder 每个线程都会有一个自己的计数器,用来在自己的线程内计算,这样就不会被其他线程干扰,等最后要获取值的时候,把所有线程的和在加起来就是总和

对比 AtomicLong

在低争用下, AtomicLongLongAdder 这两个类具有相似的特征,但是在竞争激烈的情况下,LongAdder 的预期吞吐量要高得多,但要消耗更多的空间

LongAdder 适合的场景是统计求和计数的场景,而且 LongAdder 基本只提供了 add 方法,而 AtomicLong 还具有cas方法

Accumulator 累加器

Accumulator 是一个更加灵活的累加器,使用方式上跟上面的 LongAdder 是有些不同的,如下

1
2
3
4
5
6
7
LongAccumulator accumulator = new LongAccumulator(Long::sum, 0);

LongAccumulator accumulator1 = new LongAccumulator((x, y) -> x + y, 0);

accumulator.accumulate(1);
// 获取后清空
accumulator.getThenReset();

代码中的 accumulatoraccumulator1 作用都是一样的, 就是lambda的两种写法,下面这个看着更直观一点

LongAccumulator 需要两个参数,第一个是 LongBinaryOperator 的实现他是一个函数式接口, 然后传参的时候传具体的实现就行了,第二个参数 0 就是 x 的初始值

上面代码中,刚初始化的时候 x 就是 0 ,然后第一次累加的时候会把x的值给y,然后再把传进来的x跟y做累加,结果赋值给 y
当然这个表达式不一定就是累加,也可以做自己的实现,所以这个比上面的 LongAdder 要更加灵活

总结

了解常用的原子类,以及一些常用的方法