Java原子类之AtomicInteger源码分析
一、为什么使用AtomicInteger
从a++说起为什么使用AtomicInteger?java并发机制中主要有三个特性需要去考虑,原子性、可见性和有序性。synchronized关键字可以保证可见性和有序性却无法保证原子性。而这个AtomicInteger的作用就是为了保证原子性。我们先看一个例子。
public class TestAtomicInteger {
//定义一个变量
private static volatile int a = 0;
public static void integerAdd() {
Thread[] threads = new Thread[5];
//定义5个线程池,每个线程增加10
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
System.out.println("线程:" + Thread.currentThread().getName() + ",结果:" + a++);
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
}
public static void main(String[] args) {
integerAdd();
}
}
在上面的例子中,定义了一个变量a。并且使用了5个线程分别去增加。为了保证可见性和有序性我们使用了volatile关键字对a进行修饰。如果我们第一次接触的话肯定会觉得5个线程,每个线程加10,最后结果一定是50呀。运行结果如下:
...
线程:Thread-0,结果:42
线程:Thread-2,结果:44
线程:Thread-4,结果:45
线程:Thread-1,结果:46
很明显,可能跟你想象的不一样。为什么会出现这个问题呢?这是因为变量a虽然保证了可见性和有序性,但是缺没有保证原子性。其原因我们可以来分析一下。
对于a++的操作,其实可以分解为3个步骤。
(1)从主存中读取a的值
(2)对a进行加1操作
(3)把a重新刷新到主存
volatile可以保证可见性,但是无法保证原子性。volatile修饰的变量a做++操作时,实际的指令包括三个(得到a的值,a+1,将a+1赋值给a),因此可能会出现多个线程交叉执行的结果。有三种方法可以解决
- 使用
synchronize修饰a++代码块
// 修饰的变量为a
synchronize(TestAtomicInteger.class) {
a++;
}
具体请参考
- 使用锁
ReentrantLock
ReentrantLock reentrantLock = new ReentrantLock();
private volatile a = 0;
public void addA() {
// 上锁
reentrantLock.lock();
try {
a++;
} finally {
// 保证锁的释放
reentrantLock.unlock();
}
}
- 使用
JUC包下的原子类AtomicInteger
public class TestAtomicInteger {
private static AtomicInteger b = new AtomicInteger();
public static void AtomicInteger() {
Thread[] threads = new Thread[5];
//定义5个线程池,每个线程增加10
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
System.out.println("线程:" + Thread.currentThread().getName() + ",结果:" + b.incrementAndGet());
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
}
}
二、AtomicInteger简介
AtomicInteger应该是atomic框架中用得最多的原子类了。顾名思义,AtomicInteger是Integer类型的线程安全原子类,可以在应用程序中以原子的方式更新int值。
2.1 创建AtomicInteger对象
先来看下AtomicInteger对象的创建。AtomicInteger提供了两个构造器,使用默认构造器时,内部int类型的 value值为0:
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
AtomicInteger类的内部并不复杂,所有的操作都针对内部的int值——value,并通过Unsafe类来实现线程安全的CAS操作。
2.2 AtomicInteger的使用
上面案例中使用了AtomicInteger的incrementAndGet方法,以原子的操作对int值进行自增,该段程序执行的最终结果为50(5个线程,每个线程对AtomicInteger增加10),如果不使用AtomicInteger,使用原始的int或Integer,最终结果值可能会小于50(并发时读到了过时的数据或存在值覆盖的问题)。
我们来看下incrementAndGet内部:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
内部调用了Unsafe类的getAndAddInt方法,以原子方式将value值增加1,然后返回增加前的原始值。
注意,上述是JDK1.8的实现,在JDK1.8之前,上述方法采用了自旋+CAS操作的方式:
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
2.3 AtomicInteger的特殊方法说明
AtomicInteger中有一个比较特殊的方法——lazySet:
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
lazySet方法是set方法的不可见版本。什么意思呢?
我们知道通过volatile修饰的变量,可以保证在多处理器环境下的“可见性”。也就是说当一个线程修改一个共享变量时,其它线程能立即读到这个修改的值。volatile的实现最终是加了内存屏障:
- 保证写volatile变量会强制把CPU写缓存区的数据刷新到内存
- 读volatile变量时,使缓存失效,强制从内存中读取最新的值
- 由于内存屏障的存在,volatile变量还能阻止重排序
lazySet内部调用了Unsafe类的putOrderedInt方法,通过该方法对共享变量值的改变,不一定能被其他线程立即看到。也就是说以普通变量的操作方式来写变量。
为什么会有这种奇怪方法?什么情况下需要使用lazySet呢?
考虑下面这样一个场景:
private AtomicInteger ai = new AtomicInteger();
lock.lock();
try{
// ai.set(1);
} finally {
lock.unlock();
}
由于锁的存在:
- lock() 方法获取锁时,和
volatile变量的读操作一样,会强制使CPU缓存失效,强制从内存读取变量。 - unlock() 方法释放锁时,和
volatile变量的写操作一样,会强制刷新CPU写缓冲区,把缓存数据写到主内存
所以,上述ai.set(1)可以用ai.lazySet(1)方法替换:由锁来保证共享变量的可见性,以设置普通变量的方式来修改共享变量,减少不必要的内存屏障,从而提高程序执行的效率。
三、接口说明
| 方法声明 | 描述 |
|---|---|
| int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) | 使用IntBinaryOperator 对当前值和x进行计算,并更新当前值,返回计算后的新值 |
| int addAndGet(int delta) | 以原子方式将给定值与当前值相加,返回相加后的新值 |
| boolean compareAndSet(int expect, int update) | 如果当前值 == expect,则以原子方式将该值设置为给定的更新值(update) |
| int decrementAndGet() | 以原子方式将当前值减 1,返回新值 |
| int get() | 获取当前值 |
| int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) | 使用IntBinaryOperator 对当前值和x进行计算,并更新当前值,返回计算前的旧值 |
| int getAndAdd(int delta) | 以原子方式将给定值与当前值相加,返回旧值 |
| int getAndDecrement() | 以原子方式将当前值减 1,返回旧值 |
| int getAndIncrement() | 以原子方式将当前值加 1,返回旧值 |
| int getAndSet(int newValue) | 以原子方式设置为给定值,并返回旧值 |
| int getAndUpdate(IntUnaryOperator updateFunction) | 使用IntBinaryOperator 对当前值进行计算,并更新当前值,返回计算前的旧值 |
| int incrementAndGet() | 以原子方式将当前值加 1,返回新值 |
| void lazySet(int newValue) | 设置为给定值,但不保证值的改变被其他线程立即看到 |
| void set(int newValue) | 设置为给定值 |
| int updateAndGet(IntUnaryOperator updateFunction) | 使用IntBinaryOperator 对当前值进行计算,并更新当前值,返回计算后的新值 |
| boolean weakCompareAndSet(int expect, int update) | weakCompareAndSet无法保证除操作目标外的其他变量的执行顺序( 编译器和处理器为了优化程序性能而对指令序列进行重新排序 ),同时也无法保证这些变量的可见性。 |
四、源码
ActomicInteger的内部属性可以看到,它是依赖Unsafe的一些底层能力,进行底层操作,以volatile的value字段,记录数值,以保证可见性。以下是AtomicInteger的部分源码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
......
}
Unsafe会利用value字段的内存地址偏移,直接完成操作。
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
getAndIncrement是需要明确返回值的,因此getAndAddInt实现是需要失败重试,最后拿到返回值的。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
CompareAndset这样的直接返回Boolean值,不需要失败重试。
五、其它原子类
与AtomicInteger类似的原子类还有AtomicBoolean和AtomicLong,底层都是通过Unsafe类做CAS操作,来原子的更新状态值。可以参考Oracle官方文档:https://docs.oracle.com/javase/8/docs/api
参考文章
- AtomicInteger底层实现原理