什么是Atomic
Atomic
是原子性的意思,可以自动更新,用于原子增量计数器之类的应用程序。可以解决多线程环境递增的异议性问题。
怎么使用Atomic
AtomicInteger
Demo
public class Atomic {
AtomicInteger integer = new AtomicInteger(0);
@Test
public void testAtomicInteger() throws InterruptedException {
ExecutorService executor = new ThreadPoolExecutor(10, 50, 20, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
for (int i = 0; i < 100; i++) {
executor.execute(this::add);
TimeUnit.MILLISECONDS.sleep(1);
}
executor.shutdown();
}
public void add() {
for (int i = 0; i < 100; i++) {
System.out.println(integer.incrementAndGet());
}
}
}
运行结果
为什么要使用Atomic类
首先看一下不使用Atomic,以上Demo的运行结果会有什么问题
不使用Atomic
Demo
public class IntDemo {
int a = 1;
@Test
public void testInt() {
final int[] int1 = {0};
ExecutorService executor = new ThreadPoolExecutor(10, 50, 20, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
for (int i = 0; i < 10; i++) {
executor.execute(() -> {
for (int j = 0; j < 10; j++) {
int1[0]++;
System.out.println(int1[0]);
}
});
}
executor.shutdown();
}
}
运行结果
从运行结果来看,最终的结果不为100,可见在多线程的环境下,int自增操作并不是原子性的,这样就会导致一些问题。
为什么会出现这个问题?
1. 首先先了解一下,底层CPU、缓存已经主存之间的关系(intel)
- 主存(Main Memory):允许application,Java代码将会编译成字节码,然后由操作系统翻译成机器码,最后加载到内存中。
- L3 unified cache:L3级缓存,这一块的数据的是被封装的CPU的所有核心共享的,也是三级缓存中容量最大。
- L2 unified cache:L2级缓存,这一块的数据是被单个核心所独享的。
- L1 unified cache:L2级缓存,这一块的数据是被单个核心所独享的。
- ALU:CPU计算单元,负责数理逻辑计算。
- Register:寄存器单元,其中包含若干的寄存器,有PC(程序计数器)、IR(指令寄存器)、DR(数据寄存器)等。
以上程序的共享变量i
,没有进行锁、同步等数据一致性处理。变量i
会被从内存读取到CPU封装的L3缓存,如果在多线程环境下,会存在两个操作变量i
的线程同时跑在不同的核心上。
假设线程A->Core1,线程B->Core2,线程A从L3中读取到变量i
为0,线程B从L3中读取到变量i
也为0,线程A对变量i
++得到i=1
,同样线程B也对变量i
++得到i=1
,回写到内存时i=1
,但是实际上已经进行了两次的++
,故结果不正确。
特殊:四核八线程,对于一个核心,为了提高ALU的计算效率,会存在一个ALU单元对应两组Register,也就是所谓的超线程。此处的数据同步问题,博主还在学习。【如果有大佬了解,可以一起研究研究】
2. 再来了解一下,JMM(Java Memory Model)
- 线程A从主存中将变量读取到本地内存,仅仅是读取,后续还要进行加载。
- 将读取到的变量加载到本地工作内存,此时变量是主存中变量的副本。
- 将线程本地变量读取到执行引擎进行计算。
- 将第3步的计算结果刷回到线程本地工作内存中。
- 将本地工作内存写入到主存中,仅仅是写操作,后续还要进行存储。
- 将线程本地计算结果写回主存中。
- 线程B和线程B可以同时进行以上6步。
从以上的JMM模型的执行流程来看,当多线程的环境下,线程A和线程B可以同时读取主存中的变量,然后复制到本地工作内存中,接着计算,最后在将计算结果写回到主存中会存在数据不一致性。
Atomic原子类是如何保证并发环境数据一致性的?
上源码
在前文中,对于AtomicInteger递增是调用的incrementAndGet
从源码中可见,调用的是unsafe.getAndAddInt
,让我们来看看这个方法的实现。
从源码中可见,先是以getIntVolatile
的方法(native方法)获取变量的值,然后调用compareAndSwapInt
的方法(著名的CAS)进行数据的更改操作。
CAS原理(类似于一种乐观锁的概念)
CAS(compare and swap),比较并交换。
- 在并发环境下
- 读取:
- 每个修改共享变量的线程都可以读取并进行修改
- 写入:
- 如果此时的数据等于该线程一开始读取到的值,则将计算结果写入到主存中,
- 否则就重新读取最新值,然后进行重新计算,反复如此操作,直到写入成功。
- 针对于其中的ABA问题,可以使用一个version来解决,version可以是uuid或者时间戳等,具体可以取决于业务场景。
深入源码(以AtomicInteger为例)
类关系图
如图,可知AtomicInteger继承了Number
抽象类,此抽象类中定义了一些关于数字之间的一些基础操作,具体方法如下图。
成员变量
构造方法&set&get
- 构造方法有两个:一个无参构造器、一个有参构造器
-
get
:获取value
-
set
:设置value
-
lazySet
:异步设置value
加/减/设值操作
-
getAndSet(int newValue)
:将变量值设置成newValue,并返回旧值。 -
compareAndSet(int expect, int update)
:比较并设置值,只有当原有的value=expect时,才会将变量值设置成update,返回操作结果。 -
weakCompareAndSet
:与compareAndSet(int expect, int update)
类似,但是不强制原子性。 -
getAndIncrement()
:原子递增,返回旧值。 -
getAndDecrement()
:原子递减,返回旧值。 -
getAndAdd(int delta)
:原子增加delta,返回旧值。 -
incrementAndGet()
:原子递增,返回新值。 -
decrementAndGet()
:原子递减,返回新值。 -
addAndGet(int delta)
:原子增加delta,返回新值。
更新操作
getAndUpdate(IntUnaryOperator updateFunction)
此方法会先获取之前的值,然后将updateFunction
函数作用于之前的读取出来的值,最后将变量设置成计算得到的结果。此方法返回操作之前的旧值。
updateAndGet(IntUnaryOperator updateFunction)
此方法会先获取之前的值,然后将updateFunction
函数作用于之前的读取出来的值,最后将变量设置成计算得到的结果。此方法返回操作之后的新值。
累加操作
getAndAccumulate(int x, IntBinaryOperator accumulatorFunction)
此方法会先获取之前的值,然后将accumulatorFunction
函数作用于之前的读取出来的值(其实就是将之前的旧值+x),最后将变量设置成计算得到的结果。此方法返回操作之前的旧值。
accumulateAndGet(int x, IntBinaryOperator accumulatorFunction)
此方法会先获取之前的值,然后将accumulatorFunction
函数作用于之前的读取出来的值(其实就是将之前的旧值+x),最后将变量设置成计算得到的结果。此方法返回操作之后的新值。
总结:对于get在方法名称前面的话,那么会返回操作之前的旧值。如果子啊方法名称后面,那么会返回操作之后的新值