神奇的Unsafe,你get了吗

cover

本文原创地址,我的博客https://jsbintask.cn/2019/05/05/jdk/jdk8-unsafe/(食用效果最佳),转载请注明出处!

简介

Unsafe是jdk提供的一个直接访问操作系统资源的工具类(底层c++实现),它可以直接分配内存,内存复制,copy,提供cpu级别的CAS乐观锁等操作。它的目的是为了增强java语言直接操作底层资源的能力,无疑带来很多方便。但是,使用的同时就得额外小心!它的总体作用如下(图片来源网络):

Unsafe

Unsafe位于sun.misc包下,jdk中的并发编程包juc(java.util.concurrent)基本全部靠Unsafe实现,由此可见其重要性。

基本使用

Unsafe被设计为单例,并且只允许被引导类加载器(BootstrapClassLoader)加载的类使用:

Unsafe

所以我们自己写的类是无法直接通过Unsafe.getUnsafe()获取的。当然,既然是java代码,我们就可以使用一点歪道,比如通过反射直接new一个或者将其内部静态成员变量theUnsafe获取出来:

public static void main(String[] args) throws Exception{
    // method 1
    Class<Unsafe> unsafeClass = Unsafe.class;
    Constructor<Unsafe> constructor = unsafeClass.getDeclaredConstructor();
    constructor.setAccessible(true);
    Unsafe unsafe1 = constructor.newInstance();
    System.out.println(unsafe1);

    // method2
    Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
    theUnsafe.setAccessible(true);
    Unsafe unsafe2 = (Unsafe) theUnsafe.get(null);
    System.out.println(unsafe2);
}
Unsafe

现在我们能够在自己代码里面使用Unsafe了,接下来看下它的使用以及jdk使用操作的。

CAS

CAS译为Compare And Swap,它是乐观锁的一种实现。假设内存值为v,预期值为e,想要更新成得值为u,当且仅当内存值v等于预期值e时,才将v更新为u。 这样可以有效避免多线程环境下的同步问题。

在unsafe中,实现CAS算法通过cpu的原子指令cmpxchg实现,它对应的方法如下:

Unsafe

简单介绍下它使用的参数,var1为内存中要操作的对象,var2为要操作的值的内存地址偏移量,var4为预期值,var5为想要更新成的值。

为了方便理解,举个栗子。类User有一个成员变量name。我们new了一个对象User后,就知道了它在内存中的起始值,而成员变量name在对象中的位置偏移是固定的。这样通过这个起始值和这个偏移量就能够定位到name在内存中的具体位置。

所以我们现在的问题就是如何得出name在对象User中的偏移量,Unsafe自然也提供了相应的方法:


Unsafe

他们分别为获取静态成员变量,成员变量的方法,所以我们可以使用unsafe直接更新内存中的值:

public class UnsafeTest {
    public static void main(String[] args) throws Exception {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);

        User user = new User("jsbintask");
        long nameOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("name"));
        unsafe.compareAndSwapObject(user, nameOffset, "jsbintask1", "jsbintask2");
        System.out.println("第一次更新后的值:" + user.getName());
        unsafe.compareAndSwapObject(user, nameOffset, "jsbintask", "jsbintask2");
        System.out.println("第二次更新后的值:" + user.getName());
    }
}

class User {
    private String name;

    public User(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

Unsafe

因为内存中name的值为"jsbintask",而第一次使用compareAndSwapObject方法预期值为"jsbintask1",这显然是不相等的,所以第一次更新失败,第二次我们传入了正确的预期值,更新成功!

如果我们分析juc包下的Atomic开头的原子类就会发现,它内部的原子操作全部来源于unsafe的CAS方法,比如AtomicInteger的getAndIncrement方法,内部直接调用unsafe的getAndAddInt方法,它的实现原理为:cas失败,就循环,直到成功为止,这就是我们所说的自旋锁

Unsafe

内存分配

Unsafe还给我们提供了直接分配内存,释放内存,拷贝内存,内存设置等方法,值得注意的是,这里的内存指的是堆外内存!它是不受jvm内存模型掌控的,所以使用需要及其小心:

//分配内存, 相当于C++的malloc函数
public native long allocateMemory(long bytes);
//释放内存
public native void freeMemory(long address);
//在给定的内存块中设置值
public native void setMemory(Object o, long offset, long bytes, byte value);
//内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);

我们可以写一段代码验证一下:

public static void main(String[] args) throws Exception {
    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    theUnsafe.setAccessible(true);
    Unsafe unsafe = (Unsafe) theUnsafe.get(null);

    // 分配 10M的堆外内存
    long _10M_Address = unsafe.allocateMemory(1 * 1024 * 1024 * 10);
    // 将10M内存的 前面1M内存值设置为10
    unsafe.setMemory(_10M_Address, 1 * 1024 * 1024 * 1, (byte) 10);
    // 获取第1M内存的值: 10
    System.out.println(unsafe.getByte(_10M_Address + 1000));
    // 获取第1M内存后的值: 0(没有设置)
    System.out.println(unsafe.getByte(_10M_Address + 1 * 1024 * 1024 * 5));
}
Unsafe

我们分配了10M内存,并且将前1M内存的值设置为了10,取出了内存中的值进行比较,验证了unsafe的方法。

堆外内存不受jvm内存模型掌控,在nio(netty,mina)中大量使用对外内存进行管道传输,copy等,使用它们的好处如下:

  • 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。
  • 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
    而在jdk中,堆外内存对应的类为DirectByteBuffer,它内部也是通过unsafe分配的内存:
    Unsafe

    这里值得注意的是,对外内存的回收借助了Cleaner这个类。

线程调度

通过Unsafe还可以直接将某个线程挂起,这和调用Object.wait()方法作用是一样的,但是效率确更高!

Unsafe

我们熟知的AQS(AbstractQueuedSynchronizer)内部挂起线程使用了LockSupport方法,而LockSupport内部依旧使用的是Unsafe:
Unsafe

我们同样可以写一段代码验证:

public static void main(String[] args) throws Exception {
    Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    theUnsafe.setAccessible(true);
    Unsafe unsafe = (Unsafe) theUnsafe.get(null);

    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 10; i++) {
            if (i == 5) {
                // i == 5时,将当前线程挂起
                unsafe.park(false, 0L);
            }
            System.out.println(Thread.currentThread().getName() + " printing i : " + i);
        }
    }, " Thread__Unsafe__1");

    t1.start();

    // 主线程休息三秒
    Thread.sleep(3000L);
    for (int i = 0; i < 10; i++) {
        System.out.println(Thread.currentThread().getName() + " printing i : " + i);
        if (i == 9) {
            // 将线程 t1 唤醒
            unsafe.unpark(t1);
        }
    }

    System.in.read();
}
Unsafe

当线程t1运行到i=5时,被挂起,主线程执行,而主线程运行到i=9时,将t1唤醒,t1继续打印! 在park出debug可以观察t1线程的状态:


Unsafe

Unsafe

数组操作

对于数组,Unsafe提供了特别的方法返回不同类型数组在内存中的偏移量:

Unsafe

arrayBaseOffset方法返回数组在内存中的偏移量,这个值是固定的。arrayIndexScale返回数组中的每一个元素的内存地址换算因子。举个栗子,double数组(注意不是包装类型)每个元素占用8个字节,所以换算因子为8,int类型则为4,通过这两个方法我们就能定位数组中每个元素的内存地址,从而赋值,下面代码演示:

public static void main(String[] args) throws Exception{
    Class<Unsafe> unsafeClass = Unsafe.class;
    Constructor<Unsafe> constructor = unsafeClass.getDeclaredConstructor();
    constructor.setAccessible(true);
    Unsafe unsafe = constructor.newInstance();

    Integer[] integers = new Integer[10];
    // 打印数组的原始值
    System.out.println(Arrays.toString(integers));
    // 获取Integer数组在内存中的固定的偏移量
    long arrayBaseOffset = unsafe.arrayBaseOffset(Integer[].class);
    System.out.println(unsafe.arrayIndexScale(Integer[].class));
    System.out.println(unsafe.arrayIndexScale(double[].class));
    // 将数组中第一个元素的更新为100
    unsafe.putObject(integers, arrayBaseOffset, 100);
    // 将数组中第五个元素更新为50  注意 引用类型占用4个字节,所以内存地址 需要 4 * 4 = 16
    unsafe.putObject(integers, arrayBaseOffset + 16, 50);
    // 打印更新后的值
    System.out.println(Arrays.toString(integers));
}
Unsafe

我们通过获取Integer数组的内存偏移量,结合换算因子将第一个元素,第五个元素分别替换为了100,50。验证了我们的说法。

数组的原子操作,juc包也已经提供了相应的工具类,比如AtomicIntegerArray内部就是同过Unsafe的上述方法实现了数组的原子操作。
[图片上传失败...(image-3d8cc-1557067323454)]

其它操作

Unsafe还提供了操作系统级别的方法如获取内存页的大小public native int pageSize();,获取系统指针大小public native int addressSize();
jdk8还加入了新的方法,内存屏障,它的目的是为了防止指令重排序(编译器为了优化速度,会在保证单线程不出错的情况下将某些代码的顺序调换,比如先分配内存,或者先返回引用等,这样在多线程环境下就会出错):

//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();

jdk1.8引入的StampedLock就是基于此实现的乐观读写锁.
另外,jdk1.8引入了lambda表达式,它其实会帮我们调用Unsafe的public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);方法生成匿名内部类,如下面的代码:

public class UnsafeTest2 {
    public static void main(String[] args) {
        Function<String, Integer> function = Integer::parseInt;
        System.out.println(function.apply("100"));
    }
}

查看字节码:

Unsafe

发现它调用了LambdaMetafactory.metafactory方法,最终调用了InnerClassLambdaMetafactory的spinInnerClass方法:
Unsafe

总结

通过反射可以获取Unsafe类的实例,他可以帮助我们进行堆外内存操作,内存copy,内存复制,线程挂起,提供了cpu级别的cas原子操作。另外还有lambda的匿名内部类的生成,数组内存操作等。juc包基本全部基于此类实现!

关注我,这里只有干货!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • Java8张图 11、字符串不变性 12、equals()方法、hashCode()方法的区别 13、...
    Miley_MOJIE阅读 3,635评论 0 11
  • Java SE 基础: 封装、继承、多态 封装: 概念:就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽...
    Jayden_Cao阅读 2,048评论 0 8
  • CAS(Compare And Swap)意为比较并且交换,CAS它是一个原子操作。CAS操作涉及到三个值:当前内...
    zhong0316阅读 2,185评论 0 10
  • 第二部分 自动内存管理机制 第二章 java内存异常与内存溢出异常 运行数据区域 程序计数器:当前线程所执行的字节...
    小明oh阅读 1,089评论 0 2
  • 最近太忙碌了,有好几天都没有回家,所以公众号也没怎么更新。好在没有多少人关注,也没有给我开评论权限。并且写的文章并...
    武陵笑笑生阅读 411评论 0 1