2 Atomic 类
2 Atomic 类
2.1 AtomicInteger 和 AtomicLong
2.1.1 悲观锁和乐观锁
- 悲观锁:数据发生并发冲突的概率很大,所以读操作之前就上锁。数据库中的行锁、表锁、读锁、写锁,以及 synchronized、ReentrantLock 都是悲观锁。
- 乐观锁:数据发生并发冲突的概率比较小,所以读操作之前不上锁。等到写操作的时候,再判断数据在此期间是否被其他线程修改了。如果被其他线程修改了,就把数据重新读出来,重复该过程;如果没有修改,就写回去。判断数据是否被修改,同时写会新值,这两个操作要合成一个院子操作,也就是 CAS(Compare And Set)。
AtomicInteger 的实现就是典型的乐观锁,在 Mysql 和 Redis 中有类似的思路。
2.1.2 Unsafe 的 CAS 详解
CAS 乐观锁函数,就是封装的 Unsafe 类中的一个 native 函数,如下源码。
public final class Unsafe {
/**
* var1 对象
* var2 对象的成员变量(long类型的内存偏移量表示该位置)
* var4 变量的旧值
* var5 变量的新值
*/
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
/**
* 对象字段的内存偏移量计算方法
*/
public native long objectFieldOffset(Field var1);
}
2.1.3 自旋与阻塞
当一个线程拿不到锁的时候,有以下两种基本的等待策略。
- 阻塞:放弃 CPU,进入阻塞状态,等待后续被唤醒,再重新被操作系统调度。
- 自旋:不放弃 CPU,空转,不断重试。
单核 CPU,只能用阻塞,因为如果不放弃 CPU,那么其他线程无法运行,也就无法释放锁。
多 CPU 或者多核,自旋没有切换线程的开销,会很有用。
AtomicInteger 的实现就用的是“自旋”策略,如果拿不到锁,就会一直重试。
这两种策略并不是互斥的,可以结合使用,如果拿不到锁,先自旋几圈;如果自旋还拿不到锁,再阻塞,synchronized 关键字就是这样的实现策略。
2.2 AtomicBoolean 和 AtomicReference
基本同 AtomicInteger。
2.2.1 为什么需要 AtomicBoolean
因为要实现类似 CAS 比较和设值的原子操作。AtomicReference同理。
TIP
注意 volatile 不是原子性的,因此需要使用 CAS,多个线程示例代码见 com/study/java/concurrent/chapter02/VolatileTest.java
。
2.2.2 如何支持 boolean 和 double 类型
AtomicBoolean 类型调用 Unsafe 函数时,是将 Boolean 转为了 int 类型。
AtomicDouble 类型调用 Unsafe 函数时,是依赖 double 类型提供的一堆 double 类型和 long 类型互转的函数,后续 DoubleAdder 时会详细说明。
2.3 AtomicStampedReference 与 AtomicMarkableReference
2.3.1 ABA 问题与解决办法
CAS 都是基于“值”来做比较的。如果另外一个线程把变量的值从 A 改为 B,再从 B 改回到 A,那么尽管修改过两次,可是在当前线程做 CAS 操作的时候,却会因为值没变而任务数据没有被其他线程修改过,这就所谓的 ABA 问题。
ABA 问题解决办法:不仅要比较“值”,还要比较“版本号”。AtomicStampedReference 就是这么做的,以下是源码。
public class AtomicStampedReference{
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
// 值不变直接返回,值变化则复用原有的 cas,obj 里面存放了数据和版本号
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
}
2.3.2 为什么没有 AtomicStampedInteger 或 AtomicStampedLong
如 AtomicStampedReference.compareAndSet
源码,比较的是 Pair 里面有值和版本号。保证原子 CAS 操作就没有 AtomicStampedInteger 等了。
2.3.3 AtomicMarkableReference
AtomicMarkableReference 和 AtomicStampedReference 的区别是,前者的版本号是 int 型,后者的是 boolean 型。因此后者并不能完全避免 ABA 问题,只是降低了 ABA 发生的概率。
2.4 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater
2.4.1 为什么需要 AtomicXXXFieldUpdater
对已有的类,在不更改其源代码的情况下,实现对其成员变量的原子操作。示例代码见 com/study/java/concurrent/chapter02/AtomicIntegerFieldUpdaterTest.java
。
2.4.2 限制条件
查看 AtomicIntegerFieldUpdater
源码,限制了变量为 int 和 volatile 类型。其他 AtomicXXXFieldUpdater 也有类似的限制。
AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
final String fieldName,
final Class<?> caller) {
if (field.getType() != int.class)
throw new IllegalArgumentException("Must be integer type");
if (!Modifier.isVolatile(modifiers))
throw new IllegalArgumentException("Must be volatile type");
}
2.5 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray
concurrent 包提供了 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 三个数组元素的原子操作。注意是:仅针对数组中一个元素的原子操作而言。
2.5.1 使用方式
见下面源码。
public class AtomicIntegerArray{
public final boolean compareAndSet(int i, int expect, int update) {
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
}
2.5.2 实现原理
还是使用的 Unsafe,只是将第二个参数的偏移地址改为了 i * scale + base
的位移运算。
2.6 Striped64 与 LongAdder
从 JDK8 开始,针对 Long 型的原子操作,Java 又提供了 LongAdder、LongAccumulator;针对 Double 类型,Java 提供了 DoubleAdder、DoubleAccumulator。
TIP
为什么要提供 LongAdder(其他三个同理)?
2.6.1 LongAdder 原理
AtomicLong 内部是一个 volatile long 型变量,多个线程同时对一个变量进行 CAS 操作,在高并发场景下还是不够快。而 LongAdder 把一个变量拆成多份,变为多个变量(base 和多个 cell),并发度高时,平摊到这些 Cell 上,在最后取值的时候,再把 base 和这些 Cell 求 sum 运算,有些类似 ConcurrentHashMap 的分段锁的例子。
英文 Striped 意为“条带”,也就是分片。它是最终一致性,不是强一致性的。
2.6.2 最终一致性
在 sum 求和函数中,并没有对 cells 数组加锁,是有线程边求和,有线程修改数组里的值,即中途过程不是强一致性的,但是最终结果是一致的。
2.6.3 伪共享与缓存行填充
与 CPU 架构有关系,JDK 8 对应的注解是 @sun.misc.Contented
,LongAdder 使用其主要是为了不让 Cell[] 数组中相邻的元素落到同一个缓存行里。
2.6.4 LongAdder 核心实现
详见 LongAdder
源码,核心方法是 add、longAccumulate 等方法。
2.6.5 LongAccumulator
LongAccumulator 的原理和 LongAdder 类似,只是功能更强大,即多了个二元操作符。
2.6.6 DoubleAdder 与 DoubleAccumulator
DoubleAdder 是转为了 Long 类型,然后其他逻辑同 LongAdder 了。
DoubleAccumulator 也是 Striped64 的成员函数,和 longAccumulate 类似,也是多了 long 类型和 double 类型的互相转换。