首先我们看下面这一段代码,具体实现的功能是创建100个线程,每个线程对初始化为0的整数进行1万次+1操作,等待这100个线程跑完后输出这个整数的值,理论上这个整数最后输出的数值为100万。
import java.util.concurrent.CountDownLatch;public class AddTest {private volatile long i = 0;private void add() {i++;} private void println() {System.out.println(i);}private static final int THREAD_NUM = 100;public static void main(String str[]) throws Exception {CountDownLatch latch = new CountDownLatch(THREAD_NUM);AddTest addTest = new AddTest();for (int i = 0; i < THREAD_NUM; ++i) {new Thread(() -> {for (int j = 0; j != 10000; ++j) {addTest.add();}latch.countDown();}).start();}//等待100个线程跑完latch.await();addTest.println();}
}
实际上跑完后的结果为400131,显然和预期的100万不符合,多次跑还会结果不一样,但是结果都少于100万,为什么呢?
400131进程已结束,退出代码0
因为i++并不是一个原子操作,更新整数i其实包含一下3个步骤:
这些都作为不同的任务执行,因此,当一个线程正在读取值时,另一个线程可能正在递增它,而另一个线程可能正在将更新的值分配给字段引用。为了加深理解,我们引入最简单的C语言i++的代码,如下。
int main()
{int i = 0;i++;return 0;
}
利用gcc工具将C语言代码转为汇编语言。
gcc -S test.c -o test.s
得到test.s文件,其中i++的汇编执行指令如下,更新i的步骤有3个,首先读取当前值,其次执行必要的操作以获取更新的值,再将更新的值分配给字段引用。
movl -8(%rbp), %ecx
addl $1, %ecx
movl %ecx, -8(%rbp)
以上汇编是AT&T汇编语法,平时学习常见的是Intel汇编语法(mov,add等指令),具体这2个语法差异可以跳到这个链接看看,这里不展开介绍。https://sdasgup3.github.io/Intel_Vs_Att_format/
在之前的java代码中,我们预期为100万的结果执行流程应该如下,每个线程中的movl,addl,movl指令都是作为整体一起执行的,该线程执行完毕之后才到下一个线程执行。
| 指令 | |
|---|---|
| thread1-1 | movl -8(%rbp), %ecx |
| thread1-2 | addl $1, %ecx |
| thread1-3 | movl %ecx, -8(%rbp) |
| thread2-4 | movl -8(%rbp), %ecx |
| thread2-5 | addl $1, %ecx |
| thread2-6 | movl %ecx, -8(%rbp) |
但是实际上执行的执行流程有可能如下面的表格一样。
| thread1 | thread2 | |
|---|---|---|
| 1 | movl -8(%rbp), %ecx | |
| 2 | movl -8(%rbp), %ecx | |
| 3 | addl $1, %ecx | |
| 4 | movl %ecx, -8(%rbp) | |
| 5 | addl $1, %ecx | |
| 6 | movl %ecx, -8(%rbp) |
线程1先把当前的值(i=0)存到寄存器%ecx,然后轮到线程2把当前的值存到寄存器%ecx,这个时候2个线程获取到的当前值都是一样的,接着线程2把i+1的结果存到寄存器%ecx,再把寄存器%ecx的值取出来更新到i为1。然后轮到线程1接着进行addl和movl操作,由于线程1和2之前获取的当前值(i)一样,所以最终线程1也把i更新1。
为了解决以上并发问题,在JDK1.5之后,提供了CAS(全称Compare And Swap)操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。一句话概括该原理就是,根据内存地址获得期望值,更新的时候再次根据原来的内存地址获取值,如果和期望值一样,则进行更新,否则更新失败。
这里的内存地址是指对象地址+变量偏移值。
其中AtomicLong对象的实现就是基于CAS的,下面是核心的代码。
/*** var1为对象地址,var2为变量偏移值*根据对象地址和变量偏移值就能获取到对象参数的值,该值为期望值var6。* */public final long getAndAddLong(Object var1, long var2, long var4) {long var6;do {//获取期望值,这里已经移交给外部程序执行了,是本地方法。var6 = this.getLongVolatile(var1, var2);//compareAndSwapLong也是本地方法,移交给外部的程序执行,看看更新那时候是不是还是var6,如果是就更新为var6 + var4,否则不更新。} while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));return var6;}
代码中的while循环,代表循环直到更新成功为止,因为每个线程执行一定有自己的执行任务,只是这个时刻不应该进行更新,如果缺少while循环会导致自增结果不准,因此在高并发下,会存在很多无效循环(划重点)。
所以我们可以使用AtomicLong对象来解决这个问题,更新后的代码如下。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;public class AddTest {public AtomicLong i = new AtomicLong(0);private void add() {i.incrementAndGet();} private void println() {System.out.println(i.get());}private static final int THREAD_NUM = 100;public static void main(String str[]) throws Exception {CountDownLatch latch = new CountDownLatch(THREAD_NUM);AddTest addTest = new AddTest();for (int i = 0; i < THREAD_NUM; ++i) {new Thread(() -> {for (int j = 0; j != 10000; ++j) {addTest.add();}latch.countDown();}).start();}//等待100个线程跑完latch.await();addTest.println();}
}
1000000进程已结束,退出代码0