由于 JVM 的 synchronized 重量级锁涉及到操作系统(如 Linux) 内核态下的互斥锁(Mutex)的使用, 其线程阻塞和唤醒都涉及到进程在用户态和到内核态频繁切换, 导致重量级锁开销大、性能低。 而 JVM 的 synchronized 轻量级锁使用 CAS(Compare and Swap) 进行自旋抢锁, CAS 是CPU 指令级的原子操作, 并处于用户态下, 所以 JVM 轻量级锁开销较小。
什么是 CAS
CAS 的英文全称为 Compare and Swap,翻译成中文为“比较并交换” 。 JDK5 所增加的 JUC(java.util.concurrent)并发包, 对操作系统的底层 CAS 原子操作进行了封装,为上层 Java 程序提供了 CAS 操作的 API。
Unsafe 类中的 CAS 方法
Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全的底层操作,如直接访问系统内存资源、自主管理内存资源等, Unsafe 大量的方法都是 native 方法,基于 C++语言实现, 这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。
Unsafe 类的全限定名为 sun.misc.Unsafe,从名字中我们可以看出来这个类对普通程序员来说是“危险”的,一般的应用开发都不会涉及到此类, Java 官方也不建议直接在应用程序中使用。
操作系统层面的 CAS 是一条 CPU 的原子指令(cmpxchg 指令),正是由于该指令具备了原子性,所以使用 CAS 操作数据时不会造成数据不一致问题, Unsafe 提供的 CAS 方法,直接通过native 方式(封装 C++代码)调用了底层的 CPU 指令 cmpxchg。
完成 Java 应用层的 CAS 操作,主要涉及到的 Unsafe 方法调用,具体如下:
(1) 获取 Unsafe 实例。
(2) 调用 Unsafe 提供的 CAS 方法, 这些方法主要封装了底层 CPU 的 CAS 原子操作。
(3)调用 Unsafe 提供的字段偏移量方法, 这些方法用于获取对象中的字段(属性)偏移量,
此偏移量值需要作为参数提供给 CAS 操作。
获取 Unsafe 实例
Unsafe 类是一个“final”修饰的不允许继承的最终类,而且其构造函数是 private 类型的方法,具体的源码如下:
public final class Unsafe {
private static final Unsafe theUnsafe;
public static final int INVALID_FIELD_OFFSET = -1;
private static native void registerNatives();
// 构造函数是 private 的,不允许外部实例化
private Unsafe() {
}
...
}
调用 Unsafe 提供的 CAS 方法
Unsafe 提供的 CAS 方法,主要如下:
/**
* 定义在 Unsafe 类中的三个 “比较并交换”原子方法
* @param o 需要操作的字段所处的对象
* @param offset 需要操作的字段的偏移量(相对的,相对于对象头)
* @param expected 期望值(旧的值)
* @param update 更新值(新的值)
* @return true 更新成功 | false 更新失败
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
Unsafe 提供的 CAS 方法包含四个操作数——字段所处的对象、字段内存位置、预期原值及新值。 在执行 Unsafe 的 CAS 方法的时候, 这些方法首先将内存位置的值与预期值(旧的值) 比较,如果相匹配,那么处理器会自动将该内存位置的值更新为新值, 并返回 true; 如果不相匹配,处理器不做任何操作,并返回 false。
Unsafe 的 CAS 操作会将第一个参数(对象的指针、地址)与第二个参数(字段偏移量)组合在一起,计算出最终的内存操作地址。
调用 Unsafe 提供的偏移量相关
Unsafe 提供的获取字段(属性) 偏移量的相关操作,主要如下:
/**
* 定义在 Unsafe 类中的几个 获取字段偏移量的方法
* @param o 需要操作字段的反射
* @return 字段的偏移量
*/
public native long staticFieldOffset(Field field);
public native long objectFieldOffset(Field field);
staticFieldOffset 方法用于获取静态属性 Field 在 Class 对象中的偏移量, 在 CAS 操作静态属性时,会用到这个偏移量。 objectFieldOffset 方法用于获取非静态 Field(非静态属性) 在 Object 实例中的偏移量, 在 CAS 操作对象的非静态属性时, 会用到这个偏移量。
一个获取非静态 Field(非静态属性)在 Object 实例中的偏移量的示例,代码如下:
public static class Obj {
private static final Unsafe unsafe = getUnsafe();
private static long stateOffset;
private volatile int state;
public static Unsafe getUnsafe() {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
return (Unsafe) theUnsafe.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
static {
try {
stateOffset = unsafe.objectFieldOffset(Obj.class.getDeclaredField("state"));
} catch (Exception ex) {
throw new Error(ex);
}
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
public static void main(String[] args){
Obj obj = new Obj();
boolean b = obj.compareAndSet(0, 10);
System.out.println("b:"+b+" , state:"+obj.getState());
}
}
使用 CAS 进行“无锁编程”
CAS 是一种无锁算法,该算法关键依赖两个值——期望值(就值)和新值,底层 CPU 利用原子操作,判断内存原值与期望值是否相等, 如果相等则给内存地址赋新值,否则不做任何操作。
使用 CAS 进行“无锁编程” (Lock Free) 的步骤大致如下:
(1)获得字段的期望值(oldValue) 。
(2) 计算出需要替换的新值(newValue) 。
(3) 通过 CAS 将新值(newValue)放在字段的内存地址上,如果 CAS 失败则重复第 1 步到第 2 步,一直到 CAS 成功, 这种重复俗称 CAS 自旋。
使用 CAS 进行“无锁编程”的伪代码,大致如下:
do{
获取字段的期望值(oldValue)
计算需要替换的新值(newValue)
}while(!CAS(内存地址, oldValue, newValue))
下面用一个简单的例子,对以上伪代码进行举例说明。
假如某个内存地址(某对象的属性)的值为 100,现在有两个线程(线程 A、 线程 B)使用CAS 无锁编程对该内存地址进行更新,线程 A 欲将其值更新为 200,线程 B 欲将其值更新为 300,具体如图 3-1 所示。
由于线程是并发执行,谁都有可能先执行。 但是 CAS 是原子操作,对同一个内存地址的 CAS操作在同一时刻只能执行一个。所以在这个例子中,要么线程 A 先执行,要么线程 B 先执行。假设线程 A 的 CAS(100,200)执行在前,由于内存地址的旧值 100 与该 CAS 的期望值 100 相等,所以线程 A 会操作成功,内存地址的值被更新为 200。
线程 A 执行成功 CAS(100,200) 之后,内存地址的值具体如图 3-2 所示。
接下来执行线程 B 的 CAS(100,300)操作,此时内存地址的值为 200,不等于 CAS 的期望值 100,线程 B 操作失败。线程 B 只能自旋,开始新的循环,这一轮循环首先获取到内存地址的值 200,然后进行 CAS(200,300)操作,这一次内存地址的值与 CAS 的预期值(oldValue)相等,线程 B 操作成功。
当 CAS 进行内存地址的值与预期值比较时,如果相等,则证明内存地址的值没有被修改, 可以替换成新值,然后继续往下运行;如果不相等,说明明内存地址的值已经被修改,放弃替换操作,然后重新自旋。当并发修改的线程少, 冲突出现的机会少时, 自旋的次数也会很少, CAS 性能会很高;当并发修改的线程多,冲突出现的机会高时,自旋的次数也会很多, CAS 性能会大大降低。 所以,提升 CAS 无锁编程的效率,关键在于减少冲突的机会。
JUC原子类
在多线程并发执行时,诸如“++” 或“–”类的运算不具备原子性的, 不是线程安全的操作。 通常情况下,大家会使用 synchronized 将这些线程不安全的操作变成同步操作, 但是这样会降低并发程序的性能。所以, JDK 为这些类型不安全的操作, 提供了一些原子类, 与 synchronized 同步机制相比, JDK 原子类基于 CAS 轻量级原子操作实现, 使得程序运行效率变得更高。
JUC 中的 Atomic 原子操作包
Atomic 操作翻译成中文, 是指一个不可中断的操作,即使在多个线程一起执行 Atomic 类型操作的时候,一个操作一旦开始,就不会被其他线程中断。所谓Atomic 类,指的是具有原子操作特征的类。
JUC 并发包中原子类的位置
JUC并发包中原子类,都存放在java.util.concurrent.atomic 类路径下,具体如图
根据操作的目标数据类型,可以将JUC包中的原子类分为4类:
- 基本原子类
- 数组原子类
- 原子引用类型
- 字段更新原子类
基本原子类
基本原子类的功能,是通过原子方式更新Java基础类型变量的值。 基本原子类主要包括了一下三个:
- AtomicInteger : 整型原子类
- AtomicLong : 长整型原子类
- AtomicBoolean : 布尔型原子类
数组原子类
数组原子类的功能,是通过原子方式更新数组里的某个元素的值。 数组原子类主要包括一下三个:
- AtomicIntegerArray : 整型数组原子类
- AtomicLongArray : 长整型数组原子类
- AtomicReferenceArray : 引用类型数组原子类
引用原子类
引用原子类主要包括了以下三个:
- AtomicReference : 引用类型原子类
- AtomicMarkableReference : 带有更新标记位的原子引用类型
- AtomicStampedReference : 带有更新版本号的原子引用类型
AtomicMarkableReference类将boolean 标记与引用关联起来,可以解决使用AtomicBoolean 进行原子更新时可能出现的ABA问题
AtomicStampedReference 类将整数值与引用关联起来, 可以解决使用AtomicInteger 进行原子更新时出现的ABA问题
字段更新原子类型
字段更新原子类
字段更新原子类主要包括了以下三个:
- AtomicIntegerFieldUpdater : 原子更新整型字段的更新器
- AtomicLongFieldUpdater : 原子更新长整型字段的更新器
- AtomicReferenceFieldUpdater : 原子更新引用类型里的字段
##### AtomicInteger 线程安全原理
基础原子类(以 AtomicInteger 为例 )主要通过 CAS 自旋 + volatile 相结合的方案实现,既保障了变量操作的线程安全性,又避免了 synchronized 重量级锁的高开销, 使得 Java 程序的执行效率大为提升。
注:CAS 用于保障变量操作的原子性, volatile 关键字用于保障变量的可见性,二者常常结合使用。
下面以 AtomicInteger 源码为例,分析一下原子类的 CAS 自旋 + volatile 相结合的实现方案。AtomicInteger 源码的具体的代码如下:
public class AtomicInteger extends Number implements java.io.Serializable {
//Unsafe 类实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
//内部 value 值,使用 volatile 保证线程可见性
private volatile int value;
//value 属性值的地址偏移量
private static final long valueOffset;
static {
try {
//计算 value 属性值的地址偏移量
valueOffset = unsafe.objectFieldOffset(
AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//初始化
public AtomicInteger(int initialValue) {
value = initialValue;
}
//获取当前 value 值
public final int get() {
return value;
}
//方法:返回旧值并赋新值
public final int getAndSet(int newValue) {
for (;;) {//自旋
int current = get();//获取旧值
//以 CAS 方式赋值,直到成功返回
if (compareAndSet(current, newValue))
return current;
}
}
//方法: 封装底层的 CAS 操作, 对比 expect(期望值)与 value,不同返回 false
//expect 与 value 相同, 则将新值赋给 value, 并返回 true
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//方法: 安全自增 i++
public final int getAndIncrement() {
for (;;) { //自旋
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
//方法:自定义增量数
public final int getAndAdd(int delta) {
for (;;) { //自旋
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return current;
}
}
//方法:类似++I,返回自增后的值
public final int incrementAndGet() {
for (;;) { //自旋
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
//方法:返回加上 delta 后的值
public final int addAndGet(int delta) {
for (;;) { //自旋
int current = get();
int next = current + delta;
if (compareAndSet(current, next))
return next;
}
}
//...省略其他源码
}
AtomicInteger 源码中的主要方法,都是通过 CAS 自旋实现的。 CAS 自旋的主要操作为: 如果一次 CAS 操作失败,则获取最新的 value 值后,再次进行 CAS 操作,直到成功。
另外, AtomicInteger 所包装的内部 value 成员, 是一个使用关键字 volatile 修饰的内部成员。关键字 volatile 的原理比较复杂,简单的说,该关键字可以保证任何线程在任何时刻总能拿到该变量的最新值,其目的在于保障变量值的线程可见性。
对象操作的原子性
基础的原子类型只能保证一个变量的原子操作,当需要对多个变量进行操作时, CAS 无法保证原子性操作,这时可以用 AtomicReference(原子引用类型)保证对象引用的原子性。
简单来说,如果需要同时保障对多个变量操作的原子性,就可以把多个变量放在一个对象里进行操作。
与对象操作的原子性有关的原子类型,除了引用类型原子类之外,还包括属性更新原子类。
引用类型原子类
引用类型原子类包括以下3种:
- AtomicReference: 基础的引用原子类
- AtomicMarkableReference : 带修改标志的引用原子类
- AtomicStampedReference : 带印戳的引用原子类