在此之前准备两个例子
Demo1:
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal ; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count.get());
}
private static void add() {
count.incrementAndGet();
}
运行结果:5000
Demo2:
// 请求总数
public static int clientTotal = 5000;
// 同时并发执行的线程数
public static int threadTotal = 200;
public static int count = 0;
public static void main(String[] args) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore = new Semaphore(threadTotal);
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
for (int i = 0; i < clientTotal; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("exception", e);
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
log.info("count:{}", count);
}
private static void add() {
count++;
}
运行结果:4998
在这里可以看出Demo1和Demo2的区别仅仅在于count的类型,是基本类型int还是我们的Atomic包下的AtomicInteger。
而运行结果中,Demo1的结果是准确的,达到了我们预期的5000,而Demo2似乎有一些偏差,多次运行Demo2可看出输出结果大部分于5000以下并且结果不稳定。这是什么原因造成的,这还需要从jvm的结构模型讲起。
首先放一张来自于网络上的图
结合Demo2对这张图进行讲解。流程如下
1、在某一时刻类型为int的count=200在主内存中,主内存是对所有工作内存可见的。
2、此时有200个同时线程对这个count值进行并发操作,这时候每一个线程都把主内存中的count=200进行read,load到该线程的工作内存中,这是所有线程中的工作线程拷贝了一份count的副本,工作内存对彼此之间是不可见的
3、着每个java线程use其工作线程中count的副本到引擎中进行对count的加1操作,此时每个线程count副本的值是201
4、每个java线程再把count副本assign到工作线程中,然后在进行store,write到主内存中。
5、主内存经过了200个线程的依次write,得到的值为201,而我们预期的结果为400。
这很好的解释了为什么Demo2中会出现数据实际结果小于预期结果的原因。
那么到底哪里出错了呢?问题就在于工作内存不可见。你可能会说那这好办,我给int加一个volatile让数据被修改后立即对其他线程可见不就得了?
这里需要简单的解释一下volatile的可见是对于主内存可见,线程A操作完count时count=201立即刷新到主内存,可是线程B在刷新内存之前就已经被load到线程B的工作内存中了,这样还是不能解决问题。
所以volatile是不能够解决原子性的问题的
那么Demo1中的Atomic是如何解决原子性问题的呢?让我们按照Demo1的流程,点开源码看看
//截取本文所需要的关键方法
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
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;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public final int get() {
return value;
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
首先初始化:
在初始化我们count的时候实际上是把我们传入的初始值0赋予一个AtomicInteger的一个字段value。
这个value有啥用呢?在incrementAndGet中好像没有看见对value的操作。
我们看看初始化静态代码块做了什么事情,通过unsafe类直接对内存进行操作得到AtomicInteger 字节码中字段名为“value”的相对于内存地址的偏移量。可以理解为,这个操作是直接获取主内存中AtomicInteger类中value的内存地址,我们根据这个内存地址可以直接得到value的值。进行了这些初始化后我们来了几个线程对count进行incrementAndGet操作。
下面是调用unsafe的方法对传入值count进行+1操作,点进去看看。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
这里典型的就是一个CAS操作,对于非预期值会进行自旋直到符合预期值才会进行加1然后返回,这样可以保证我们想要加1的count永远都是一个最新的值,这样可以保证原子性操作。
让我们重新看看开始的那张图,看看AtomicInteger是怎么操作的
1、当某个时刻主内存中count的值为200,然后同时被200个线程read、load到工作内存中。
2、线程1打算对count进行加1操作,根据count值所在的地址直接得到一个的值count1=200,然后对于这个count1进行一次cas操作,看count1是否被其他线程改变过而导致内存地址对应的值不为预期的值,这里发现count1没有被修改,于是count1+1,刷新到主内存。
3、此刻线程2也打算对count进行加1,操作和步骤2类似,此时得到的count1也为200,就在这时线程1操作完成,目标地址对应的值变为201,然后线程2的count1=200进行cas操作,通过比较发现200!=201,无法进行加1操作,于是线程2再次循环重新通过内存地址获得count1的值为201,在进行cas发现符合条件进行加1操作。
4、其他线程同理,每当发现我要操作的count值不为预期值则进行自旋操作。