原子类也是 java 并发编程学习中不可或缺的一个部分了。今天我们来学习一下 JUC 并发包下原子类 AtomicInteger 。
下面我们看看该类的类图:
类图很简单,没有太多的继承实现关系,就先来看看 Number 类:
public abstract class Number implements java.io.Serializable {
public abstract int intValue();
public abstract long longValue();
public abstract float floatValue();
public abstract double doubleValue();
/**
* 提供最基本的方法转型
*/
public byte byteValue() {
return (byte)intValue();
}
/**
* 提供最基本的方法转型
*/
public short shortValue() {
return (short)intValue();
}
}
抽象类 Number 是所有基本类型的包装类的父类,提了了一些数据类型转型的方法。具体的以后写 Integer 之类源码分析讲解。
CAS简介:
可能很多同学都不知道 CAS 是什么,下面我们来进行简单讲解:
无锁操作常见的指令:
比较并交换(全称 Compare-and-Swap,简称 CAS)
(以下以 java 平台进行简述)
CAS 指令一般需要三个操作数,分别是变量的内存地址(用 V 表示)、旧的预期值(用 A 表示)、新值(用 B 表示)。CAS 指令执行时,只有当 V 的值符合 A 的时(如果是 int 类型,可以理解为 V == A 的情况下成立),处理器才会将新值 B 更新 V 的值,否则就是失败,不进行更新。但无论是否更新了 V 的值,都会返回 V 的旧的值。注:上述比较并交换的执行过程必须保证它 原子性(下面模拟实现使用了 synchronized 关键字保证)。
不同版本代码比较:
记得以前版本的 JDK 中查看 AtomicInteger 的自增方法 incrementAndGet 的代码如下所示:
/**
* Atomically increment by one the current value
* @return the update value
*/
public final int incrementAndGet() {
for (;;) {
int current= get();
int next = current + 1;
if (compareAndSet(current, next)) {
return next;
}
}
}
后来,不再是我们代码中循环调用了,而是使用 unsafe 类中 native 方法来实现了,代码如下:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
本质上没什么差别,都是进行自增操作,以前的是我们是自己实现一个死循环,不断尝试将当前值 +1 赋值给自己,失败后继续循环,直到赋值成功。
其实 CAS 指令在我们并发包下十分常见,很多并发容器都使用到 CAS,这里介绍一下 CAS 的一个缺陷 – ABA 问题。
ABA 问题:
下面我们来看一下什么是 ABA 问题。这里先举个小例子:有三个线程 A,B,C,当前值为 1,线程 A 来时读到的值是 1,准备进行下一步 CAS 操作,这时线程 B 已经将原来的值改为了 2,线程 C 也在进行 CAS 将值又变回了 1,。线程 A 在接下来判断值是否为预期 1 ,是则修改成功。可是有一个问题,CAS 是不知道它曾经改变过的,这就可能会带来一些问题。
CAS 在大部分情况下是不影响程序并发的正确性,比如我们将数据自增到 100,是不影响的。假设是数据要强一致性的业务,那就要避免这样使用了,老老实实用 互斥锁 保证正确性吧。
ABA 问题可以使用 AtomicStampedReference/AtomicMarkableReference 类来解决,后续文章将会对这两个类进行分析,看看是如何解决 ABA 问题的。
附:
JDK1.8 中,对 AtomicInteger 原子类添加了几个新方法,方法如下:
/**
* 下面两个方法是自定义增加多少
* IntUnaryOperator 是 1.8 新增接口,用于参数计算
* 增加多是写死的
*/
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
/**
* 下面两个方法是自定义增加多少
* IntBinaryOperator 是 1.8 新增接口,用于参数计算
* 与 IntBinaryOperator 区别是可以将传入数据进行相加
* 也就是自定义增加或减少多少
*/
public final int getAndAccumulate(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
public final int accumulateAndGet(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return next;
}
最后:
我们知道,CAS 操作涉及到我们的操作系统(简单理解:平台相关),也就是我们点进代码上有个 native 关键字修饰的方法。下面,我们自己来实现一个简单的 CAS 方法。
/**
* @author Gentle
* @date 2019/04/20 : 15:52
*/
public class MyAtomicInteger {
private volatile int count;
/**
* 自增
* @return 自增成功后的值
*/
public final int incrementAndGet() {
int pre, next;
for (; ; ) {
pre = get();
next = pre + 1;
if (compareAndSet(pre, next)) {
return next;
}
}
}
/**
* 比较并交换,一定要保证原子性,所以加了 synchronized 锁
* @param except 当前值
* @param newValue 新值
* @return 是否 CAS 成功
*/
private boolean compareAndSet(int except, int newValue) {
synchronized (this) {
if (get() == except) {
count = newValue;
return true;
}
}
System.out.println("CAS fail");
return false;
}
/**
* 获取值
* @return 当前值
*/
public int get() {
return count;
}
public static void main(String[] args) throws InterruptedException {
MyAtomicInteger myAtomicInteger = new MyAtomicInteger();
//闭锁
CountDownLatch countDownLatch = new CountDownLatch(10);
/**
* 模拟 10 线程 每条线程执行 5000 次
*/
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5000; j++){
myAtomicInteger.incrementAndGet();
}
//完成后计数器减 1
countDownLatch.countDown();
}).start();
}
//等待所有线程完成工作
countDownLatch.await();
//拿出当前值
System.out.println(myAtomicInteger.get());
}
}
总结:
上面方法中可能会涉及 unsafe 类,该类不是提供给用户使用的类,只有类加载器加载 Class 的时候才能访问到它。实际上该类是一个后门,用于调用操作系统的方法。如果我们非要使用的话,可以尝试使用反射来调用。
CAS 中最具价值的就是 ABA 问题了,理解了 ABA 问题,大致这篇就可以过了。其他 JDK1.8 提供的新特性暂时可以不了解。
有兴趣的同学可以关注公众号