什么是原子性呢?
在数据库事务的ACID特性中就有原子性,它是指当前操作中包含的多个数据库事务操作,要么全部成功,要么全部失败,不允许存在部分成功、部分失败的情况。而在多线程中的原子性与数据库事务的原子性相同,它是指一个或多个指令操作在CPU执行过程中不允许被中断。
下面我们来演示一个多线程中出现原子性问题的例子。
public class AtomicExample {
volatile int i= 0;public void incr(){
i++;}
public static void main(String[] args) throws InterruptedException {
AtomicExample atomicExample = new AtomicExample();Thread[] threads=new Thread[2];
for (int j= θ;j<2;j++){
threads[j]=new Thread(() ->{ for (int k=0;k<10000;k++){
atomicExample.incr();}
});
threads[j].start();}
threads[0].join();//保证线程执行结束 threads[1].join( );
system.out.println(atomicExample.i);}
在上述代码中启动了两个线程,每个线程对成员变量i累加10000次,然后打印出累加后的结果。我们从结果中发现,原本期望的值是20000,但是打印出来的i值都是一个小于20000的数。和预期的结果不一致,导致这个现象产生的原因就是原子性问题,
从本质上说,原子性问题产生的原因有两个。
以后会在博客中讲述CPU时间片切换的原理,也就是当CPU不管因为何种原因处于空闲状态时,CPU会把自己的时间片分配给其他线程来处理,整体过程如图所示,CPU通过上下文切换来提升资源利用率。

i++指令的原子性
在Java 程序中,i++操作看起来是一个完整的不可分割的指令,但是实际上并不是这样的。我们通过javap -v命令来查看AtomicExample 类中incr()方法的字节码,运行结果如下。
- 0:aload_0
- 1: dup
- 2: getfield #2 // Field i:I
- 5: iconst_1
- 6:iadd
- 7: putfield #2 // Field i:I
- 10: return
可以发现,i++操作实际上是三个指令:getfield、iadd、putficld。
需要注意,这三个指令并不具备原子性,也就是说,CPU在执行的过程中会存在中断的情况,这种中断就会导致原子性问题。
如图所示,假设有两个线程同时对变量i进行修改,那么可能的执行过程如下:

除上述这种情况外,在多核CPU中,线程的并行执行也会导致原子性问题。如图所示。两个线程并行执行,同时从内存中将i加载到寄存器中并进行计算,最终导致i的结果小于我们的预期值

通过上述问题的分析,我们发现,多线程环境下线程的并行或切换导致最终执行结果不符合预期,解决问题的办法可以从两个方面考虑。
在Java中,synchronized关键字提供了这样一个功能,在incr()方法上增加synchronized关键字后,可以保证下面这段代码中i变量最终的输出结果必然是20000。
- public class AtomicExample {
- volatile int i= 0;
- public synchronized void incr(){
- i++;
- }
- public static void main(String[] args) throws InterruptedException {
- AtomicExample atomicExample = new AtomicExample();
- Thread[] threads=new Thread[2];
- for (int j= θ;j<2;j++){
- threads[j]=new Thread(() ->{
- for (int k=0;k<10000;k++){
- atomicExample.incr( );
- }
-
- });
- thread[i].start();
- }
- threads[0].join();
- threads[i].join();
- system.out.println(atomicExample.i);
- }
- }