Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类,Java官方也不建议直接使用的Unsafe类,但我们还是很有必要了解该类,因为Java中CAS操作的执行依赖于Unsafe类的方法,注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,关于Unsafe类的主要功能点如下:
1、内存管理,Unsafe类中存在直接操作内存的方法。
2、获取对象的实例。
3、挂起与恢复。
4、CAS操作
内存管理
通过Unsafe类可以分配内存,可以释放内存;类中提供的3个本地方法allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。
//分配内存指定大小的内存
public native long allocateMemory(long bytes);
//根据给定的内存地址address设置重新分配指定大小的内存
public native long reallocateMemory(long address, long bytes);
//用于释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long address);
除此之外,它还有下列一些操作内存的方法:
//将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
public native void setMemory(Object o, long offset, long bytes, byte value);
//设置给定内存地址的值
public native void putAddress(long address, long x);
//获取指定内存地址的值
public native long getAddress(long address);
//设置指定内存的byte值
//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同
public native byte getByte(long address);
//获取指定内存的byte值
public native void putByte(long address, byte x);
//操作系统的内存页大小
public native int pageSize();
//对象字段的定位,该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的
public native long staticFieldOffset(Field field);
//获取对象中offset偏移地址对应的整型field的值,支持volatile load语义
public native int getIntVolatile(Object obj, long l);
//获取数组第一个元素的偏移地址
public native int arrayBaseOffset(Class class1);
//获取数组的转换因子,也就是数组中元素的增量地址
public native int arrayIndexScale(Class class1);
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配合使用,可以定位数组中每个元素在内存中的位置。
获取对象的实例
//传入一个对象的class并创建该实例对象,但不会调用构造方法
public native Object allocateInstance(Class cls) throws InstantiationException;
挂起与恢复
将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。Java对线程的挂起操作被封装在 LockSupport类中(java.util.concurrent包中挂起操作都是在LockSupport类实现的),LockSupport类中有各种版本pack方法,其底层实现最终还是使用Unsafe.park()方法和Unsafe.unpark()方法来实现的。
public class LockSupport {
public static void unpark(Thread thread) {
if (thread != null)
unsafe.unpark(thread);
}
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
unsafe.park(false, 0L);
setBlocker(t, null);
}
public static void parkNanos(Object blocker, long nanos) {
if (nanos > 0) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
unsafe.park(false, nanos);
setBlocker(t, null);
}
}
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
unsafe.park(true, deadline);
setBlocker(t, null);
}
public static void park() {
unsafe.park(false, 0L);
}
public static void parkNanos(long nanos) {
if (nanos > 0)
unsafe.park(false, nanos);
}
public static void parkUntil(long deadline) {
unsafe.park(true, deadline);
}
}
Unsafe里的CAS操作
CAS是一些CPU直接支持的指令,在Java中无锁操作CAS基于以下3个方法实现:
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值。expected表示期望值,x表示要设置的值。
利用Unsafe类
获取Unsafe实例
public static Unsafe getUnsafeInstance() throws Exception{
Field unsafeStaticField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeStaticField.setAccessible(true);
return (Unsafe) unsafeStaticField.get(Unsafe.class);
}
通过java反射机制,我们跳过了安全检测,拿到了一个Unsafe类的实例。
修改和读取数组中的值
@RequestMapping("/readandwritearray")
public void readAndWriteArray() throws Exception {
Unsafe unsafe = getUnsafeInstance();
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//返回当前数组的首地址
int b = unsafe.arrayBaseOffset(int[].class);
//返回当前数组一个元素占用的字节数
int s = unsafe.arrayIndexScale(int[].class);
//获取数组对象obj的起始地址,加上偏移值,得到对应元素的地址,将intval写入内存
unsafe.putInt(arr, (long) b + s * 9, 1);
for (int i = 0; i < 10; i++) {
//获取数组对象obj的起始地址,加上偏移值,得到对应元素的地址,从而获得元素的值
int v = unsafe.getInt(arr, (long) b + s * i);
System.out.print(v + " ");
}
}
打印结果:1 2 3 4 5 6 7 8 9 1 ,可以看到,成功读出了数组中的值,而且最后一个值由10改为了1。
偏移值: 数组元素偏移值 = arrayBaseOffset + arrayIndexScalse * i。
修改静态变量和实例变量的值
先定义一个UnsafeTest类:
public class UnsafeTest {
public int infield;
public static int staticIntField;
public static int[] arr;
private UnsafeTest() {
System.out.println("constructor called!");
}
}
修改UnsafeTest类的实例变量:
@RequestMapping("/changeinstancefield")
public void changeInstanceField() throws Exception {
Unsafe unsafe = getUnsafeInstance();
//传入一个对象的class并创建该实例对象,但不会调用构造方法
UnsafeTest unsafeTest = (UnsafeTest)unsafe.allocateInstance(UnsafeTest.class);
//获取对象某个属性的地址偏移值
long b1 = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("infield"));
unsafe.putInt(unsafeTest, b1, 2);
System.out.println("infield:" + unsafeTest.infield);
}
这里使用allocateInstance方法获取了一个UnsafeTest类的实例,并且没有打印“constructor called”,说明构造方法没有调用。修改实例变量与修改数组的值类似,同样要获取地址偏移值,然后调用putInt方法。
修改UnsafeTest类的静态变量:
@RequestMapping("/changestaticfield")
public void changeStaticField() throws Exception {
Unsafe unsafe = getUnsafeInstance();
Field staticIntField = UnsafeTest.class.getDeclaredField("staticIntField");
//获取静态变量所属的类在方法区的首地址。可以看到,返回的对象就是UnsafeTes.class
Object o = unsafe.staticFieldBase(staticIntField);
System.out.println(o == UnsafeTest.class);
//获取静态变量地址偏移值
Long b4 = unsafe.staticFieldOffset(staticIntField);
//因为是静态变量,传入的Object参数应为class对象
unsafe.putInt(o, b4, 10);
System.out.println("staticIntField:" + unsafe.getInt(UnsafeTest.class, b4));
}
打印结果:
true
staticIntField:10
静态变量与实例变量不同之处在于,静态变量位于方法区中,它的地址偏移值与UnsafeTest类在方法区的地址相关,与UnsafeTest类的实例无关。
调戏String.intern
在jdk7中,String.intern不再拷贝string对象实例,而是保存第一次出现的对象的引用。在下面的代码中,通过Unsafe修改被引用对象s的私有属性value达到间接修改s1的效果!
@RequestMapping("/stringintern")
public void stringIntern() throws Exception {
String s = "abc";
//保存s的引用
s.intern();
//此时s1==s,地址相同
String s1 = "abc";
Unsafe unsafe = getUnsafeInstance();
//获取s的实例变量value
Field valueInString = String.class.getDeclaredField("value");
//获取value的变量偏移值
long offset = unsafe.objectFieldOffset(valueInString);
//value本身是一个char[],要修改它元素的值,仍要获取baseOffset和indexScale
long base = unsafe.arrayBaseOffset(char[].class);
long scale = unsafe.arrayIndexScale(char[].class);
//获取value
char[] values = (char[]) unsafe.getObject(s, offset);
//为value赋值
unsafe.putChar(values, base + scale, 'c');
System.out.println("s:" + s + " s1:" + s1);
//将s的值改为 abc
s = "abc";
String s2 = "abc";
String s3 = "abc";
System.out.println("s:" + s + " s1:" + s1);
System.out.println("s2:" + s2 +" s3:" + s3);
}
打印结果:
s:acc s1:acc
s:acc s1:acc
s2:acc s3:acc
我们发现了什么?所有值为“abc”的字符串都变成了“acc”。Unsafe类果然不安全!!!