Unsafe是jdk提供的一个直接访问操作系统资源的工具类(底层c++实现),它可以直接分配内存,内存复制,copy,提供cpu级别的CAS乐观锁等操作。Unsafe位于sun.misc包下,jdk中的并发编程包juc(java.util.concurrent)基本全部靠Unsafe实现,由此可见其重要性。它的目的是为了增强java语言直接操作底层资源的能力,无疑带来很多方便。但是,使用的同时就得额外小心!它的总体作用如下:
Unsafe被设计为单例,并且只允许被引导类加载器(BootstrapClassLoader)加载的类使用:
@CallerSensitive
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe");
return theUnsafe;
}
所以我们自己写的类是无法直接通过Unsafe.getUnsafe()获取的。所以只能通过反射直接new一个或者将其内部静态成员变量theUnsafe
获取出来:
public static void main(String[] args) throws Exception{
Class<Unsafe> unsafeClass = Unsafe.class;
//方法一:通过反射构造一个Unsafe对象
Constructor<Unsafe> constructor = unsafeClass.getDeclaredConstructor();
constructor.setAccessible(true);
Unsafe unsafe1 = constructor.newInstance();
System.out.println(unsafe1);
//方法二:获取内部静态成员变量
Field theUnsafe = unsafeClass.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe2 = (Unsafe) theUnsafe.get(null);
System.out.println(unsafe2);
}
可以得到以下结果:
sun.misc.Unsafe@3b192d32
sun.misc.Unsafe@12bb4df8
现在我们能够在自己代码里面使用Unsafe了,接下来看下它的使用以及jdk使用操作的。
CAS译为Compare And Swap,它是乐观锁的一种实现。假设内存值为o,预期值为expect,想要更新成得值为x,当且仅当内存值o等于预期值expect时,才将o更新为x。 这样可以有效避免多线程环境下的同步问题。
在unsafe中,实现CAS算法通过cpu的原子指令cmpxchg实现,它对应的方法如下:
@ForceInline
public final boolean compareAndSwapObject(Object o, long offset,
Object expected,
Object x) {
return theInternalUnsafe.compareAndSetObject(o, offset, expected, x);
}
@ForceInline
public final boolean compareAndSwapInt(Object o, long offset,
int expected,
int x) {
return theInternalUnsafe.compareAndSetInt(o, offset, expected, x);
}
@ForceInline
public final boolean compareAndSwapLong(Object o, long offset,
long expected,
long x) {
return theInternalUnsafe.compareAndSetLong(o, offset, expected, x);
}
简单介绍下它使用的参数:
为了方便理解,举个栗子。类User有一个成员变量name。我们new了一个对象User后,就知道了它(User对象)在内存中的起始值
,而成员变量name在对象中的位置偏移是固定的
。这样通过这个起始值和这个偏移量就能够定位到成员变量name在内存中的具体位置
。
所以我们现在的问题就是如何得出name在对象User中的偏移量,Unsafe自然也提供了相应的方法:
@ForceInline
public long objectFieldOffset(Field f) {
return theInternalUnsafe.objectFieldOffset(f);
}
@ForceInline
public long staticFieldOffset(Field f) {
return theInternalUnsafe.staticFieldOffset(f);
}
他们分别为获取成员变量和静态成员变量的方法,所以我们可以使用unsafe直接更新内存中的值:
import sun.misc.Unsafe;
import java.lang.reflect.*;
public class UnsafeDemo {
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"));
boolean res1 = unsafe.compareAndSwapObject(user, nameOffset, "jsbintask1", "jsbintask2");
System.out.println(res1+", 第一次更新后的值:" + user.getName());
boolean res2 = unsafe.compareAndSwapObject(user, nameOffset, "jsbintask", "jsbintask2");
System.out.println(res2+", 第二次更新后的值:" + user.getName());
}
public static class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
}
输出:
false, 第一次更新后的值:jsbintask
true, 第二次更新后的值:jsbintask2
因为内存中name的值为"jsbintask",而第一次使用compareAndSwapObject方法预期值为"jsbintask1",这显然是不相等的,所以第一次更新失败,返回false。第二次我们传入了正确的预期值,返回true,更新成功!
如果我们分析juc包下的Atomic开头的原子类就会发现,它内部的原子操作全部来源于unsafe的CAS方法
,比如AtomicInteger的getAndIncrement方法,内部直接调用unsafe的getAndAddInt方法,它的实现原理为:cas失败,就循环,直到成功为止,这就是我们所说的自旋锁!
对于数组,Unsafe提供了特别的方法返回不同类型数组在内存中的偏移量:
@ForceInline
public int arrayBaseOffset(Class<?> arrayClass) {
return theInternalUnsafe.arrayBaseOffset(arrayClass);
}
@ForceInline
public int arrayIndexScale(Class<?> arrayClass) {
return theInternalUnsafe.arrayIndexScale(arrayClass);
}
arrayBaseOffset方法返回数组在内存中的偏移量
,这个值是固定的。arrayIndexScale返回数组中的每一个元素的内存地址换算因子
。举个栗子,double数组(注意不是包装类型)每个元素占用8个字节,所以换算因子为8,int类型则为4,通过这两个方法我们就能定位数组中每个元素的内存地址,从而赋值,下面代码演示:
import sun.misc.Unsafe;
import java.lang.reflect.*;
import java.util.*;
public class UnsafeDemo {
public static void main(String[] args) throws Exception {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
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));
}
}
输出:
[null, null, null, null, null, null, null, null, null, null]
4
8
[100, null, null, null, 50, null, null, null, null, null]
我们通过获取Integer数组的内存偏移量,结合换算因子将第一个元素,第五个元素分别替换为了100,50。验证了我们的说法。
设置值,不管java的访问限制。
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));
}
我们分配了10M内存,并且将前1M内存的值设置为了10,取出了内存中的值进行比较,验证了unsafe的方法。
堆外内存不受jvm内存模型掌控,在nio(netty,mina)中大量使用对外内存进行管道传输,copy等,使用它们的好处如下:
而在jdk中,堆外内存对应的类为DirectByteBuffer
,它内部也是通过unsafe分配的内存
:
DirectByteBuffer(int cap) {
// package-private
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 {
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;
}
这里值得注意的是,对外内存的回收借助了Cleaner这个类。
通过Unsafe还可以直接将某个线程挂起,这和调用Object.wait()方法作用是一样的,但是效率确更高!
@ForceInline
public void unpark(Object thread) {
theInternalUnsafe.unpark(thread);
}
@ForceInline
public void park(boolean isAbsolute, long time) {
theInternalUnsafe.park(isAbsolute, time);
}
我们熟知的AQS(AbstractQueuedSynchronizer)内部挂起线程使用了LockSupport
,而LockSupport内部依旧使用的是Unsafe:
public static void unpark(Thread thread) {
if (thread != null)
U.unpark(thread);
}
Unsafe还提供了操作系统级别的方法如获取内存页的大小public native int pageSize();,获取系统指针大小public native int addressSize();
jdk8还加入了新的方法,内存屏障,它的目的是为了防止指令重排序
(编译器为了优化速度,会在保证单线程不出错的情况下将某些代码的顺序调换,比如先分配内存,或者先返回引用等,这样在多线程环境下就会出错),jdk1.8引入的StampedLock就是基于此实现的乐观读写锁。:
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();
另外,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"));
}
}