Unsafe类一半天堂一半地狱

Unsafe类,全限定名是sun.misc.Unsafe,从名字中我们可以看出来这个类对 普通程序员来说是“危险”的,一般应用开发者不会用到这个类。但是,几乎每个使用 java开发的工具、软件基础设施、高性能开发库都在底层使用了 sun.misc.Unsafe;(比如Netty、Cassandra、Hadoop、Kafka等)

Unsafe类官方并不对外开放,因为Unsafe类支持硬件级别的原子操作,提供了一些绕开JVM的更底层功能,通过它可以提高效率。Unsafe API的大部分方法都是native实现。

Java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C++手动管理内存的能力。

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,一旦能够直接操作内存,这也就意味着
1、不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏。
2、Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就
是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
3、直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。

因此,从上面三个角度来看,虽然在一定程度上提升了效率但是也带来了指针的不安全性。

下面直接上源码~

// final 类,不允许继承。
public final class Unsafe {
    private static final Unsafe theUnsafe;
    public static final int INVALID_FIELD_OFFSET = -1;
    ...
    public static final int ADDRESS_SIZE;

    // 注册native方法,使得Unsafe类可以操作C语言
    private static native void registerNatives();

    // 构造函数是private的,不允许外部实例化
    private Unsafe() {
    }

    // 初始化方法实现
    // 采用单例模式,不是系统加载初始化就会抛出SecurityException异常。
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

怎么获取Unsafe ?

这个类尽管里面的方法都是 public 的,但是并没有办法使用它们;
该类功能很强大,涉及到类加载机制,其实例一般情况是获取不到的。只能通过反射来获取Unsafe;

public Unsafe getUnsafe() throws IllegalAccessException {
    Field unsafeField = Unsafe.class.getDeclaredFields()[0];
    unsafeField.setAccessible(true);
    Unsafe unsafe = (Unsafe) unsafeField.get(null);
    return unsafe;
}

Unsafe主要功能

图片.png
普通读写

Unsafe还可以直接在一个内存地址上读写。
通过Unsafe可以读写一个类的属性,即使这个属性是私有的,也可以对这个属性进行读写。

读写一个Object属性的相关方法

# 获取对象obj中,偏移地址为offset的属性值,不受修饰符的限制
public native Object getObject(Object obj, long offset);

# 用于从对象的指定偏移地址处读取一个int。
public native int getInt(Object var1, long var2);

# 用于在对象指定偏移地址处写入一个int。
public native void putInt(Object var1, long var2, int var4);

# 用于从指定内存地址处开始读取一个byte。
public native byte getByte(long var1);

# 用于从指定内存地址写入一个byte。
public native void putByte(long var1, byte var3);

其他的primitive type也有对应的方法。
volatile读写

普通的读写无法保证可见性和有序性,而volatile读写就可以保证可见性和有序性。

# 强制从主内存中获取属性值
public native Object getObjectVolatile(Object o, long offset);

# getIntVolatile方法用于在对象指定偏移地址处volatile读取一个int。
public native int getIntVolatile(Object var1, long var2);

# putIntVolatile方法用于在对象指定偏移地址处volatile写入一个int。
public native void putIntVolatile(Object var1, long var2, int var4);

volatile读写相对普通读写是更加昂贵的,因为需要保证可见性和有序性,而与volatile写入相比putOrderedXX写入代价相对较低,putOrderedXX写入不保证可见性,但是保证有序性,所谓有序性,就是保证指令不会重排序。

有序写入

有序写入只保证写入的有序性,不保证可见性,就是说一个线程的写入不保证其他线程立马可见。

# 将对象var1中,偏移地址为var2的属性(object类型)值改为var4。
# 保证有序性,不保证可见性
public native void putOrderedObject(Object var1, long var2, Object var4);

public native void putOrderedInt(Object var1, long var2, int var4);

public native void putOrderedLong(Object var1, long var2, long var4);
直接内存操作

我们都知道Java不可以直接对内存进行操作,对象内存的分配和回收都是由JVM帮助我们实现的。但是Unsafe为我们在Java中提供了直接操作内存的能力。

# 获取本地指针的大小(单位是byte),通常值为4或者8。常量ADDRESS_SIZE就是调用此方法。
public native int addressSize();

# 获取本地内存的页数,此值为2的幂次方。
public native int pageSize();

# 分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址。
public native long allocateMemory(long var1);

# 通过指定的内存地址address重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)。
public native long reallocateMemory(long var1, long bytes);

# 将给定内存块中的所有字节设置为固定值(通常是0)。
public native void setMemory(long var1, long var3, byte var5);

# 内存复制
public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);

# 清除内存
public native void freeMemory(long var1);
CAS相关

CAS(Compare and Swap 即比较并交换),java并发常用到的一种技术,java.util.concurrent 包完全建立在 CAS 之上,没有 CAS 也就没有此包,可见 CAS 的重要性。(JUC中大量运用了CAS操作,CAS就是JUC的基础)

现在的处理器基本都支持 CAS,只不过不同的厂家的实现不一样罢了。CAS 有三个操作数:内存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做并返回 false。

Unsafe中提供了int,long和Object的CAS操作

final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

CAS一般用于乐观锁,它在Java中有广泛的应用;
ConcurrentHashMap,ConcurrentLinkedQueue中都有用到CAS来实现乐观锁。

ABA问题:
如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?如果在这段期间它的值曾经被改成了 B,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。

解决:java.util.concurrent 包为了解决这个问题,提供了一个带有标记的原子引用类 ”AtomicStampedReference”,它可以通过控制变量值的版本来保证 CAS 的正确性。不过目前来说这个类比较”鸡肋”,大部分情况下 ABA 问题并不会影响程序并发的正确性,如果需要解决 ABA 问题,使用传统的互斥同步可能会比原子类更加高效。

CAS 看起来很美,但这种操作显然无法涵盖并发下的所有场景

偏移量相关
# 用于获取静态属性Field在对象中的偏移量,读写静态属性时必须获取其偏移量。
public native long staticFieldOffset(Field var1);

# 用于获取非静态属性Field在对象实例中的偏移量,读写对象的非静态属性时会用到这个偏移量。
public native long objectFieldOffset(Field var1);

# 用于返回Field所在的对象。
public native Object staticFieldBase(Field var1);

# 用于返回数组中第一个元素实际地址相对整个数组对象的地址的偏移量。
public native int arrayBaseOffset(Class<?> var1);

# 用于计算数组中第一个元素所占用的内存空间
# 返回数组中元素与元素之间的偏移地址的增量
public native int arrayIndexScale(Class<?> var1);

arrayBaseOffset + arrayIndexScale这两个方法配合使用就可以定位到任何一个元素的地址
由于Java的数组最大值为Integer.MAX_VALUE,使用Unsafe类的内存分配方法可以实现超大数组。
实际上这样的数据就可以认为是C数组,因此需要注意在合适的时间释放内存。
线程调度
# 释放被park创建的在一个线程上的阻塞。由于其不安全性,因此必须保证线程是存活的。
public native void unpark(Object var1);

# 阻塞当前线程,一直等到unpark方法被调用。
public native void park(boolean var1, long var2);

public native void monitorEnter(Object var1);

public native void monitorExit(Object var1);

public native boolean tryMonitorEnter(Object var1);

monitorEnter方法和monitorExit方法用于加锁,Java中的synchronized锁就是通过这两个指令来实现的。

// 挂起线程
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    // 通过Unsafe的putObject方法设置阻塞当前线程的blocker
    setBlocker(t, blocker); 
    // 通过Unsafe的park方法来阻塞当前线程,注意此方法将当前线程阻塞后;
    // 当前线程就不会继续往下走了,直到其他线程unpark此线程
    UNSAFE.park(false, 0L); 
    // 清除blocker
    setBlocker(t, null); 
}

// 唤醒线程
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

看过LockSupport类的都不会陌生;整个并发框架中对线程的挂起操作被封装在LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。

类加载
# 定义一个类,用于动态地创建类。
public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);

# 用于动态的创建一个匿名内部类。
public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);

# 用于创建一个类的实例,但是不会调用这个实例的构造方法,如果这个类还未被初始化,则初始化这个类。
# 在对象反序列化的时候很有用,能够重建和设置final字段,而不需要调用构造方法
public native Object allocateInstance(Class<?> var1) throws InstantiationException;

# 用于判断是否需要初始化一个类。
public native boolean shouldBeInitialized(Class<?> var1);

# 用于保证已经初始化过一个类。
public native void ensureClassInitialized(Class<?> var1);
内存屏障
# 保证在这个屏障之前的所有读操作都已经完成。
public native void loadFence();

# 保证在这个屏障之前的所有写操作都已经完成。
public native void storeFence();

# 保证在这个屏障之前的所有读写操作都已经完成。
public native void fullFence();
八卦

在很久之前盛传着这个类将要在jdk9移除,事实上如果移除了那么一大批框架将会消失,比如说赫赫有名的Netty框架。最终jdk9出现的时候也只是对其进行了改进和优化。不过这也再一次说明了这个类的重要地位。

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

推荐阅读更多精彩内容

  • 前一段时间在研究juc源码的时候,发现在很多工具类中都调用了一个Unsafe类中的方法,出于好奇就想要研究一下这个...
    码农参上阅读 895评论 1 1
  • Unsafe类 不能直接 new, 其构造函数被私有化 public final class Unsafe : U...
    lj72808up阅读 449评论 0 0
  • java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作。Java和C++...
    笨比乔治阅读 211评论 0 1
  • https://www.jianshu.com/p/db8dce09232d[https://www.jiansh...
    疯狂撸代码的奋青骚年阅读 235评论 0 0
  • Java和C++语言的一个重要区别就是Java中我们无法直接操作一块内存区域,不能像C++中那样可以自己申请内存和...
    随风_d6a2阅读 355评论 0 4