多线程/并发编程——CAS、Unsafe及Atomic
文章目录
在前面的文章中,我们已经知道了线程安全及实现机制: 多线程——线程安全及实现机制,在 Java 中主要从以下三个方面来实现线程安全
- 互斥同步:多线程——深入剖析 Synchronized、多线程\并发编程——ReentrantLock 详解
- 非阻塞同步:无锁CAS、Unsafe及 Atomic 原子类
- 无同步方案:ThreadLocal
在前面我们已经详细学习了基于互斥同步手段实现线程安全的两种方式——Synchronized、ReentrantLock
在本篇文章中,我们将要学习基于非阻塞同步的线程安全实现手段——无锁CAS、Unsafe及 Atomic
一、无锁CAS
我们在讨论无锁概念的时候,总是会关联起乐观派和悲观派。对于乐观派而言,它们认为事情总是会往好的方向发展,总是认为坏的情况发生的概率特别少,可以无所顾忌的做事;但是对于悲观派来说,它们如果不对事态发展进行控制,就一定会发生无法挽回的严重后果
这两种派系映射到并发编程中就如同加锁和无锁的策略,即加锁是一种悲观策略,无锁是一种乐观策略。对于加锁的并发程序来说,它们总是认为访问共享资源时一定会发生冲突,因此必须对每一次的数据操作都加锁;而对于无锁来说,它们总是认为每次对共享资源的访问不会发生冲突,线程可以不停执行,无需加锁,无需等待,一旦发生数据争用,则使用一种名为 CAS 的补救措施来保证线程安全性,因此 CAS 操作就是非互斥同步的乐观并发策略的关键,下面我们就来学习 CAS 的奇妙之处
1、无锁的执行者——CAS
CAS 是一条操作系统级别的原子指令,从硬件级别来保证 CAS 本身是线程安全的
CAS 的全称是 Compare And Swap,即比较与交换,其核心思想如下
执行函数:CAS(V, E, N)
CAS 函数包含三个操作数:
- V:要操作的变量的内存地址
- E:变量的旧的预期值
- N:准备设置的新值
CAS 指令执行时,当且仅当 V 符合 E 时,才会用新值 N 更新 V 的值。若 V 的值不符合 E ,说明在其进行修改之前,有其他线程修改了变量 V 的值,此时就不会执行更新操作,但可以重新读取该变量的值并再次尝试进行 CAS 修改此变量(比如一个需要自增的 int 变量阅读数,只要保证能够自增一次即可,不要求其在什么时候自增),也可以放弃操作,根据具体场景选择哪种方式
由于CAS操作属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作
基于这样的原理,CAS操作即使没有锁,同样也能知道其他线程对共享资源操作影响,并执行相应的处理措施。同时由于 CAS 操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁
2、CAS 的缺陷——ABA问题
虽然 CAS 的效率很高,但是 CAS 由于无锁,所有人都能访问共享资源,因此会带来另一个问题—— ABA
即线程 1 在被告知执行CAS操作:CAS(V, A, B) 的时候,线程 1 的时间片到了,CPU切换另一个线程 2 执行,另一个线程 2 执行的时候,把变量 V 从 A 修改成了 B,又从 B 修改成了 A,这样当切换回线程 1 的时候,对于线程 1 而言 V 好像是从来没有发生过改变一样
比如你和你女朋友分手了,隔了一年你女朋友又来找你复合,你女朋友还是你女朋友这个人,但是在这段时间,她是否交往过别的男朋友,性格脾气发生了什么变化,都是不知道的。换言之,你女朋友不一定还是你印象中的温暖的、青涩的女朋友了
一般而言 ABA 发生的概率很小,可能发生了也没什么问题,比如我们对某个数字做减法,不关心数字的变化过程,那么发生 ABA 问题也没啥关系。但是对于引用类型的数据,有些情况下还是会需要防止的,那么该如何解决 ABA 问题呢?我么可以使用带时间戳或者版本号的原子类来解决,最常用的是——AtomicStampedReference
二、CAS 的使用支持——Unsafe
在 Java 中并不是原生支持 CAS 操作,因为 Java 中 CAS 操作的执行依赖于 Unsafe 类
Unsafe 类存在于 sun.misc
包中,其内部方法操作可以向 C 语言的指针一样直接操作内存,仅从类的名字我们就可以直到这个类是不安全的,毕竟 Unsafe 拥有着类似 C 语言的指针,因此总是不应该首先使用 Unsafe 类,Java 官方团队也不建议直接使用 Unsafe 类,据说官方在后续版本打算去除 Unsafe 类
Unsafe类是在sun.misc包下,不属于Java标准,但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Hadoop、Kafka等。使用Unsafe可用来直接访问系统内存资源并进行自主管理,Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用
但是 Unsafe 被认为是Java中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等,有可能产生一定的安全风险,因此官方并不建议使用Unsafe
Unsafe 类的所有方法都是 native 修饰的,也就是说 Unsafe 类中的方法都是直接调用操作系统底层资源执行相应任务,关于 Unsafe 类的主要功能点如下:
//分配内存指定大小的内存
public native long allocateMemory(long bytes);
//根据给定的内存地址address设置重新分配指定大小的内存
public native long reallocateMemory(long address, long bytes);
//用于释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long address);
//将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//设置给定内存地址的值
public native void putAddress(long address, long x);
//获取指定内存地址的值
public native long getAddress(long address);
//设置给定内存地址的long值
public native void putLong(long address, long x);
//获取指定内存地址的long值
public native long getLong(long address);
//设置或获取指定内存的byte值
public native byte getByte(long address);
public native void putByte(long address, byte x);
//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同
//操作系统的内存页大小
public native int pageSize();
我们是不能直接使用 Unsafe 类的,它只提供给高级的类加载器 BootStrap 使用,普通用户调用将抛出异常,我们只能通过反射技术才能获取 Unsafe 类实例对象进行相关操作
三、Java 中 CAS 操作的使用——原子类 Atomic
通过前面的分析我们已经基本了解了无锁 CAS 的原理以及通过 Unsafe 类的支持使用 CAS 操作,但是我们并不能直接使用 Unsafe 的 CAS 操作。不过在 JDK 1.5 提供了 J.U.C.Atomic
包,其底层实现封装、调用了 Unsafe 类,在该包中提供了许多基于 CAS 实现的原子操作类,用法方便,简单高效,主要分为以下四种类型:
- 原子更新基本类型
- 原子更新引用
- 原子更新数组
- 原子更新属性
1、原子更新基本类型
原子更新基本类型主要包括3个类:
- AtomicBoolean:原子更新布尔类型
- AtomicInteger:原子更新整型
- AtomicLong:原子更新长整型
这3个类的实现原理和使用方式几乎是一样的,这里我们以AtomicInteger为例进行分析,AtomicInteger主要是针对int类型的数据执行原子操作,它提供了原子自增方法、原子自减方法以及原子赋值方法等,鉴于AtomicInteger的源码不多,我们直接看源码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 获取指针类Unsafe
private static final Unsafe unsafe = Unsafe.getUnsafe();
//下述变量value在AtomicInteger实例对象内的内存偏移量
private static final long valueOffset;
static {
try {
//通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移
//通过该偏移量valueOffset,unsafe类的内部方法可以获取到变量value对其进行取值或赋值操作
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//当前AtomicInteger封装的int变量value
private volatile int value;
public AtomicInteger(int initialValue) {
value = initialValue;
}
public AtomicInteger() {
}
//获取当前最新值,
public final int get() {
return value;
}
//设置当前值,具备volatile效果,方法用final修饰是为了更进一步的保证线程安全。
public final void set(int newValue) {
value = newValue;
}
//最终会设置成newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
//设置新值并获取旧值,底层调用的是CAS操作即unsafe.compareAndSwapInt()方法
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//如果当前值为expect,则设置为update(当前值指的是value变量)
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//当前值加1返回旧值,底层CAS操作
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//当前值减1,返回旧值,底层CAS操作
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
//当前值增加delta,返回旧值,底层CAS操作
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
//当前值加1,返回新值,底层CAS操作
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
//当前值减1,返回新值,底层CAS操作
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
//当前值增加delta,返回新值,底层CAS操作
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
//省略一些不常用的方法....
}
通过上述的分析,可以发现AtomicInteger原子类的内部几乎是基于前面分析过 Unsafe 类中的 CAS 相关操作的方法实现的,这也同时证明 AtomicInteger 是基于无锁实现的,这里重点分析自增操作实现过程,其他方法自增实现原理一样:
//当前值加1,返回新值,底层CAS操作
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
我们发现AtomicInteger类中所有自增或自减的方法都间接调用Unsafe类中的getAndAddInt()方法实现了CAS操作,从而保证了线程安全,关于getAndAddInt其实前面已分析过,它是Unsafe类中1.8新增的方法,源码如下
//Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
可看出getAndAddInt通过一个while循环不断的重试更新要设置的值,直到成功为止,调用的是Unsafe类中的compareAndSwapInt 方法,是一个CAS操作方法
我们知道一个 int 类型数据的自增并不是线程安全的,需要由加锁机制来保证线程安全,示例如下:
public int count = 0;
public synchronized void increase(){
for (int i = 0; i < 10000; i++){
count++;
}
}
由于 Synchronized 是基于互斥实现的线程安全,高并发情况下只有一个线程能进入方法,其他线程都会挂起,将会导致大量的线程挂起和唤醒的资源开销,此时使用原子类 AtomicInteger 效率将会提高很多,示例如下:
AtomicInteger count = new AtomicInteger(0);
public void increase(){
for (int i = 0; i < 10000; i++){
count.getAndIncrement();
}
}
使用 count.getAndIncrement() 方法来完成自增,没有使用互斥操作也能保证线程安全,效率要高很多
在上述示例中,使用原子类型 AtomicInteger 替换普通 int 类型执行自增的原子操作,保证了线程安全。至于AtomicBoolean 和 AtomicLong 的使用方式以及实现原理是一样,大家可以自行查阅源码
2、原子更新引用、数组、属性
原子更新引用
原子更新引用的实现是 AtomicReference 原子类,从源码看来,AtomicReference 与 AtomicInteger 的实现原理基本是一样的,最终执行的还是Unsafe类,关于AtomicReference的其他方法也是一样的,如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-29mRPjJX-1599379157062)(imges/20170705094719427.png)]
原子更新数组
原子更新数组指的是通过原子的方式,更改数组里面的某个元素,主要有以下三个实现类:
- AtomicIntegerArray:原子更新整数数组里的元素
- AtomicLongArray:原子更新长整数数组里的元素
- AtomicReferenceArray:原子更新引用类型数组里的元素
原子更新属性
如果我们只需要某个类里的某个字段,也就是说让普通的变量也享受原子操作,可以使用原子更新字段类,主要有以下三个实现类:
- AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段
这里的底层原理都是类比于 AtomicInteger ,由于这些类使用较少,面试也不太会问到,我们就不再详细展开讲了。AtomicInteger 是较常使用的类,重点理解 AtomicInteger 原子类即可,理解了AtomicInteger 也就大致理解了其余的原子类的实现
关联文章: