java Unsafe详细解析
问题
(1)unsafe是什么?
(2)unsafe只有cas的功能吗?
(3)unsafe为什么是不安全的?
(4)怎么使用unsafe?
简介
unsafe为我们提供了访问底层的机制,这种机制仅供java核心类库使用,而不应该被普通用户使用。
但是,为了更好地了解java的生态体系,我们应该去学习它,去了解它,不求深入到底层的c/c++代码,但求能了解它的基本功能。
获取unsafe的实例
查看unsafe的源码我们会发现它提供了一个getunsafe()的静态方法。
@callersensitive public static unsafe getunsafe() { class var0 = reflection.getcallerclass(); if (!vm.issystemdomainloader(var0.getclassloader())) { throw new securityexception("unsafe"); } else { return theunsafe; } }
但是,如果直接调用这个方法会抛出一个securityexception异常,这是因为unsafe仅供java内部类使用,外部类不应该使用它。
那么,我们就没有方法了吗?
当然不是,我们有反射啊!查看源码,我们发现它有一个属性叫theunsafe,我们直接通过反射拿到它即可。
public class unsafetest { public static void main(string[] args) throws nosuchfieldexception, illegalaccessexception { field f = unsafe.class.getdeclaredfield("theunsafe"); f.setaccessible(true); unsafe unsafe = (unsafe) f.get(null); } }
使用unsafe实例化一个类
假如我们有一个简单的类如下:
class user { int age; public user() { this.age = 10; } }
如果我们通过构造方法实例化这个类,age属性将会返回10。
user user1 = new user(); // 打印10 system.out.println(user1.age);
如果我们调用unsafe来实例化呢?
user user2 = (user) unsafe.allocateinstance(user.class); // 打印0 system.out.println(user2.age);
age将返回0,因为unsafe.allocateinstance()只会给对象分配内存,并不会调用构造方法,所以这里只会返回int类型的默认值0。
修改私有字段的值
使用unsafe的putxxx()方法,我们可以修改任意私有字段的值。
public class unsafetest { public static void main(string[] args) throws nosuchfieldexception, illegalaccessexception, instantiationexception { field f = unsafe.class.getdeclaredfield("theunsafe"); f.setaccessible(true); unsafe unsafe = (unsafe) f.get(null); user user = new user(); field age = user.getclass().getdeclaredfield("age"); unsafe.putint(user, unsafe.objectfieldoffset(age), 20); // 打印20 system.out.println(user.getage()); } } class user { private int age; public user() { this.age = 10; } public int getage() { return age; } }
一旦我们通过反射调用得到字段age,我们就可以使用unsafe将其值更改为任何其他int值。(当然,这里也可以通过反射直接修改)
抛出checked异常
我们知道如果代码抛出了checked异常,要不就使用try...catch捕获它,要不就在方法签名上定义这个异常,但是,通过unsafe我们可以抛出一个checked异常,同时却不用捕获或在方法签名上定义它。
// 使用正常方式抛出ioexception需要定义在方法签名上往外抛 public static void readfile() throws ioexception { throw new ioexception(); } // 使用unsafe抛出异常不需要定义在方法签名上往外抛 public static void readfileunsafe() { unsafe.throwexception(new ioexception()); }
使用堆外内存
如果进程在运行过程中jvm上的内存不足了,会导致频繁的进行gc。理想情况下,我们可以考虑使用堆外内存,这是一块不受jvm管理的内存。
使用unsafe的allocatememory()我们可以直接在堆外分配内存,这可能非常有用,但我们要记住,这个内存不受jvm管理,因此我们要调用freememory()方法手动释放它。
假设我们要在堆外创建一个巨大的int数组,我们可以使用allocatememory()方法来实现:
class offheaparray { // 一个int等于4个字节 private static final int int = 4; private long size; private long address; private static unsafe unsafe; static { try { field f = unsafe.class.getdeclaredfield("theunsafe"); f.setaccessible(true); unsafe = (unsafe) f.get(null); } catch (nosuchfieldexception e) { e.printstacktrace(); } catch (illegalaccessexception e) { e.printstacktrace(); } } // 构造方法,分配内存 public offheaparray(long size) { this.size = size; // 参数字节数 address = unsafe.allocatememory(size * int); } // 获取指定索引处的元素 public int get(long i) { return unsafe.getint(address + i * int); } // 设置指定索引处的元素 public void set(long i, int value) { unsafe.putint(address + i * int, value); } // 元素个数 public long size() { return size; } // 释放堆外内存 public void freememory() { unsafe.freememory(address); } }
在构造方法中调用allocatememory()分配内存,在使用完成后调用freememory()释放内存。
使用方式如下:
offheaparray offheaparray = new offheaparray(4); offheaparray.set(0, 1); offheaparray.set(1, 2); offheaparray.set(2, 3); offheaparray.set(3, 4); offheaparray.set(2, 5); // 在索引2的位置重复放入元素 int sum = 0; for (int i = 0; i < offheaparray.size(); i++) { sum += offheaparray.get(i); } // 打印12 system.out.println(sum); offheaparray.freememory();
最后,一定要记得调用freememory()将内存释放回操作系统。
compareandswap操作
juc下面大量使用了cas操作,它们的底层是调用的unsafe的compareandswapxxx()方法。这种方式广泛运用于无锁算法,与java中标准的悲观锁机制相比,它可以利用cas处理器指令提供极大的加速。
比如,我们可以基于unsafe的compareandswapint()方法构建线程安全的计数器。
class counter { private volatile int count = 0; private static long offset; private static unsafe unsafe; static { try { field f = unsafe.class.getdeclaredfield("theunsafe"); f.setaccessible(true); unsafe = (unsafe) f.get(null); offset = unsafe.objectfieldoffset(counter.class.getdeclaredfield("count")); } catch (nosuchfieldexception e) { e.printstacktrace(); } catch (illegalaccessexception e) { e.printstacktrace(); } } public void increment() { int before = count; // 失败了就重试直到成功为止 while (!unsafe.compareandswapint(this, offset, before, before + 1)) { before = count; } } public int getcount() { return count; } }
我们定义了一个volatile的字段count,以便对它的修改所有线程都可见,并在类加载的时候获取count在类中的偏移地址。
在increment()方法中,我们通过调用unsafe的compareandswapint()方法来尝试更新之前获取到的count的值,如果它没有被其它线程更新过,则更新成功,否则不断重试直到成功为止。
我们可以通过使用多个线程来测试我们的代码:
counter counter = new counter(); executorservice threadpool = executors.newfixedthreadpool(100); // 起100个线程,每个线程自增10000次 intstream.range(0, 100) .foreach(i->threadpool.submit(()->intstream.range(0, 10000) .foreach(j->counter.increment()))); threadpool.shutdown(); thread.sleep(2000); // 打印1000000 system.out.println(counter.getcount());
park/unparkjvm
在上下文切换的时候使用了unsafe中的两个非常牛逼的方法park()和unpark()。
当一个线程正在等待某个操作时,jvm调用unsafe的park()方法来阻塞此线程。
当阻塞中的线程需要再次运行时,jvm调用unsafe的unpark()方法来唤醒此线程。
我们之前在分析java中的集合时看到了大量的locksupport.park()/unpark(),它们底层都是调用的unsafe的这两个方法。
总结
使用unsafe几乎可以操作一切:
(1)实例化一个类;
(2)修改私有字段的值;
(3)抛出checked异常;
(4)使用堆外内存;
(5)cas操作;
(6)阻塞/唤醒线程;
彩蛋
论实例化一个类的方式?
(1)通过构造方法实例化一个类;
(2)通过class实例化一个类;
(3)通过反射实例化一个类;
(4)通过克隆实例化一个类;
(5)通过反序列化实例化一个类;
(6)通过unsafe实例化一个类;
public class instantialtest { private static unsafe unsafe; static { try { field f = unsafe.class.getdeclaredfield("theunsafe"); f.setaccessible(true); unsafe = (unsafe) f.get(null); } catch (nosuchfieldexception e) { e.printstacktrace(); } catch (illegalaccessexception e) { e.printstacktrace(); } } public static void main(string[] args) throws exception { // 1. 构造方法 user user1 = new user(); // 2. class,里面实际也是反射 user user2 = user.class.newinstance(); // 3. 反射 user user3 = user.class.getconstructor().newinstance(); // 4. 克隆 user user4 = (user) user1.clone(); // 5. 反序列化 user user5 = unserialize(user1); // 6. unsafe user user6 = (user) unsafe.allocateinstance(user.class); system.out.println(user1.age); system.out.println(user2.age); system.out.println(user3.age); system.out.println(user4.age); system.out.println(user5.age); system.out.println(user6.age); } private static user unserialize(user user1) throws exception { objectoutputstream oos = new objectoutputstream(new fileoutputstream("d://object.txt")); oos.writeobject(user1); oos.close(); objectinputstream ois = new objectinputstream(new fileinputstream("d://object.txt")); // 反序列化 user user5 = (user) ois.readobject(); ois.close(); return user5; } static class user implements cloneable, serializable { private int age; public user() { this.age = 10; } @override protected object clone() throws clonenotsupportedexception { return super.clone(); } } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。