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的操作过程中,没有其它线程来修改值,我们就可以放心的更新原值了。
参考资料:
posted on 2016-04-23 10:32 Traveling_Light_CC 阅读(391) 评论(0) 编辑 收藏 举报