你一定不知道的Java Unsafe用法详解
Unsafe是什么
首先我们说Unsafe类位于rt.jar里面sun.misc包下面,Unsafe翻译过来是不安全的,这倒不是说这个类是不安全的,而是说开发人员使用Unsafe是不安全的,也就是不推荐开发人员直接使用Unsafe。而且Oracle JDK源码包里面是没有Unsafe的源码的。其实JUC包里面的类大部分都用到了Unsafe,可以说Unasfe是java并发包的基石。
如何正确地获取Unsafe对象
我们从源码中看如何获取Unsafe对象
private Unsafe() { }
首先构造方法私有化,这就说明我们不能通过new Unsafe的方式创建Unsafe对象。
@CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } }
我又发现有一个静态方法,方法名为getUnsafe,而且返回值为Unsafe,看来调用这个方法可以获取Unsafe对象。
为此我又编写了如下代码测试这样是否行得通
public static void main(String[] args) throws Exception{ Unsafe unsafe = Unsafe.getUnsafe(); System.out.println(unsafe); }
谁知道控制台竟然报错了
看错误提示信息是权限方面的错误,但是我看AtomicBoolean类获取Unsafe的方式就是调用getUnsafe方法,可能是只允许JDK内部的类可以通过这种方式访问吧,这里我们不深究,再想别的办法获取。
继续看源码找突破口
Unsafe类里面第一个常量是 private static final Unsafe theUnsafe; 用static和final修饰而且没有直接赋值,这就说明肯定有静态代码块对theUnsafe赋值了,然后再类的底部发现了。
static { registerNatives(); Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"}); theUnsafe = new Unsafe(); ARRAY_BOOLEAN_BASE_OFFSET = theUnsafe.arrayBaseOffset(boolean[].class); ARRAY_BYTE_BASE_OFFSET = theUnsafe.arrayBaseOffset(byte[].class); ARRAY_SHORT_BASE_OFFSET = theUnsafe.arrayBaseOffset(short[].class); ARRAY_CHAR_BASE_OFFSET = theUnsafe.arrayBaseOffset(char[].class); ARRAY_INT_BASE_OFFSET = theUnsafe.arrayBaseOffset(int[].class); ARRAY_LONG_BASE_OFFSET = theUnsafe.arrayBaseOffset(long[].class); ARRAY_FLOAT_BASE_OFFSET = theUnsafe.arrayBaseOffset(float[].class); ARRAY_DOUBLE_BASE_OFFSET = theUnsafe.arrayBaseOffset(double[].class); ARRAY_OBJECT_BASE_OFFSET = theUnsafe.arrayBaseOffset(Object[].class); ARRAY_BOOLEAN_INDEX_SCALE = theUnsafe.arrayIndexScale(boolean[].class); ARRAY_BYTE_INDEX_SCALE = theUnsafe.arrayIndexScale(byte[].class); ARRAY_SHORT_INDEX_SCALE = theUnsafe.arrayIndexScale(short[].class); ARRAY_CHAR_INDEX_SCALE = theUnsafe.arrayIndexScale(char[].class); ARRAY_INT_INDEX_SCALE = theUnsafe.arrayIndexScale(int[].class); ARRAY_LONG_INDEX_SCALE = theUnsafe.arrayIndexScale(long[].class); ARRAY_FLOAT_INDEX_SCALE = theUnsafe.arrayIndexScale(float[].class); ARRAY_DOUBLE_INDEX_SCALE = theUnsafe.arrayIndexScale(double[].class); ARRAY_OBJECT_INDEX_SCALE = theUnsafe.arrayIndexScale(Object[].class); ADDRESS_SIZE = theUnsafe.addressSize(); }
第四行对theUnsafe进行了赋值。也就是说在类加载完成后Unsafe里面的theUnsafe常量就已经赋值好了Unsafe对象,如果我们想获取Unsafe对象只要用反射拿到theUnsafe属性就可以了。
/** * 获得Unsafe * @throws NoSuchFieldException * @throws IllegalAccessException */ public static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException { Field field = Unsafe.class.getDeclaredField("theUnsafe"); //私有属性可以访问 field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); System.out.println(unsafe); return unsafe; }
Unsafe实现CAS锁
CAS是compare and swap的缩写,中文翻译成比较并交换。 在juc包下Atomic开头的类都是使用的CAS锁实现的并发条件下对一个变量赋值不覆盖的。我们也可以自己使用Unsafe实现CAS锁。
interface Counter{ void increment(); long getCounter(); } /** * 自己用unsafe实现CAS锁 */ static class CasCounter implements Counter{ private volatile long counter = 0; private Unsafe unsafe; private long offset; CasCounter() throws NoSuchFieldException, IllegalAccessException { unsafe = getUnsafe(); //获得该类counter属性内存偏移量起始位置 offset = unsafe.objectFieldOffset(CasCounter.class.getDeclaredField("counter")); } @Override public void increment() { long current = counter; //循环判断是否赋值成功 第一个参数为调用本方法的对象,第二个参数为要更改属性的内存偏移量,第三个参数是未修改之前的值,第四个参数是想要修改为那个值。 while (!unsafe.compareAndSwapLong(this,offset,current,current+1)){ current = counter; } } @Override public long getCounter() { return counter; } }
这里我们主要看unsafe.objectFieldOffset(CasCounter.class.getDeclaredField("counter"));这一行代码,因为通过CAS对属性值进行更改是直接在内存上进行更改的,所以我们需要拿到这个对象的counter属性的内存偏移量。
再看increment方法,这里我们在更改之前先拿到counter的值,unsafe.compareAndSwapLong方法就是根据内存偏移量进行更改值的,第一个参数确定那个对象,第二个参数确定那个属性,第三个参数比对要更改属性的原值,第四个参数要更改的值。如果更改成功则返回true,更改失败返回false。这里的逻辑是更改失败就一直更改,知道更改成功才跳出循环。这样就会有效的防止属性值被覆盖的问题。 我们写的CasCounter类就实现了AtomicInteger类的部分功能。
使用Unsafe创建对象
我们都知道反射可以‘走后门'创建对象,其实Unsafe也是可以的
static class Simple{ static { System.out.println("类初始化"); } private long l = 0; public Simple(){ this.l = 1; System.out.println("对象初始化"); } public long get(){ return l; } } public static void main(String[] args) throws Exception { Unsafe unsafe = getUnsafe(); //相当于直接在内存中开辟一块地址,不运行构造方法 Simple simple = (Simple) unsafe.allocateInstance(Simple.class); System.out.println("通过unsafe创建对象不会运行构造方法: " + simple.get()); System.out.println("但是可以通过对象获得class对象" + simple.getClass()); System.out.println("也可以拿到类加载器 " + simple.getClass().getClassLoader()); }
控制台输出如下
这里我们发现使用Unsafe创建对象并没有运行构造方法,而只是将对象创建出来了。而使用反射创建对象是会运行构造方法的和使用new的方式创建对象别无二致。所以不推荐使用Unsafe创建对象。
Unsafe加载类
既然Unsafe是直接操作的内存那应该也可以加载类,下面我们看看Unsafe是如何加载类的。
我们先自己编写A类
public class A { private int i = 0; public A(){ this.i = 10; } public int get(){ return i; } }
然后运行javac A.java 生成A.class此时A.class的位置是F:\tmp
其次我们编写Unsafe加载class的代码
/** * 通过class文件获得二进制 * @return * @throws IOException */ public static byte[] loadClassContent() throws IOException { File file = new File("F:\\tmp\\a.class"); FileInputStream fis = new FileInputStream(file); byte[] content = new byte[(int) file.length()]; fis.read(content); fis.close(); return content; } public static void main(String[] args) throws Exception { Unsafe unsafe = getUnsafe(); byte[] bytes = loadClassContent(); Class<?> aClass = unsafe.defineClass(null, bytes, 0, bytes.length,null,null); Method get = aClass.getMethod("get"); int i = (int) get.invoke(aClass.newInstance(), null); System.out.println(i); }
这里unsafe.defineClass方法就是加载类的方法。
运行后输出结果为10
这样我们就实现了通过Unsafe加载类。
Unsafe更改私有属性值
我们都知道反射可以更改对象私有属性值,其实Unsafe也可以直接更改私有属性值,代码如下
static class Guard{ private int ACCESS_ALLOWED = 1; private boolean allow(){ return 42==ACCESS_ALLOWED; } public void work(){ if (allow()){ System.out.println("你进行了暗箱操作"); } } } public static void main(String[] args) throws Exception { Unsafe unsafe = getUnsafe(); Guard guard = new Guard(); Field access_allowed = guard.getClass().getDeclaredField("ACCESS_ALLOWED"); unsafe.putInt(guard,unsafe.objectFieldOffset(access_allowed),42); guard.work(); }
输出结果为 你进行了暗箱操作 ,putInt方法第一个参数是要更改的属性属于哪个对象,第二个参数是要更改属性的内存偏移量,第三个参数是要改成什么值。其实就是直接更改指定内存地址中的int属性的值。这样我们就完成了使用Unsafe更改对象私有属性值。
Unsafe类能直接操作内存的特性决定了它能走太多的后门了,而且大部分方法都是native修饰的,底层调用的C++。估计这也是Unsafe的不安全的原因。