java原子类详解
什么原子类
原子类是具有原子性的类,原子性的意思是对于一组操作,要么全部执行成功,要么全部执行失败,不能只有其中某几个执行成功。
原子类作用
作用和锁有类似之处,是为了保证并发情况下的线程安全。
相对于锁的优势
-
粒度更细
原子变量可以把竞争范围缩小到变量级别,通常情况下锁的粒度也大于原子变量的粒度 -
效率更高
除了在高并发之外,使用原子类的效率往往比使用同步互斥锁的效率更高,因为原子类底层利用了CAS,不会阻塞线程。
原子类种类
在JDK中J.U.C包下提供了种类丰富的原子类,以下所示:
类型 | 具体类型 |
---|---|
Atomic* 基本类型原子类 | AtomicInteger、AtomicLong、AtomicBoolean |
Atomic*Array 数组类型原子类 | AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray |
Atomic*Reference 引用类型原子类 | AtomicReference、AtomicStampedReference、AtomicMarkableReference |
Atomic*FieldUpdater 升级类型原子类 | AtomicIntegerfieldupdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater |
Adder 累加器 | LongAdder、DoubleAdder |
Accumulator 积累器 | LongAccumulator、DoubleAccumulator |
AtomicInteger常用方法
上面列举了J.U.C中提供的一些原子操作类,接下来从简单的AtomicInteger开始分析,来看看它的常用方法,其他的两种AtomicLong、AtomicBoolean和它相似
方法 | 作用 |
---|---|
public final int get() | 获取当前的值 |
public final int getAndSet(int newValue) | 获取当前的值,并设置新的值 |
public final int getAndIncrement() | 获取当前的值,并自增+1 |
public final int getAndDecrement() | 获取当前的值,并自减-1 |
public final int getAndAdd(int delta) | 获取当前的值,并加上预期的值。getAndIncrement和getAndDecrement不满足,可使用当前方法 |
boolean compareAndSet(int expect, int update) | 如果输入的数值等于预期值,则以原子方式将该值更新为输入值(update) |
Array 数组类型原子类
AtomicArray 数组类型原子类,数组里的元素,都可以保证其原子性,比如 AtomicIntegerArray 相当于把 AtomicInteger 聚合起来,组合成一个数组。我们如果想用一个每一个元素都具备原子性的数组的话, 就可以使用 AtomicArray
该类包括:
类名 | 作用 |
---|---|
AtomicIntegerArray | 整形数组原子类 |
AtomicLongArray | 长整形数组原子类 |
AtomicReferenceArray | 引用类型数组原子类 |
Atomic*Reference 引用类型原子类
AtomicReference引用类型原子类,作用和AtomicInteger没有本质区别,AtomicReference是让一个对象保持原子性,而不局限一个变量。
该种类所有类型:
类名 | 作用 |
---|---|
AtomicReference | 保证对象的原子性 |
AtomicStampedReference | 它是对 AtomicReference 的升级,在此基础上还加了时间戳,用于解决 CAS 的 ABA 问题。 |
AtomicMarkableReference | 和 AtomicReference 类似,多了一个绑定的布尔值,可以用于表示该对象已删除等场景。 |
Atomic*FieldUpdater原子更新器
原子类更新器主要用于对已经声明的非原子变量,为它增加原子性,让该变量拥有CAS操作的能力。
该种类所有类型:
类名 | 作用 |
---|---|
AtomicIntegerFieldUpdater | 原子更新整形的更新器 |
AtomicLongFieldUpdater | 原子更新长整形的更新器 |
AtomicReferenceFieldUpdater | 原子更新引用的更新器 |
这里使用AtomicIntegerFieldUpdater来举例说明:
public class AtomicIntegerFieldUpdaterDemo implements Runnable {
static Score useUpdaterScore;
static Score unusedComputerScore;
//声明原子更新类,泛型为Score类,更新的是"score"字段
public static AtomicIntegerFieldUpdater<Score> scoreUpdater = AtomicIntegerFieldUpdater
.newUpdater(Score.class, "score");
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
//非原子操作直接自增
unusedComputerScore.score++;
//使用更新器更新字段,同样执行自增
scoreUpdater.getAndIncrement(useUpdaterScore);
}
}
public static class Score {
volatile int score;
}
public static void main(String[] args) throws InterruptedException {
useUpdaterScore = new Score();
unusedComputerScore = new Score();
AtomicIntegerFieldUpdaterDemo r = new AtomicIntegerFieldUpdaterDemo();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("普通变量的结果:" + unusedComputerScore.score);
System.out.println("升级后的结果:" + useUpdaterScore.score);
}
}
输出结果:
普通变量的结果:1980
升级后的结果:2000
从结果可以看出,升级后结果是期望值。
- 使用场景(相对于直接使用AtomicInteger)
- 历史原因
如果当前变量在之前开发版本中使用地方过多,这个时候为了不做大量改造,可以在需要原子性的时候,使用原子更新器 - 少部分情况需要原子性的时候
由于原子类型的变量比普通变量更耗资源。在大部分不需要原子性的时候,如果都设置成原子类型,这非常的耗资源。因此,我们可以再需要原子性的少数情况下,对当前变量使用AtomicIntegerFieldUpdater进行合理的升级。
- 历史原因
常见问题
原子类是如何利用 CAS 保证线程安全的?
以AtomicInteger为例,jdk 8实现,看怎么实现累加原子操作的
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
U是Unsafe类
该方法实际调用的是,unsafe.getAndAddInt(this, VALUE, delta),这里先介绍一下Unsafe类。
- Unsafe类
Unsafe是CAS的核心类。由于java无法直接访问底层操作系统,而是需要通过本地方法来实现。JVM还是提供了Unsafe类,他提供了硬件层面的原子操作,可以直接操作内存的数据。
接下来看一下AtomicInteger的一些关键代码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception var1) {
throw new Error(var1);
}
}
private volatile int value;
public final int get() {
return value;
}
......
}
首先获取Unsafe类型变量unsafe,并定义变量valueOffset,然后在静态代码块中value值在内存中的偏移地址,赋值给valueOffset变量。因为 Unsafe 就是根据内存偏移地址获取数据的原值的,这样我们就能通过 Unsafe 来实现 CAS 了。
value是用volatile修饰的,他就是我们原子类存储值的变量,由于使用volatile修饰,所以在多线程中看到的value值都是同一份,保证了可见性。
接下来看实际执行cas的地方。Unsafe的getAndAddInt()方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 +var4));
return var5;
}
先看do-while循环,它是一个无限循环,知道满足条件才退出循环。do中的代码时,获取到当前内存中的值赋值给var5,var1是当前原子对象,var2是value在内存中的偏移量。此时var5就是此时原子类的数值。
再看看while中的退出条件。compareAndSwapInt这个方法参数,他们的实际意义是:
- 第一个参数 --> 当前原子类对象,即AtomicInteger 这个对象本身
- 第二个参数 --> 当前值的内存偏移量,接触它可以获取到value的值
- 第三个参数 --> 当前值,如果当前值不匹配,更新失败会返回false,开始下次循环。如果当前值匹配,同时更新成功,方法返回true,跳出循环,完成当前累加操作
- 第四个参数 --> 期望值,即当前值加上累加的值。
所以 compareAndSwapInt 方法的作用就是,判断如果现在原子类里 value 的值和之前获取到的 var5 相等的话,那么就把计算出来的 var5 + var4 给更新上去,所以说这行代码就实现了 CAS 的过程。
总结一下,Unsafe 的 getAndAddInt 方法是通过循环 + CAS 的方式来实现的,在此过程中,它会通过 compareAndSwapInt 方法来尝试更新 value 的值,如果更新失败就重新获取,然后再次尝试更新,直到更新成功。
AtomicInteger 在高并发下
AtomicLong 高并发下存在的问题
在并发情况下,如果我们需要实现计数器(例如下载任务数),可以利用AtomicInteger和AtomicLong,这样一来可以避免加锁和复杂的代码逻辑。虽然它们好用,但是也存在着一些问题。
public class AtomicLongDemo {
public static void main(String[] args) throws InterruptedException {
AtomicLong counter = new AtomicLong(0);
ExecutorService service = Executors.newFixedThreadPool(16);
for (int i = 0; i < 100; i++) {
service.submit(new Task(counter));
}
Thread.sleep(2000);
System.out.println(counter.get());
}
static class Task implements Runnable {
private final AtomicLong counter;
public Task(AtomicLong counter) {
this.counter = counter;
}
@Override
public void run() {
counter.incrementAndGet();
}
}
}
在这里,定义了一个为0的AtomicLong变量counter,然后创建了线程池数为16的线程池。然后执行100个任务。然后任务是执行counter.incrementAndGet()。结果毫无疑问是100,虽然并发执行,但是AtomicLong依然能保证incrementAndGet是一个原子操作,所以不会发生线程安全问题。
不过我们仔细看细节,对于 AtomicLong 内部的 value 属性而言,也就是保存当前 AtomicLong 数值的属性,它是被 volatile 修饰的,所以它需要保证自身可见性。
当线程1执行incrementAndGet操作更新成功后,会将值向主内存中修改,同时主内存会将其他线程的value给修改掉,而且CAS也会经常失败,这两个操作是非常耗资源的。
如何改进AtomicLong 在高并发下性能
在JDK 8中新增了LongAdder 类,来优化这一个问题。
我们将上面例子中做小小的调整,将AtomicLong换成LongAdder,get()方法换成sum(),incrementAndGet()换成increment()
public class LongAdderDemo {
public static void main(String[] args) throws InterruptedException {
LongAdder counter = new LongAdder();
ExecutorService service = Executors.newFixedThreadPool(16);
for (int i = 0; i < 100; i++) {
service.submit(new Task(counter));
}
Thread.sleep(2000);
System.out.println(counter.sum());
}
static class Task implements Runnable {
private final LongAdder counter;
public Task(LongAdder counter) {
this.counter = counter;
}
@Override
public void run() {
counter.increment();
}
}
}
运行得到的结果还是100,但是运行速度比刚才AtomicLong实现要快。那么为什么LongAddr比AtomicLong快呢?
LongAddr源码解析
//集成自Striped64
public class LongAdder extends Striped64 implements Serializable {}
abstract class Striped64 extends Number {
......
/**
* 单元格表。如果为非null,则大小为2的幂。
*/
transient volatile Cell[] cells;
/**
* 基本值,主要在没有争用时使用,也用作表初始化过程中的回退。通过CAS更新。
*/
transient volatile long base;
......
}
LongAdder中引入了分段累加的概念,内部的Cell[]数组和base变量都参与了计数
其中base在竞争不激烈的情况下,直接把累加的结果改到base变量上;
在竞争激烈的时候,各个线程会分散累加到自己所对应的Cell[] 数组的某一个对象中,而不是公用一个。LongAdder这样分段的思想将不同线程到不同Cell上的修改,避免了大量的冲突,提升了并发性。更JDK 7 中的ConcurrentHashMap思想相似。
竞争激烈的时候,LongAdder 会通过计算出每个线程的 hash 值来给线程分配到不同的 Cell 上去,每个 Cell 相当于是一个独立的计数器,这样一来就不会和其他的计数器干扰,Cell 之间并不存在竞争关系,所以在自加的过程中降低了冲突的概率。这个思想的本质就是空间换时间,所以会耗费更多的内存。
最终的结果是通过LongAdder#sum()方法来获取的,将各个Cell值累计求和,再加上base返回。
public long sum() {
Cell[] as = cells;
long sum = base;
if (as != null) {
for (Cell a : as)
if (a != null)
sum += a.value;
}
return sum;
}
AtomicLong和LongAdder使用如何选择
在低竞争的情况下,AtomicLong和LongAdder的性能相似;但是在竞争激烈的情况下,LongAdder 的预期吞吐量要高得多,经过试验,LongAdder 的吞吐量大约是 AtomicLong 的十倍,虽然性能提高了,但是LongAdder会耗费更多的空间
- LongAdder 只提供了 add、increment 等简单的方法,适合的是统计求和计数的场景,场景比较单一
- 而 AtomicLong 还具有 compareAndSet 等高级方法,可以应对除了加减之外的更复杂的需要 CAS 的场景。
- 小结
如果我们的场景仅仅是需要用到加和减操作的话,那么可以直接使用更高效的 LongAdder,但如果我们需要利用 CAS 比如 compareAndSet 等操作的话,就需要使用 AtomicLong 来完成。
原子类 和 volatile的区别
-
volatile只保证可见性
volatile只保证了可见性,而不能保证原子性。例如主内存中volatile int a=0,在两个线程中时,如果都同时获取当前的volatile修饰的变量值,但是都执行自增操作,由于两个线程都在之前获取了值即a=0,两线程都执行自增后都是a=1了,这是两个线程同时更新主内存中的a,最后得到的结果是a=1,这个就和预期值不一样,由于并没有保证获取和赋值这个操作的原子性,会有线程安全问题。 -
原子类保证了可见性和原子性
上面的例子可以使用原子类AtomicInteger来修复,将自增操作换成incrementAndGet(),底层通过CPU指令保证原子性,解决线程安全问题
使用场景
- | 使用场景 |
---|---|
volatile | 通常情况下,volatile 可以用来修饰 boolean 类型的标记位,因为对于标记位来讲,直接的赋值操作本身就是具备原子性的,再加上 volatile 保证了可见性,那么就是线程安全的了 |
原子类 | 对于先获取值,然后做一定修改,再赋值回去的操作,就需要原子类来保证原子性 |
AtomicInteger 和 synchronized 的异同点
相同点
- 都能保证线程安全
不同点
- | 原理 | 使用范围 | 粒度 | 性能 |
---|---|---|---|---|
AtomicInteger | 使用CAS来保证线程的原子性 | 仅一个对象,少数场景使用 | 竞争在变量级别 | 乐观锁,低并发时性能好 |
synchronized | 使用monitor锁来保证线程安全性。再执行同步代码之前,需要先获取到monitor锁,执行完毕,释放锁。 | 既可以修饰一个方法,又可以修饰一段代码,可以根据我们的需要,非常灵活地去控制它的应用范围 | 通常会大于变量级别 | 悲观锁(jdk6之后锁升级优化后,低并发情况性能也不错),在高并发时由于AtomicInteger |
Java 8 中 Adder 和 Accumulator 有什么区别
上面讲了Adder是通过CAS加分段思想来提高Atomic*的性能。
而LongAccumulator是LongAdder的功能增强版,LongAccumulator在LongAdder只有数值加减的基础上提供自定义的函数操作。
例如,下面代码
public class LongAccumulatorDemo {
public static void main(String[] args) throws InterruptedException {
LongAccumulator accumulator = new LongAccumulator((x, y) -> x + y, 0);
ExecutorService executor = Executors.newFixedThreadPool(8);
IntStream.range(1, 10).forEach(i -> executor.submit(() -> accumulator.accumulate(i)));
Thread.sleep(2000);
System.out.println(accumulator.getThenReset());
}
}
上面是使用8线程数的线程池累加0-9。最后获取到结果为45.
x是上一次的结果,y是传入的新值。由于使用多线程,当前的方法只适用于即使执行顺序不同,结果依然一样的情况,即 交 换 性 \color{red}交换性 交换性。
下面几种场景适合使用:
- 相加
- 相乘
- 最大值、最小值
总结
上面讲了原子类所有的种类、使用、及其原理,同时也介绍了各个原子类的使用场景,并且和volatile、synchronized做了对比。我们在实际开发中可以根据具体业务来选择适合的原子类使用。