前言
写这篇博文的原因,是因为我今天在看阿里的规范手册的时候(记录在了这里:【小家java】《阿里巴巴 Java开发手册》读后感—拥抱规范,远离伤害),发现了有一句规范是这么写的:
如果是count++操作,使用如下类实现: AtomicInteger count = new AtomicInteger(); count.addAndGet(1);如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。
这里面提到了Atomic系列来进行原子操作。之前我在各个地方使用过AtomicInteger很多次,但一直没有做一个系统性的了解和做笔记。因此本此恰借此机会,把这块的知识点好好梳理一下, 并希望在学习的过程中解决掉问题
简单例子铺垫
废话不多说,展示代码:
public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); Count count = new Count(); // 100个线程对共享变量进行加1 for (int i = 0; i < 100; i++) { service.execute(() -> count.increase()); } // 等待上述的线程执行完 和三个方法的区别 这里不做概述,反正都能关闭 service.shutdown(); //service.shutdownNow(); service.awaitTermination(2, TimeUnit.SECONDS); service.shutdown(); System.out.println(count.getCount()); } //计数类 private static class Count { // 共享变量 private Integer count = 0; public Integer getCount() { return count; } public void increase() { count++; } }
你们猜猜执行的结果会是多少?是100吗?
我相信稍微基础好一点的,或者说遇见过类似问题的,答案都是No吧。我执行了多次,结果是不确定的:29、69、48、99都有。。。
(备注:类似的方案,有时候可以通过volatile关键字,此处不对此关键字做过多的讨论,它是一种内存可见性方案,并不是真正意义上的锁哟)
根据结果我们得知:上面的代码是线程不安全的!如果线程安全的代码,多次执行的结果是一致的!
原因分析
什么上述的结果不确定呢?我们可以发现问题所在:**count++并不是原子操作。**因为count++需要经过读取-修改-写入三个步骤。举个例子还原一下真相:
1.如果某一个时刻:线程A读到count的值是10,线程B读到count的值也是10
2.线程A对count++,此时count的值为11
3.线程B对count++,此时count的值也是11(因为线程B读到的count是10)
4.所以到这里应该知道为啥我们的结果是不确定了吧。
怎么破?
要得出正确的结果100,怎么办?
synchronized
在increase()加synchronized锁就好了:
public synchronized void increase() { count++; }
这样子无论执行多少次,得出的都是100。这个对于只要求解决问题,但不在乎效率,不想深挖的人,肯定已经ok了。但是我们仅仅只是对于这么简单的一个++,就动用这么"强悍的"Synchronized未免有点太小题大作了。
Synchronized悲观锁,是独占的,意味着如果有别的线程在执行,当前线程只能是等待!
那么接下来针对我们频繁碰到这个问题,JDK5提供的原子操作就要登场了
Atomic原子操作
在JDK1.5+的版本中,Doug Lea和他的团队还为我们提供了一套用于保证线程安全的原子操作。
JDK1.5的版本中为我们提供了java.util.concurrent.atomic原子操作包。所谓“原子”操作,是指一组不可分割的操作:操作者对目标对象进行操作时,要么完成所有操作后其他操作者才能操作;要么这个操作者不能进行任何操作。
有了他们,我们就很好解决上面遇到的问题了,只需要采用AtomicInteger稍加改动就OK了~~
public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); AtomicInteger count = new AtomicInteger(); // 100个线程对共享变量进行加1 for (int i = 0; i < 100; i++) { service.execute(() -> count.incrementAndGet()); } // 等待上述的线程执行完 和三个方法的区别 这里不做概述,反正都能关闭 service.shutdown(); //service.shutdownNow(); service.awaitTermination(2, TimeUnit.SECONDS); service.shutdown(); System.out.println(count.get()); }
改用Atomic来执行后,我们发现不管执行多少次,结果都是正确的100;
JDK1.5以后这种轻量级的解决方案不再推荐使用synchronized,而使用Atomic代替,因为效率更高
源码分析
AotmicInteger其实就是对int的包装,然后里面内部使用CAS算法来保证操作的原子性
public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; }
可以看到,内部主要依赖于unsafe提供的CAS算法来实现的,因此我们很有必要了解一下,到底什么是CAS呢?
CAS解释
先概念走一波
比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。
从定义中我们可以总结出CAS有三个操作数:
1.内存值V
2.旧的预期值A
3.要修改的新值B
为了方便大家理解也为了我记忆深刻点,我特意自己尝试着画了一些图解(下同):
可以发现CAS有两种情况:
如果内存值V和我们的预期值A相等,则将内存值修改为B,操作成功!
如果内存值V和我们的预期值A不相等,一般也有两种情况:
1、重试(自旋) 2、什么都不做
CAS失败重试(自旋)
上面的例子,我们启动的100个线程,实质上都对结果进行了+1。但是可以想象到,肯定存在多个线程同一时刻同时想+1的情况,因此可见下图:
虽然这幅图只画了两个线程的情况,举一反三,任意多个线程的情况都是一样的处理方式。