Java 线程总结(五) —— 原子变量和 CAS
原子变量相关
1、Java 并发包中的基本原子变量类型有:
AtomicBoolean:原子 Boolean 类型
AtomicInteger:原子 Integer 类型
AtomicLong:原子 Long 类型
AtomicReference:原子引用类型
针对 Integer, Long 和 Reference 类型,还有对应的数组类型:
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
为了便于以原子方式更新对象中的字段,还有如下的类:
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
AtomicReference 还有两个类似的类,在某些情况下更为易用:
AtomicMarkableReference
AtomicStampedReference
2、对于 char, short, float, double 类型的原子变量,可以转为 int/long,然后使用 AtomicInteger 或 AtomicLong。eg:float 类型和 int 类型互相转换:1
2public static int floatToIntBits(float value)
public static float intBitsToFloat(int bits);
3、之所以称为原子变量,是因为其包含一些以原子方式实现组合操作的方法。这些方法的实现都依赖另一个 public 方法:public final boolean compareAndSet(int expect, int update)
比较并设置。该方法以原子方式实现了如下功能:如果当前值等于 expect,则更新为 update,否则不更新,如果更新成功,返回 true,否则返回 false。
AtomicInteger 源码解析:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
// 用 volatile 关键字保证获取到的当前值是内存中的最新值
private volatile int value;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// unsafe 中的 compareAndSwapInt 方法,用 native 声明,该方法封装了底层操作系统的硬件级别的原子操作
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
}
什么是 unsafe 呢?Java 语言不像 C,C++ 那样可以直接访问底层操作系统,但是 JVM 为我们提供了一个后门,这个后门就是 unsafe。unsafe 为我们提供了硬件级别的原子操作。
至于 valueOffset 对象,是通过 unsafe.objectFieldOffset 方法得到,它是一个 native 声明的方法,所代表的是 AtomicInteger 对象的成员变量 value 在内存中的偏移量。我们可以简单地把 valueOffset 理解为 value 变量的内存地址。
CAS 的底层实现?
1、用 volatile 关键字保证获取到的 value 当前值是内存中的最新值。
2、使用静态代码块在类初始化的时候就获取到某个对象的成员变量 value 在内存中的偏移量 valueOffset,可以简单的理解为 value 变量的内存地址。
3、compareAndSwapInt 是 native 声明的方法,封装了底层操作系统的硬件级别的原子操作。
4、所谓的自旋操作实际上就是在一个死循环中不断的调用操作系统硬件级别的原子操作,直到成功为止。
说到底,正是因为 unsafe 该方法封装了底层操作系统的硬件级别的原子操作,例如:compareAndSwapInt,才能有 Java 的 CAS。
什么是 ABA 问题?如何解决?
所谓 ABA 问题,就是一个变量的值从 A 改成了 B,又从 B 改成了 A。虽然表面上看,某块内存里的值没有改变,但是实际上此 A 非彼 A。但是在使用 CAS 进行更新的时候,会认为两个 A 是同一个 A,从而产生问题。「当一个值从 A 更新成 B,又更新会 A,普通 CAS 机制会误判通过检测。利用版本号比较可以有效解决 ABA 问题。」
要想解决 ABA 问题,本质上就是要区分此 A 非彼 A。例如:给 A 加个版本号。真正要做到严谨的 CAS 机制,在 Compare 阶段不仅要比较期望值和地址中的实际值,还要比较变量的版本号是否一致。在 Java 当中,AtomicStampedReference 类 和 AtomicMarkableReference 类就实现了用版本号做比较的 CAS 机制。
4、synchronized VS CAS
synchronized 是悲观的,它假定更新很可能冲突,所以先获取锁,得到锁后才更新。原子变量的更新逻辑是乐观的,它假定冲突比较少,但使用 CAS 更新,也就是进行冲突检测,如果确实冲突了,那也没关系,继续尝试就好了。
synchronized 代表一种阻塞式算法,得不到锁的时候,进入锁等待队列,等待其他线程唤醒,有上下文切换开销。原子变量的更新逻辑是非阻塞式的,更新冲突的时候,它就重试,不会阻塞,不会有上下文切换开销。
原子变量是比较简单的,但对于复杂一些的数据结构和算法,非阻塞方式往往难于实现和理解,幸运的是,Java 并发包中已经提供了一些非阻塞容器。
ConcurrentLinkedQueue 和 ConcurrentLinkedDeque:非阻塞并发队列。
ConcurrentSkipListMap 和 ConcurrentSkipListSet:非阻塞并发 Map 和 Set。
5、AtomicReference 用来以原子方式更新复杂类型,它有一个类型参数,使用时需要指定引用的类型。
原子数组方便以原子的方式更新数组中的每个元素。public AtomicIntegerArray(int[] array)
注意:该方法接受一个已有的数组,但不会直接操作该数组,而是会创建一个新数组,只是拷贝参数数组中的内容到新数组。
FieldUpdater 方便以原子方式更新对象中的字段,字段不需要声明为原子变量,FieldUpdater 是基于反射机制实现的。
CAS 相关
1、什么是 CAS?
CAS 是英文单词 Compare And Swap 的缩写,翻译过来就是比较并替换。CAS 机制当中使用了 3 个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 当中的实际值相同时,才会将内存地址 V 对应的值修改为 B。
2、CAS 机制的缺点?
CAS 虽然没有上下文切换了,但却是在消耗 CPU 不停的自旋,即 CPU 不停的重新计算。CAS 机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。ABA 问题。
3、CAS 的底层实现?
什么是 unsafe 呢?Java 语言不像 C,C++ 那样可以直接访问底层操作系统,但是 JVM 为我们提供了一个后门,这个后门就是 unsafe。unsafe 为我们提供了硬件级别的原子操作。unsafe 是 Sun 的私有实现,从名字看,表示的也是”不安全”,一般应用程序不应该直接使用。原理上,一般的计算机系统都在硬件层次上直接支持 CAS 指令,而 Java 的实现都会利用这些特殊指令。从程序的角度看,我们可以将 compareAndSet 视为计算机的基本操作,直接接纳就好。
至于 valueOffset 对象,是通过 unsafe.objectFieldOffset 方法得到,所代表的是 AtomicInteger 对象 value 成员变量在内存中的偏移量。我们可以简单地把 valueOffset 理解为 value 变量的内存地址。
CAS 是 Java 并发包的基础,基于它可以实现高效的、乐观、非阻塞式数据结构和算法,它也是并发包中锁、同步工具和各种容器的基础。
1 | private static final Unsafe unsafe = Unsafe.getUnsafe(); |
4、CAS 中的 ABA 问题?
所谓 ABA 问题,就是一个变量的值从 A 改成了 B,又从 B 改成了 A。即当一个值从 A 更新成 B,又更新会 A,普通 CAS 机制会误判通过检测。真正要做到严谨的 CAS 机制,我们在 Compare 阶段不仅要比较期望值 A 和地址 V 中的实际值,还要比较变量的版本号是否一致。在 Java 中,AtomicStampedReference 类就实现了用版本号做比较的 CAS 机制。ABA 是不是一个问题与程序的逻辑有关。
volatile 相关
首先明确一点,volatile 修饰符并不是 Java 语言的首创,早在 C 和 C++ 当中就已经存在了。
注意:工作内存所做的修改不会立刻同步到主内存,所以可能会产生问题。
1、什么是 volatile 关键字?
Java 内存模型简称 JMM(Java Memory Model)。线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。
volatile 关键字保证了用 volatile 修饰的变量对所有线程的可见性。可见性是指:当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。注意的是 volatile 只能保证变量的可见性,并不能保证变量的原子性。
volatile 保证可见性的特性,得益于 Java 语言的 先行发生原则(happens-before)。先行发生原则是指:在计算机科学中,先行发生原则是两个事件的结果之间的关系,如果一个事件发生在另一个事件之前,结果必须反映,即使这些事件实际上是乱序执行的(通常是优化程序流程)。
2、什么是指令重排?
指令重排是指JVM在编译Java代码的时候,或者 CPU 在执行 JVM 字节码的时候,对现有的指令顺序进行重新排序。指令重排是在字节码指令的层面进行重排序的。
指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。
3、什么是内存屏障?
内存屏障(Memory Barrier)是一种 CPU 指令。内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使 CPU 或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
内存屏障共分为四种类型:
1.LoadLoad 屏障
抽象场景:Load1; LoadLoad; Load2
Load1 和 Load2 代表两条读取指令。在 Load2 要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。2.StoreStore 屏障
抽象场景:Store1; StoreStore; Store2
Store1 和 Store2 代表两条写入指令。在 Store2 写入执行前,保证 Store1 的写入操作对其它处理器可见。3.LoadStore 屏障
抽象场景:Load1; LoadStore; Store2
在 Store2 被写入前,保证 Load1 要读取的数据被读取完毕。4.StoreLoad 屏障
抽象场景:Store1; StoreLoad; Load2
在 Load2 读取操作执行前,保证 Store1 的写入对所有处理器可见。StoreLoad 屏障的开销是四种屏障中最大的。
4、volatile 如何使用内存屏障来保证变量在线程之间的可见性?
在一个变量被 volatile 修饰后,JVM 会为我们做两件事:
1.在每个 volatile 写操作前插入 StoreStore 屏障,在写操作后插入 StoreLoad 屏障。
2.在每个 volatile 读操作前插入 LoadLoad 屏障,在读操作后插入 LoadStore 屏障。
5、volatile 只是保证,写的时候能够立刻从工作内存同步到主内存中,读的时候,一定读的是当前内存中的最新值,仅此而已。它不会保证原子性,如果 10 个线程同时读,它们都会读到内存中的最新值,但是,只会读一次,之后内存中的值改变了,就必须重新读了,重新读的时候,依然是当前时刻内存中的最新值。即 volatile 值负责它读的那一刻,读取到的是内存中的最新值,至于以后内存中的该值怎么变化,跟它已经没关系了。
参考博客
什么是 CAS 机制?
什么是 CAS 机制?(进阶篇)
什么是 volatile 关键字?
Java 编程的逻辑 - 原子变量和 CAS
用户模式和内核模式