Java基础、中级、高级、架构面试资料

Unsafe 的 CAS 和内存操作的原理、源码解毒

JAVA herman 2410浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog2,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

Java 语言的一大特点就是跨平台,并且提供的有一套完美的内存管理机制。但这都是 JVM 提供的,如果我们想要直接访问系统内存资源、自主管理内存资源等就无法实现。于是 Java 又提供了一个魔法类:Unsafe。

Unsafe 类位于 sun.misc 包中。从名字看,这个类就是一个不安全的类,实际上它确实是封装了一些不安全的操作!

Unsafe 类和 String 类一样的被定义为 final,也就是说它不可以被继承。并且 Unsafe 被设计成了单例,构造函数是私有的,只能通过 getUnsafe 方法获得它。除此之外,getUnsafe 方法还设置了限制条件,只有授信的代码才能获得该类的实例。哪些是授信的代码呢?当然是 JDK 库里面的类是可以随意使用的。

public final class Unsafe {
  // 单例对象
  private static final Unsafe theUnsafe;
  private Unsafe() {}
  @CallerSensitive
  public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    // 仅在引导类加载器`BootstrapClassLoader`加载时才合法
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {    
      throw new SecurityException("Unsafe");
    } else {
      return theUnsafe;
    }
  }
  ...... 省略其他代码
}

说了半天,这个类,我们无法使用,你讲它又何意义?

别急,Java 虽然不建议我们使用它,但是我们还是可以通过两种方式来使用它。

第一种方式是:让我们的代码在启动时“授信”。在运行程序时,指定 bootclasspath 选项,让你使用 Unsafe 实例的类被引导类加载器加载,从而通过 Unsafe.getUnsafe 方法安全的获取 Unsafe 实例。

java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:. com.xttblog.UnsafeTest

这个做法比较少用,所以推荐大家采用第二种方法:通过反射来使用它。

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

注意有的 IDE 可能支持的不是很友好。比如:eclipse 显示”Access restriction…”错误,但如果你运行代码,它将正常运行。如果这个错误提示令人烦恼,可以通过以下设置来避免:

Preferences -> Java -> Compiler -> Errors/Warnings ->
Deprecated and restricted API -> Forbidden reference -> Warning

Unsafe 有 8 大功能,很多号主只讲了它的 CAS 功能。

Unsafe 的 8 大功能

如上图所示,Unsafe 提供的 105 个 API 大致可分为内存操作、CAS、Class 相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等。今天我先来说两个大功能:CAS 和内存操作(和我前面的《手把手教你通过Java代码体验强引用、软引用、弱引用、虚引用的区别》、《90%的程序员可能都不了解的堆外内存》都有些关联,这是一个系列)。

CAS 操作主要涉及到下面 3 个 API。

/**
  *  CAS
  * @param o         包含要修改field的对象
  * @param offset    对象中某field的偏移量
  * @param expected  期望值
  * @param update    更新值
  * @return          true | false
  */
public final native boolean compareAndSwapObject(Object o, 
  long offset,  Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, 
  long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, 
  long offset, long expected, long update);

CAS 即比较并替换,实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg。

CAS 在 java.util.concurrent.atomic 相关类、Java AQS、CurrentHashMap 等实现上有非常广泛的应用。比如,在 AtomicInteger 的实现中,静态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset 的值在 AtomicInteger 初始化时,在静态代码块中通过 Unsafe 的 objectFieldOffset 方法获取。在 AtomicInteger 中提供的线程安全方法中,通过字段 valueOffset 的值可以定位到 AtomicInteger 对象中 value 的内存地址,从而可以根据 CAS 实现对 value 字段的原子操作。

CAS 原子性操作原理

比如,下图就为某个 AtomicInteger 对象自增操作前后的内存示意图,对象的基地址 baseAddress=“0x110000”,通过 baseAddress+valueOffset 得到 value 的内存地址 valueAddress=“0x11000c”;然后通过 CAS 进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止。

图解 AtomicInteger CAS 操作过程

说完 CAS,我们再来说说 Unsafe 的内存操作。

内存操作主要有下面 9 个 API。

//分配内存, 相当于C++的malloc函数
public native long allocateMemory(long bytes);
//扩充内存
public native long reallocateMemory(long address, 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);
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
public native byte getByte(long address);
//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);

在《手把手教你通过Java代码体验强引用、软引用、弱引用、虚引用的区别》和《90%的程序员可能都不了解的堆外内存》两篇文章中,我已经讲过了。在 Java 中创建的对象都处于堆内内存(heap)中,堆内内存是由 JVM 所管控的 Java 进程内存,并且它们遵循 JVM 的内存管理机制,JVM 会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于 JVM 管控之外的内存区域,Java 中对堆外内存的操作,依赖于 Unsafe 提供的操作堆外内存的 native 方法。

使用堆外内存的原因是:

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

我前面提到的 DirectByteBuffer,在 Netty、MINA 等 NIO 框架中应用广泛。DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。

DirectByteBuffer 源码解读

上图为 DirectByteBuffer 构造函数,创建 DirectByteBuffer 的时候,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存一起被释放。具体的释放就是我前面讲的 PhantomReference 虚引用。

以上就是 Unsafe 类 8 大主要功能的 2 个重要的功能。其他功能,我会在对应使用到的框架脑图中串起来讲。当然,如果你们现在希望了解的话,我也可以提前写一下这方便的内容。选择权在于你们的留言和评论!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » Unsafe 的 CAS 和内存操作的原理、源码解毒