你一定不知道的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的不安全的原因。
如果喜欢本篇文章不妨关注点赞收藏,有什么困惑欢迎评论。
欢迎关注接地气程序员公众号。
你一定不知道的Unsafe用法的更多相关文章
- TabLayout您可能不知道的实用用法
一.修改点击的动画 函数:setUnboundedRipple 这是默认的点击的动画 我们用代码修改一下: mGlueTabLayout.setUnboundedRipple(true); 这是之后的 ...
- 你所不知道的setInterval
在你所不知道的setTimeout记载了下setTimeout相关,此篇则整理了下setInterval:作为拥有广泛应用场景(定时器,轮播图,动画效果,自动滚动等等),而又充满各种不确定性的这set ...
- 你所不知道的setTimeout
JavaScript提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()和setInterval()这两个函数来完成.它们向任务队列添加定时任务.初始接触它的人都觉得好简单 ...
- 你所不知道的linq(二)
上一篇说了from in select的本质,具体参见你所不知道的linq.本篇说下from...in... from... in... select 首先上一段代码,猜猜结果是什么? class P ...
- ES6 你可能不知道的事 – 基础篇
序 ES6,或许应该叫 ES2015(2015 年 6 月正式发布),对于大多数前端同学都不陌生. 首先这篇文章不是工具书,不会去过多谈概念,而是想聊聊关于每个特性 你可能不知道的事,希望能为各位同学 ...
- 你所不知道的html5与html中的那些事第三篇
文章简介: 关于html5相信大家早已经耳熟能详,但是他真正的意义在具体的开发中会有什么作用呢?相对于html,他又有怎样的新的定义与新理念在里面呢?为什么一些专家认为html5完全完成后,所有的工作 ...
- 你所不知道的Html5那些事(一)
文章简介: 关于html5相信大家早已经耳熟能详,但是他真正的意义在具体的开发中会有什么作用呢?相对于html,他又有怎样的新的定义与新理念在里面呢?为什么一些专家认为html5完全完成后 ...
- 你所不知道的 CSS 滤镜技巧与细节
承接上一篇你所不知道的 CSS 动画技巧与细节,本文主要介绍 CSS 滤镜的不常用用法,希望能给读者带来一些干货! OK,下面直接进入正文.本文所描述的滤镜,指的是 CSS3 出来后的滤镜,不是 IE ...
- 你所不知道的 CSS 阴影技巧与细节
关于 CSS 阴影,之前已经有写过一篇,box-shadow 与 filter:drop-shadow 详解及奇技淫巧,介绍了一些关于 box-shadow 的用法. 最近一个新的项目,CSS-Ins ...
随机推荐
- PHP密码散列算法的学习
不知道大家有没有看过 Laravel 的源码.在 Laravel 源码中,对于用户密码的加密,使用的是 password_hash() 这个函数.这个函数是属于 PHP 密码散列算法扩展中所包含的函数 ...
- Linux系列(25) - 常用快捷键(未更新完)
快捷键 说明 Ctrl+L 清屏 tab tab按一次自动补全目录文件名称/tab按二次将目录下带有补全前面字段的所有文件目录展示出来,例子: cd / tab键按两次将根目录下所有文件展示出来 ...
- 深入HTML5第三天
表单form属性:method="get|post" action="url": 特性:不写样式是没有样式的 input:属性:type="text| ...
- 怎么让jenkins保持后台运行
今天在腾讯云直接使用命令启动jenkins,java -jar jenkins.war --httpPort=8080,这样启动关掉shell窗口或退回shell窗口,进程就会结束,需要重新 启动,非 ...
- php安全 过滤、验证、转义
不要相信外部源 $_GET $_POST $_REQUEST $_COOKIE $argv php://stdin php://input file_get_contents() 远程数据库 远程ap ...
- command ' cl.exe' failed: No such file or directory解决办法
1.安装C ++编译器 https://pan.baidu.com/s/1D1-tM-mWO4TVLdTrh3k1GA 提取码:ym67 2.找到安装文件夹:Visual C++ Build T ...
- k8s负载资源StatefulSet工作解析
在k8s中工作负载资源StatefulSet用于管理有状态应用. 什么是无状态? 组成一个应用的pod是对等的,它们之前没有关联和依赖关系,不依赖外部存储. 即我们上篇小作文中deployment创建 ...
- 软件测试工程师简历要怎么写,才能让HR看到
作为软件测试的从业者,面试或者被面试都是常有的事. 可是不管怎样,和简历有着理不清的关系,面试官要通过简历了解面试者的基本信息.过往经历等. 面试者希望通过简历把自己最好的一面体现给面试官,所以在这场 ...
- 小米路由器4a千兆版刷openwrt
现在网上搜小米路由器4a千兆版刷机的都是刷的padavan的,很少能找到openwrt的刷机教程. 首先刷openwrt系统的时候要先刷入引导程序breed,网上有一篇帖子写的很详细(https:// ...
- 实现线程按顺序输出ABC
线程按顺序输出ABC 实现描述:建立三个线程A.B.C,分别按照顺序输出十次ABC 首先建立一个方法,按照条件进行输出 class PrintABC{ private int index=0; pub ...