Java多线程:Unsafe 类以及 CAS 函数

前言

学习了前面那么多原子更新类,我们从它们的底层代码中看出,每个类中都通过 Unsafe.getUnnsafe() 方法来获取到了一个 Unsafe 的实例,并且更新类中的大部分方法底层都是通过调用 Unsafe 类的方法来实现的,当你想看这些 Unsafe 中方法的具体实现时,你会发现它们全是本地方法(native)。那么这个类到底为我们做了什么呢?

Unsafe 类

Java最初被设计为一种安全的受控环境,它无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能。

这个类是用于执行低级别、不安全操作的方法集合。尽管这个类和所有的方法都是公开的(public),但是这个类的使用仍然受限,你无法在自己的java程序中直接使用该类,因为只有授信的代码(jre中)才能获得该类的实例。(通过反射还是可以获得Unsafe的实例)

它主要提供了如下几个功能:

1. 通过 Unsafe 类可以分配内存,可以释放内存。

类中提供的3个本地方法allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。

2. 可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的。

字段的定位:

JAVA中对象的字段的定位可能通过staticFieldOffset方法实现,该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的。

getIntVolatile方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。

getLong方法获取对象中offset偏移地址对应的long型field的值

数组元素定位:

Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,这些常量值是通过arrayIndexScale方法得到的。arrayIndexScale方法也是一个本地方法,可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。

 1 public final class Unsafe {
 2     public static final int ARRAY_INT_BASE_OFFSET;
 3     public static final int ARRAY_INT_INDEX_SCALE;
 4 
 5     /**
 6      * 返回指定静态field的内存地址偏移量,在这个类的其他方法中这个值只是被用作一个访问
 7      * 特定field的一个方式。这个值对于 给定的field是唯一的,并且后续对该方法的调用都应该返回相同的值。
 8      * @param field 需要返回偏移量的field
 9      * @return 指定field的偏移量
10      */
11     public native long objectFieldOffset(Field field);
12 
13     /**
14      * 设置obj对象中offset偏移地址对应的整型field的值为指定值。支持volatile store语义
15      * @param obj 包含需要去读取的field的对象
16      * @return offset obj中整型field的偏移量
17      *
18      */
19     public native int getIntVolatile(Object obj, long offset);
20 
21     /**
22      * 获取obj对象中offset偏移地址对应的long型field的值
23      * @param obj 包含需要去读取的field的对象
24      * @param offset object中long型field的偏移量
25      */
26     public native long getLong(Object obj, long offset);
27 
28     /**
29      * 获取给定数组中第一个元素的偏移地址。
30      * 为了存取数组中的元素,这个偏移地址与arrayIndexScale方法的非0返回值一起被使用
31      * @param arrayClass  第一个元素地址被获取的class
32      * @return 数组第一个元素 的偏移地址
33      */
34     public native int arrayBaseOffset(Class arrayClass);
35 
36     /**
37      * 获取用户给定数组寻址的换算因子.一个合适的换算因子不能返回的时候(例如:基本类型),
38      * 返回0.这个返回值能够与arrayBaseOffset一起使用去存取这个数组class中的元素
39      * @param arrayClass
40      */
41     public native int arrayIndexScale(Class arrayClass);
42 
43     static 
44     {
45         ARRAY_INT_BASE_OFFSET = theUnsafe.arrayBaseOffset([I);
46         ARRAY_INT_INDEX_SCALE = theUnsafe.arrayIndexScale([I);
47     }
48 }

3. 挂起和恢复

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本park方法,但最终都调用了Unsafe.park()方法。

4. CAS操作

  CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”

是通过compareAndSwapXXX方法实现的

/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
* 
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param update 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作。

这里可以举个例子来说明compareAndSet的作用,如支持并发的计数器,在进行计数的时候,首先读取当前的值,假设值为a,对当前值 + 1得到b,但是+1操作完以后,并不能直接修改原值为b,因为在进行+1操作的过程中,可能会有其它线程已经对原值进行了修改,所以在更新之前需要判断原值是不是等于a,如果不等于a,说明有其它线程修改了,需要重新读取更新后的值进行操作,如果等于a,说明在+1的操作过程中,没有其它线程来修改值,我们就可以放心的更新原值了。

参考资料:

Java中Unsafe类详解

《Java并发编程实践》

原文地址:https://www.cnblogs.com/2015110615L/p/6751804.html