# CAS
我们希望在并发的过程中,先进行操作,如果没有其他线程争用共享数据,那么操作就成功了,否则就采取补偿措施。这种基于冲突检测的乐观的操作许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里的原子性不能够使用互斥同步来保证,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换 ( Compare-and-Swap,CAS
)。 CAS
指令需要有 3 个操作数,分别是内存地址 V
、旧的预期值 A
和新值 B
。当执行操作时,只有当 V
的值等于 A
,才将 V
的值更新为 B
。
因为 CAS
操作时原子性的,所以多线程并发使用 CAS
更新数据时不用锁。类似 sql
中的条件更新一样: update set id=3 from table where id=2
。因为单条 sql
执行具有原子性,如果有多个线程同时执行此 sql
语句,只有一条能更新成功(就是最后一个执行的那个)。
CAS
方式为乐观锁, synchronized
为悲观锁。因此使用 CAS
解决并发问题通常情况下性能更优。
问题:
ABA
问题,如果一个变量初次读取的时候是A
值,它的值被改成了B
,后来又被改回为A
,那CAS
操作就会误认为它从来没有被改变过。J.U.C
包提供了一个带有标记的原子引用类AtomicStampedReference
来解决这个问题,它可以通过控制变量值的版本来保证CAS
的正确性。在变量前加上版本号,每次变量更新的时候把版本加 1,那么A->B->A
就会变成1A->2B->3A
,从前是比较期望的旧值和当前值,现在多比较一个版本号(compareAndSet
方法),更新时两个一起更新。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。- 自旋
CAS
如果长时间不成功,会给CPU
带来非常大的执行开销。 - 只能保证一个共享变量的原子操作,多了就可以用锁。
# Unsafe 类详解
Unsafe 是位于 sun.misc
包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再 “安全”,因此对 Unsafe 的使用一定要慎重。
该类的构造方法是私有的,只有通过反射才能拿到其实例化对象
public void test() { | |
//Unsafe 构造方法私有,不能被实例化,通过反射获得对象 | |
Field f = Unsafe.class.getDeclaredField("theUnsafe"); | |
f.setAccessible(true); | |
Unsafe unsafe = (Unsafe) f.get(null); | |
} |
Unsafe 类提供的 API 大致可分为:内存操作,CAS,Class 相关,对象操作,线程调度,系统信息获取,内存屏障,数组操作。
通过查看源码,其实 Unsafe 只提供三种 CAS 方法
//paramObject1:int 字段所在的对象,paramLong:该字段在该对象中的偏移量,paramObject2:期望的旧值,paramObject3:要更新的值 | |
public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3); | |
public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2); | |
public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3); |
这几个都是 native
方法,由 C++ 代码实现,都位于 unsafe.cpp
文件中
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
//之前的代码展示我们看到,CAS函数是放在while循环中的,也就是说,返回值是一个布尔值。
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
//该函数要更新的是一个jint,所以要先根据对象引用+偏移量得到其地址,也就是对应的指针
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
//很明显,Atomic::cmpxhg才是进行CAS操作(比较+替换)的那一步,x是即将更新的值,e是期望的旧值
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
当然,Unsafe 类还提供了许多硬件级别的操作
// 获取给定的 paramField 的内存地址偏移量 | |
public native long staticFieldOffset(Field paramField); | |
// 分配内存 | |
public native long allocateMemory(long paramLong); | |
// 扩充内存 | |
public native long reallocateMemory(long paramLong1, long paramLong2); | |
// 释放内存 | |
public native void freeMemory(long paramLong); | |
// 获取数组第一个元素的偏移地址 | |
public native int arrayBaseOffset(Class paramClass); |
在 AQS
讲解中,我们就提到过 AQS
的部分属性,就是 cas
时需要用到的偏移值。
关于 Unsafe 类的更多知识,可以参考美团技术团队的文章:Java 魔法类:Unsafe 应用解析
# 参考
https://pdai.tech/md/java/thread/java-thread-x-juc-AtomicInteger.html
https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html