Unsafe 类的介绍和使用

本贴最后更新于 1908 天前,其中的信息可能已经时移世易

🌹🌹 如果您觉得我的文章对您有帮助的话,记得在 GitHub 上 star 一波哈 🌹🌹

🌹🌹GitHub_awesome-it-blog 🌹🌹


近期在看 JDK8 的 ConcurrentHashMap 源码时,发现里面大量用到了 Unsafe 类的 API,这里来深入研究一下。

0 简介

Java 是一个安全的面向对象的编程语言。这里的安全指的是什么呢?不妨从什么是不安全的角度来看看。

啥是不安全呢?这里以 C 语言为例,在 C 语言中:

  • 当编写了开辟内存的代码之后,需要想着手动去回收这块内存,否则会造成内存泄漏;
  • 指针的操作提供了很多的便利,但如果出现了例如计算指针指向地址错误的问题,就会产生很多意想不到的结果;

其他的不安全的情况这里不再一一列举。在 Java 中,很好的解决了 C 语言中诸多的不安全问题。例如用 GC 解决了内存回收的问题,而 Java 中本身没有指针的概念,只是提供了引用类型,而引用类型是无法直接修改其引用的内存地址的,所以指针误操作的问题也得到了有效的解决。

那么,在 Java 中有没有突破这些限制的方法呢?答案是肯定的,他就是今天要聊的 sum.misc.Unsafe 类。

使用 Unsafe 的 API,你可以:

  • 开辟内存:allocateMemory
  • 扩充内存:reallocateMemory
  • 释放内存:freeMemory
  • 在指定的内存块中设置值:setMemory
  • 未经安全检查的加载 Class:defineClass
  • 原子性的更新实例对象指定偏移内存地址的值:compareAndSwapObject
  • 获取系统的负载情况:getLoadAverage,等同于 linux 中的 uptime
  • 不调用构造函数来创建一个类的实例:allocateInstance

本文会介绍几个 API 的使用方式,但主要关注可用于处理多线程并发问题的几个 API:

  • compareAndSwapInt
  • getAndAddInt
  • getIntVolatile

1 在 OpenJDK 中查看 Unsafe 源码

想要了解 Unsafe,最直接的一个方式就是看源码啦。

但是从 Oracle 官方下载的 JDK 中,Unsafe 类是没有注释的。而 OpenJDK 中是有的,我们可以从 OpenJDK 源码入手。

下面介绍一下如何通过 OpenJDK 查看 Unsafe 源码(同样适用与查看其它类的源码以及查看 native 实现)。

1.1 下载 OpenJDK8

从下面链接下载

OpenJDK8

点下面这里下载即可

unsafe1.png

下载后是 zip 包,解压到一个地方就好。

1.2 下载 NetBeans

因为我们接下来要同时看 C++ 和 Java 的源码,NetBeans 是同时支持这两种语言的,所以这里通过 NetBeans 来看 OpenJDK 的源码。

下载地址为:
NetBeans 下载

http://137.254.56.27/download/trunk/nightly/latest

注意要下载这个 ALL 版本的,只有这个才能同时支持 Java 和 C++。

unsafe2.png

1.3 导入 OpenJDK 源码

  • 文件 -> 新建项目
  • 按下面这样选择,然后下一步

unsafe3.png

  • 选择刚才解压出来的 openjdk 根目录

unsafe4.png

  • 点击“完成”

Unsafe 的源码在 jdk/src/share/classes/sun/misc/Unsafe.java

2 Unsafe 的获取

通过源码发现,Unsafe 在 JVM 中是一个单例对象,我们不能直接去 new 它。

private Unsafe() {}

private static final Unsafe theUnsafe = new Unsafe();

继续往下看,然后可以发现有这样一个方法

/**
 * Provides the caller with the capability of performing unsafe
 * operations.
 *
 * <p> The returned <code>Unsafe</code> object should be carefully guarded
 * by the caller, since it can be used to read and write data at arbitrary
 * memory addresses.  It must never be passed to untrusted code.
 *
 * <p> Most methods in this class are very low-level, and correspond to a
 * small number of hardware instructions (on typical machines).  Compilers
 * are encouraged to optimize these methods accordingly.
 *
 * <p> Here is a suggested idiom for using unsafe operations:
 *
 * <blockquote><pre>
 * class MyTrustedClass {
 *   private static final Unsafe unsafe = Unsafe.getUnsafe();
 *   ...
 *   private long myCountAddress = ...;
 *   public int getCount() { return unsafe.getByte(myCountAddress); }
 * }
 * </pre></blockquote>
 *
 * (It may assist compilers to make the local variable be
 * <code>final</code>.)
 *
 * @exception  SecurityException  if a security manager exists and its
 *             <code>checkPropertiesAccess</code> method doesn't allow
 *             access to the system properties.
 */
@CallerSensitive
public static Unsafe getUnsafe() {
    Class<?> caller = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(caller.getClassLoader()))
        throw new SecurityException("Unsafe");
    return theUnsafe;
}

看似我们可以通过 Unsafe.getUnsafe()来获取 Unsafe 的实例。

其实不然,这个方法在 return 之前做了一个校验,他会通过 VM.isSystemDomainLoader 方法校验调用者的 ClassLoader,此方法的实现如下

/**
 * Returns true if the given class loader is in the system domain
 * in which all permissions are granted.
 */
public static boolean isSystemDomainLoader(ClassLoader loader) {
    return loader == null;
}

如果调用者的 ClassLoader==null,在 getUnsafe 方法中才可以成功返回实例,否则会抛出 SecurityException("Unsafe")异常。

啥时候是 null 呢?可以想到只有由启动类加载器(BootstrapClassLoader)加载的 class 才是 null。

PS:关于类加载器可以参考笔者的另一篇文章【深入分析 Java 类加载器原理】

所以在我们自己的代码中是不能直接通过这个方法获取 Unsafe 实例的。

还有啥别的办法么?有的!反射大法好!

在源码中可以发现,它是用 theUnsafe 字段来引用 unsafe 实例的,那我们可以尝试通过反射获取 theUnsafe 字段,进而获取 Unsafe 实例。代码如下:

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class UnsafeTest1 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        Class klass = Unsafe.class;
        Field field = klass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);
        System.out.println(unsafe.toString());
    }
}

运行此代码,没有报错,大功告成。

3 Unsafe 几个 API 的使用

3.1 通过内存偏移量原子性的更新成员变量的值

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * Description:
 *
 * @author zhiminxu
 * @package com.lordx.sprintbootdemo
 * @create_time 2019-03-22
 */
public class UnsafeTest {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        Test test = new Test();
        test.test();
    }
}

class Test {

    private int count = 0;

    public void test() throws NoSuchFieldException, IllegalAccessException {
        // 获取unsafe实例
        Class klass = Unsafe.class;
        Field field = klass.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);

        // 获取count域的Field
        Class unsafeTestClass = Test.class;
        Field fieldCount = unsafeTestClass.getDeclaredField("count");
        fieldCount.setAccessible(true);

        // 计算count的内存偏移量
        long countOffset = (int) unsafe.objectFieldOffset(fieldCount);
        System.out.println(countOffset);

        // 原子性的更新指定偏移量的值(将count的值修改为3)
        unsafe.compareAndSwapInt(this, countOffset, count, 3);

        // 获取指定偏移量的int值
        System.out.println(unsafe.getInt(this, countOffset));
    }
}

3.2 用 Unsafe 模拟 synchronized

用到了 Unsafe 中的 monitorEnter 和 monitorExit 方法,但 monitorEnter 后一定要记着 monitorExit。

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * Description:
 *
 * @author zhiminxu
 * @package com.lordx.sprintbootdemo
 * @create_time 2019-03-22
 */
public class UnsafeTest {

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
        final Test test = new Test();
        // 模拟两个线程并发给Test.count递增的场景
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    test.addCount();
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    test.addCount();
                }
            }
        }).start();
        Thread.sleep(5000);
        System.out.println(test.getCount());
    }
}

class Test {

    private int count = 0;

    public int getCount() {
        return this.count;
    }

    private Unsafe unsafe;
    public Test() {
        try {
            Class klass = Unsafe.class;
            Field field = klass.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Object lock = new Object();

    public void addCount() {
        // 给lock对象设置锁
        unsafe.monitorEnter(lock);
        count++;
        // 给lock对象解锁
        unsafe.monitorExit(lock);
    }


}

4 Unsafe 中几个线程安全 API 的实现原理

4.1 compareAndSwapInt

此方法在 Unsafe 中的源码为

/**
 * Atomically update Java variable to <tt>x</tt> if it is currently
 * holding <tt>expected</tt>.
 * 如果对象o指定offset所持有的值是expected,那么将它原子性的改为值x。
 * @return <tt>true</tt> if successful
 */
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

在 OpenJDK 中可以看到这个方法的 native 实现,在 unsafe.cpp 中。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  // #1
  oop p = JNIHandles::resolve(obj);
  // #2
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // #3
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

代码#1 将目标对象转换为 oop,oop 是本地实现中 oopDesc 类的实现,其定义在 oop.hpp 中。oopDesc 是所有 class 的顶层 baseClass,它描述了 Java object 的格式,使 Java object 中的 field 可以被 C++ 访问。

代码#2 负责获取 oop 中指定 offset 的内存地址,指针变量 addr 记录的就是这个地址中存储的 int 值。

代码#3 调用 Atomic::cmpxchg 来原子性的完成值得替换。

4.2 getAndAddInt

此方法的源码如下

/**
 * Atomically adds the given value to the current value of a field
 * or array element within the given object <code>o</code>
 * at the given <code>offset</code>.
 *
 * @param o object/array to update the field/element in
 * @param offset field/element offset
 * @param delta the value to add
 * @return the previous value
 * @since 1.8
 */
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

用于原子性的将值 delta 加到对象 o 的 offset 上。

getIntVolatile 方法用于获取对象 o 指定偏移量的 int 值,此操作具有 volatile 内存语义,也就是说,即使对象 o 指定 offset 的变量不是 volatile 的,次操作也会使用 volatile 语义,会强制从主存获取值。

然后通过 compareAndSwapInt 来替换值,直到替换成功后,退出循环。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3186 引用 • 8212 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...