扫描下方二维码或者微信搜索公众号
菜鸟飞呀飞
,即可关注微信公众号,阅读更多Spring源码分析
和Java并发编程
文章。
在上一篇文章《初始CAS的实现原理》中,提到了Unsafe类相关方法,今天这篇文章将详细介绍Unsafe类的源码。
为什么要单独用一篇文章介绍Unsafe类呢?这是因为在看源码过程中,经常会碰到它,例如JUC包下的原子类、AQS、Netty等源码中,最终都会看见Unsafe类的使用。搞清楚Unsafe类的使用,对以后看源码会有很大的帮助。
rt.jar
中sun.misc
包下的类,从类名就能看出来,这个类是不安全的,但是它的功能十分强大。相比C和C++的开发人员,作为一名Java开发人员是十分幸福的,因为在Java中程序员在开发时不需要关注内存的管理,对象的回收,因为JVM全部都帮助我们完成了。如果Java开发人员需要自己手动去操作内存,那么可以通过Unsafe类去进行申请,这也是Unsafe类被定义为不安全
的类的原因,因为一不小心就容易出现忘记释放内存
等问题。笔者画了一张脑图,因为图片占用空间较大,为了不影响阅读,我把这张图放在了文章末尾,以供参考。
// 类被final修饰,表示不能被继承
public final class Unsafe {
// 构造器被私有化
private Unsafe() {}
private static final Unsafe theUnsafe = new Unsafe();
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
}
SecurityException
异常。例如如下示例:public class Demo {
public static void main(String[] args) {
Unsafe unsafe = Unsafe.getUnsafe();
}
}
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.tiantang.study.Demo.main(Demo.java:14)
SecurityException
异常呢?这是因为在Unsafe类的getUnsafe()
方法中,它做了一层校验,判断当前类(Demo)的类加载器(ClassLoader)是不是启动类加载器(Bootstrap ClassLoader)
,如果不是,则会抛出SecurityException
异常。在JVM的类加载机制中,自定义的类使用的类加载器是应用程序类加载器(Application ClassLoader)
,所以这个时候校验失败,会抛出异常。反射反射,程序员的快乐
)。反射的代码可以参考如下示例:public static void main(String[] args) {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 将字段的访问权限设置为true
field.setAccessible(true);
// 因为theUnsafe字段在Unsafe类中是一个静态字段,所以通过Field.get()获取字段值时,可以传null获取
Unsafe unsafe = (Unsafe) field.get(null);
// 控制台能打印出对象哈希码
System.out.println(unsafe);
} catch (Exception e) {
e.printStackTrace();
}
}
compareAndSwapInt()、compareAndSwapLong()、compareAndSwapObject()
这三个CAS方法都是native方法,具体实现是在JVM中实现,它们的作用是比较并交换,这个操作是原子操作。关于CAS更详细的讲解可以参考这篇文章:初识CAS的实现原理。 protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 调用unsafe申请内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 初始化内存
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
io.netty.buffer.UnpooledUnsafeDirectByteBuf
类申请内存时的源码如下:public class UnpooledUnsafeDirectByteBuf extends AbstractReferenceCountedByteBuf {
protected ByteBuffer allocateDirect(int initialCapacity) {
// 调用ButeBuffer来申请堆外内存,ButeBuffer是java.nio包下的内
return ByteBuffer.allocateDirect(initialCapacity);
}
}
java.nio.ByteBuffer
类是通过DirectByteBuffer类来操作内存,DirectByteBuffer又是通过Unsafe类来操作内存,所以最终实际上Netty对堆外的内存的操作是通过Unsafe类中的API来实现的。LockSupport.park()、LockSupport.unpark()
方法来进行线程间的通信。LockSupport中的这些方法最终调用的是Unsafe类的park()和unPark()。下面是LockSupport类的部分源代码。public class LockSupport {
// UNSAFE是Unsafe类的实例
public static void park() {
// 阻塞线程
UNSAFE.park(false, 0L);
}
public static void unpark(Thread thread) {
if (thread != null)
// 唤醒线程
UNSAFE.unpark(thread);
}
}
arrayBaseOffset()、arrayIndexScale()
。// 返回数组中第一个元素在内存中的偏移量
public native int arrayBaseOffset(Class<?> arrayClass);
// 返回数组中每个元素占用的内存大小,单位是字节
public native int arrayIndexScale(Class<?> arrayClass);
public class AtomicIntegerArray implements java.io.Serializable {
private static final long serialVersionUID = 2862133569453604235L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 获取数组中第一元素在内存中的偏移量
private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int shift;
private final int[] array;
static {
// 获取数组中每个元素占用的内存大小
// 对于int类型的元素,占用的是4个字节大小,所以此时返回的是4
int scale = unsafe.arrayIndexScale(int[].class);
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
shift = 31 - Integer.numberOfLeadingZeros(scale);
}
private static long byteOffset(int i) {
// 根据数组中第一个元素在内存中的偏移量和每个元素占用的大小,
// 计算出数组中第i个元素在内存中的偏移量
return ((long) i << shift) + base;
}
}
getObject()和putObject()
,一个是从内存中获取给定对象的指定偏移量的Object类型对象,一个是向内存中写。与此类似的还有getInt()、getLong()…等方法。还有一组加了volatile语义的方法,例如:getObjectValotile()、putObjectVolatile()
,它们的作用就是使用volatile语义获取值和存储值。什么是volatile语义呢?就是读数据时每次都从内存中取最新的值,而不是使用CPU缓存中的值;存数据时将值立马刷新到内存,而不是先写到CPU缓存,等以后再刷新回内存。部分方法注释如下://从对象o的指定地址偏移量offset处获取变量的引用,与此类似方法有:getInt,getLong等等
public native Object getObject(Object o, long offset);
//对对象o的指定地址偏移量offset处设值,与此类似方法有:putInt,putLong等等
public native void putObject(Object o, long offset, Object x);
//从对象o的指定地址偏移量offset处获取变量的引用,使用volatile语义读取,与此类似方法有:getIntVolatile,getLongVolatile等等
public native Object getObjectVolatile(Object o, long offset);
//对对象o的指定地址偏移量offset处设值,使用volatile语义存储,与此类似方法有:putIntVolatile,putLongVolatile等等
public native void putObjectVolatile(Object o, long offset, Object x);
objectFieldOffset()
。它的作用是获取对象的某个非静态字段相对于该对象
的偏移地址,它与staticFieldOffset()
的作用类似,但是存在一点区别。staticFieldOffset()获取的是静态字段相对于类对象(即类所对应的Class对象)的偏移地址。静态字段存在于方法区中,静态字段每次获取的偏移量的值都是相同的。// 获取对象的某个非静态字段相对于该对象的偏移地址
public native long objectFieldOffset(Field f);
objectFieldOffset()
的应用场景十分广泛,因为在Unsafe类中,大部分API方法都需要传入一个offset参数,这个参数表示的是偏移量,要想直接操作内存中某个地址的数据,就必须先找到这个数据在哪儿,而通过offset就能知道这个数据在哪儿。因此这个方法应用得十分广泛,下面以AtomicInteger类为例:在静态代码块中,通过objectFieldOffset()
获取了value属性在内存中的偏移量,这样后面将value写入到内存时,就能根据offset来写入了。public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 在static静态块中调用objectFieldOffset()方法,获取value字段在内存中的偏移量
// 因为后面AtomicInteger在进行原子操作时,需要调用Unsafe类的CAS方法,而这些方法均需要传入offset这个参数
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
}
staticFieldOffset()
,获取静态字段的对象指针:staticFieldBase()
。// 获取给定静态字段的偏移量
public native long staticFieldOffset(Field f);
// 获取给定静态字段的对象指针
public native Object staticFieldBase(Field f);
invokedynimic
和VM Anonymous Class
模板机制来实现的,VM Anonymous Class
模板机制最终会使用到Unsafe类的defineAnonymousClass()方法来创建匿名类。对这一块感兴趣的朋友可以去查阅一下相关的资料,欢迎分享。 // 定义一个匿名内部类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
loadFence()、 storeFence() 、fullFence()
。// 禁止load操作重排序
public native void loadFence();
// 禁止store操作重排序
public native void storeFence();
// 禁止load和store操作重排序
public native void fullFence();
// 获取指针的大小,单位是字节。
// 对于64位系统,返回8,表示指针大小是8字节
// 对于32位系统,返回4,表示指针大小是4字节
public native int addressSize();
// 返回内存页的大小,单位是字节。返回值一定是2的多少次幂
public native int pageSize();
public static void main(String[] args) {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
// 指针大小
System.out.println(unsafe.addressSize());
// 内存页大小
System.out.println(unsafe.pageSize());
}
8
4096
java.nio.Bits
类中有实际应用。Bits作为工具类,提供了计算所申请内存需要占用多少内存页的方法,这个时候需要知道硬件的内存页大小,才能计算出占用内存页的数量。因此在这里借助了Unsafe.pageSize()方法来实现。Bits
类的部分源码如下。class Bits {
static int pageSize() {
if (pageSize == -1)
// 获取内存页大小
pageSize = unsafe().pageSize();
return pageSize;
}
// 根据内存大小,计算需要的内存页数量
static int pageCount(long size) {
return (int)(size + (long)pageSize() - 1L) / pageSize();
}
}
objectFieldOffset(Field f)
这个方法很常用,它是获取字段在内存中的偏移量,通常和Unsafe类中的其他方法结合使用。通过这个方法能知道要修改的数据在内存中的位置,然后再通过Unsafe类中其他方法来根据数据在内存中的位置从而来修改数据。