AtomicInteger 详细解读
一、原始数据并发写引发的问题
对于共享变量整数的加减操作,当出现并发的情况时,很容易造成线程不安全。
1、代码示例
public class Demo {
static int num = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 2; i++) {
Thread t = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
num++;
}
}
};
t.start();
list.add(t);
}
for (Thread thread : list) {
thread.join();
}
System.out.println(num);
}
}
运行结果如下图,结果并不是预期的20000,出现并发问题:
2、原因分析
-
全局变量num作为共享资源:程序运行时,每个线程都会读取
num
的当前值,然后对其进行加1操作。当多个线程并发执行时,它们可能几乎同时读到num
的同一个值,然后各自加1,最后写回。这样就导致了部分加1操作丢失,因为多个线程实际上是基于相同的初始值执行加法,而不是基于上一个线程已经更新后的值。 -
缺乏同步机制:在多线程编程中,为了防止这种数据竞争(data race)问题,通常需要使用锁或其他同步工具来确保同一时间只有一个线程能够访问和修改共享资源。
二、使用AtomicInteger改写
上述示例中,我们可以使用锁机制,来保证线程安全,但锁的粒度太大,会造成一定程度的性能损耗,推荐使用 AtomicInteger。
改写代码如下:
public class Demo {
// static int num = 0;
static AtomicInteger num = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int i = 0; i < 2; i++) {
Thread t = new Thread() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
// num++;
num.getAndIncrement();
}
}
};
t.start();
list.add(t);
}
for (Thread thread : list) {
thread.join();
}
System.out.println(num);
}
}
运行结果:
三、 AtomicInteger底层原理
源码追踪
AtomicInteger实现线程安全的自增,从底层原理出发,举例来说:
当线程A和线程B同时尝试对AtomicInteger
的值进行自增操作时,如果出现了并发情况,我们可以这样理解其底层原理及如何确保线程安全的:
-
初始状态:假设
AtomicInteger
的当前值为X
。 -
并发尝试:
- 线程A读取到当前值
X
。 - 几乎同时,线程B也读取到了相同的值
X
(因为两个线程读操作之间没有互斥,这是并发冲突的根源)。
- 线程A读取到当前值
-
CAS操作介入:
- 线程A操作:线程A尝试通过CAS操作将值从
X
更改为X+1
。这个操作包括三个步骤:检查当前值是否仍为期望值X
,如果是,则更新为X+1
;如果不是(即值已经被其他线程改变),则操作失败。 - 假设线程A的CAS操作成功,此时
AtomicInteger
的值变为X+1
。
- 线程A操作:线程A尝试通过CAS操作将值从
-
线程B的处理:
- 线程B随后尝试执行同样的CAS操作,但因为线程A已经将值更新为
X+1
,线程B发现当前值不再等于它期望的旧值X
,因此线程B的CAS操作失败。 - CAS操作失败后,线程B通常会重新读取当前值(现在已经是
X+1
),然后再次尝试CAS操作,这次期望值为X+1
,试图将其更新为X+2
。这个重试过程确保了线程B最终能够成功执行自增,并且不会丢失更新。
- 线程B随后尝试执行同样的CAS操作,但因为线程A已经将值更新为
通过这样的机制,AtomicInteger
确保了即使在高并发情况下,每个线程的自增操作都能够正确反映到最终结果中,从而实现了线程安全的自增。这个过程是非阻塞的,意味着线程不会因为等待锁而被挂起,提高了效率。
设计思想:
对于悲观锁,认为数据发生并发冲突的概率很大,读操作之前就上锁。synchronized关键字、ReentrantLock都是悲观锁的典型。
对于乐观锁,认为数据发生并发冲突的概率比较小,读操作之前不上锁。等到写操作的时候,再判断数据在此期间是否被其他线程修改了。如果被其他线程修改了,就把数据重新读出来,重复该过程; 如果没有被修改,就写回去。判断数据是否被修改,同时写回新值,这两个操作要合成一个原子操作, 也就是CAS ( Compare And Set )。 AtomicInteger的实现就是典型的乐观锁。
程修改了,就把数据重新读出来,重复该过程; 如果没有被修改,就写回去。判断数据是否被修改,同时写回新值,这两个操作要合成一个原子操作, 也就是CAS ( Compare And Set )。 AtomicInteger的实现就是典型的乐观锁。