一、前言
1、原子类为了解决什么问题?
为了解决并发场景下无锁的方式保证单一变量的数据一致性
2、什么情况下存在并发问题?
多个线程同时读写同一个共享数据时存在多线程并发问题
二、非原子计算
大家应该都知道, 类似于代码中的 i++ 操作, 虽然是一行, 但是执行时候是分为三步的
- 从主存获取变量 i
- 变量i值+1
- 新增后变量i值写回主存
写个小程序来更好的理解, 不论我们怎么运行下方程序,99% 以上概率不会到达 100000000
static int NUM = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
NUM++;
}
}).start();
}
Thread.sleep(2000);
System.out.println(NUM);
/**
* 99149419
*/
}
那么如何俩解决这个问题呢?
1.可以使用 JDK 自带的synchronized, 通过互斥锁的方式同步执行 NUM++ 这个代码块
static int NUM = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
synchronized (Object.class) {
NUM++;
}
}
}).start();
}
Thread.sleep(2000);
System.out.println(NUM);
/**
* 100000000
*/
}
2.如果不使用锁来解决上面的非原子自增问题, 可以这么来写 Atomic 开头的类库
static AtomicInteger NUM = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
// 🚩 重点哦, 自增并获取新值
NUM.incrementAndGet();
}
}).start();
}
Thread.sleep(2000);
System.out.println(NUM);
/**
*100000000
*/
}
三、AtomicInteger
1、是什么?
- AtomicInteger 是 JDK 并发包下提供的操作 Integer 类型原子类, 通过调用底层Unsafe 的 CAS 相关方法实现原子操作
- 基于乐观锁的思想实现的一种无锁化原子操作,保障了多线程情况下单一变量的线程安全问题
2、有什么优点?
尽管 synchronized 经过升级后, 性能有了大幅度提升, 但在一般并发场景下, CAS 无锁算法性能更高一些
AtmoicInteger使用硬件级别的指令 CAS 来更新计数器的值, 机器直接支持的指令,这样可以避免加锁,比如像互斥锁 synchronized 在并发比较严重情况下, 会将锁升级到重量级锁,唤醒与阻塞线程时会有一个用户态到内核态的一个转变, 而转换状态是需要消耗很多时间的
四、结构分析
- AtomicInteger 有两个构造方法, 分别是一个无参构造及有参构造
无参构造的 value 就是 int 的默认值 0
有参构造会将 value 赋值
public AtomicInteger() { }
public AtomicInteger(int initialValue) {
value = initialValue;
}
AtomicInteger 有三个重要的变量, 分别是:
- Unsafe:可以理解它对于 Java 而言, 是一个 "BUG" 的存在,在 AtomicInteger 里的最大作用就是直接操作内存进行值替换
- value:使用 int 类型存储 AtomicInteger 计算的值,通过 volatile 进行修饰,提供了内存可见性及防止指令重排序
- valueOffset:value 的内存偏移量
// 获取Unsafe实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
// 静态代码块,在类加载时运行
static {
try {
// 获取 value 的内存偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
private volatile int value;
常用API:
// 获取当前 value 值
public final int get();
// 取当前的值, 并设置新的值
public final int getAndSet(int newValue);
// 获取当前的值, 并加上预期的值
public final int getAndAdd(int delta);
// 获取当前值, 并进行自增1
public final int getAndIncrement();
// 获取当前值, 并进行自减1
public final int getAndDecrement();
我们拿getAndIncrement()来举一个例子看一下,其他的大同小异
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* unsafe.getAndAddInt
*
* @param var1 AtomicInteger 对象
* @param var2 value 内存偏移量
* @param var4 增加的值, 比如在原有值上 + 1
* @return
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 内存中 value 最新值
var5 = this.getIntVolatile(var1, var2);
} while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
我们来看一下底层的执行步骤:
1.、根据 AtomicInteger 对象 以及 value 内存偏移量获取对应 value 最新值
2、通过 compareAndSwapInt(...) 将内存中的值(var5)更改为期望的值(var5+var4), 不存在多线程竞争成功修改返回 True 结束循环, Flase 继续执行循环
compareAndSwapInt(....)源码解析:
/**
* 比较 var1 的 var2 内存偏移量处的值是否和 var4 相等, 相等则更新为 var5
*
* @param var1 AtomicInteger 对象
* @param var2 value 内存偏移量
* @param var4 value 原本的值
* @param var5 期望将 value 设置的值
* @return
*/
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
由于是 native 关键字修饰, 我们无法查看其源码, 说明一下方法思路
1、通过 var1(AtomicInteger) 获取到 var2 (内存偏移量) 的 value 值
2、将 value(内存中值) 与 var4(线程内获取的value值) 进行比较
3、如果相等将 var5(期望值) 设置为内存中新的 value 并返回 True
4、不相等返回 False 继续尝试执行循环
五、不足之处
- CAS 虽然能够实现无锁编程, 在一般情况下对性能做出了提升, 但是并不是没有局限性或缺点
- 在高并发情况下, 自旋 CAS 如果长时间不成功, 会给 CPU 带来非常大的执行开销
- CAS 需要在操作值的时候检查下值有没有发生变化, 如果没有发生变化则更新
- 但是如果一个值原来是A, 变成了B, 又变成了A, 那么使用 CAS 进行检查时会发现它的值没有发生变化, 但是实际上却变化了
如果感兴趣的小伙伴可以去看下 JUCA 原子包下的 AtomicStampedReference
六、结束
博主很懒!今天就先到这把,如何规避ABA问题等,下回分解把,其实也很简单,先来说下解决 ABA 的思路吧, 也就是 AtomicStampedReference 的原理:内部维护了一个 Pair 对象, 存储了 value 值和一个版本号, 每次更新除了 value 值还会更新版本号
有引用其他大佬们的一些东西,如有侵权请联系本人
作者:作者很懒不想打字