sun.misc.Unsafe使用指南

Unsafe简介

Posted by Jay on December 9, 2018

sun.misc.Unsafe使用指南

转自 朱小厮的博客

Java是一个安全的开发语言,它阻止开发人员犯很多低级的错误,而大部分的错误都是基于内存管理方面的。如果想搞破坏,可以使用Unsafe这个类。这个类是属于sun.* API中的类,并且它不是J2SE中真正的一部分。

实例化sun.misc.Unsafe

如果尝试创建Unsafe类的实例,基于以下两种原因是不被允许的:

  • Unsafe类的构造函数是私有的;

  • 虽然它有静态的getUnsafe()方法,但是如果尝试调用Unsafe.getUnsafe(),会得到一个SecutiryException。这个类只有被JDK信任的类实例化。

    但是这总会是有变通的解决办法的,一个简单的方式就是使用反射进行实例化

    public class UnsafeAcquireTest {
    	public static void main(String[] args) throws Exception {
    		// 不能直接获取,抛出异常 java.lang.SecurityException: Unsafe
    		// Unsafe unsafe = Unsafe.getUnsafe();
      
    		// 可以通过反射获取
    		Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
    		theUnsafe.setAccessible(true);
    		// Unsafe unsafe = (Unsafe) theUnsafe.get(Unsafe.class);
    		 Unsafe unsafe = (Unsafe) theUnsafe.get(null);
    		System.out.println(unsafe);
    	}
    }
    

使用sun.misc.Unsafe

1.实例化对象,避免初始化

通过allocateInstance()方法,可以创建一个类的实例,但是却不需要调用它的构造函数、初始化代码、各种JVM安全检查以及其它的一些底层的东西。即使构造函数是私有,也可以通过这个方法创建它的实例。

public class UnsafeAllocateInstanceTest {
	public static void main(String[] args) throws Exception {
		// 反射创建Unsafe实例
		Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
		theUnsafe.setAccessible(true);
		Unsafe unsafe = (Unsafe) theUnsafe.get(Unsafe.class);

		// 实例化构造器私有的类
		User user = (User) unsafe.allocateInstance(User.class);
		user.setAge(1);
		user.setName("Jay");
		System.out.println(user);
		System.out.println(unsafe.objectFieldOffset(User.class.getDeclaredField("age")));
	}

	private static class User {
		private String name;
		private int age;
		private static int count = 1;

		/** 构造器私有化 */
		private User() {
		}

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}

		public int getAge() {
			return age;
		}

		public void setAge(int age) {
			this.age = age;
		}

		@Override
		public String toString() {
			return "User{" + "name='" + name + '\'' + ", age=" + age + '}';
		}
	}
}
/* 输出
User{name='Jay', age=1}
12
*/

2.可以分配内存和释放内存

类中提供的3个本地方法allocateMemoryreallocateMemoryfreeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。

3.通过内存偏移地址修改变量值

  • public native long objectFieldOffset(Field field); 返回指定对象实例field的内存地址偏移量,在这个类的其他方法中这个值只是被用作一个访问特定field的一个方式。这个值对于给定的field是唯一的,并且后续对该方法的调用都应该返回相同的值。

  • public native int arrayBaseOffset(Class arrayClass); 获取给定数组中第一个元素的偏移地址。为了存取数组中的元素,这个偏移地址与arrayIndexScale方法的非0返回值一起被使用。 public native int arrayIndexScale(Class arrayClass) 获取用户给定数组寻址的换算因子。如果不能返回一个合适的换算因子的时候就会返回0。这个返回值能够与arrayBaseOffset一起使用去存取这个数组class中的元素。

  • public native boolean compareAndSwapInt(Object obj, long offset,int expect, int update);objoffset位置比较integer field和期望的值,如果相同则更新。这个方法的操作应该是原子的,因此提供了一种不可中断的方式更新integer field。当然还有与ObjectLong对应的compareAndSwapObjectcompareAndSwapLong方法。

  • public native void putOrderedInt(Object obj, long offset, int value); 设置obj对象中offset偏移地址对应的整型field的值为指定值。这是一个有序或者有延迟putIntVolatile方法,并且不保证值的改变被其他线程立即看到。只有在fieldvolatile修饰并且期望被意外修改的时候使用才有用。当然还有与ObjectLong对应的putOrderedObjectputOrderedLong方法。

  • public native void putObjectVolatile(Object obj, long offset, Object value); 设置obj对象中offset偏移地址对应的objectfield的值为指定值。支持volatile store语义。 与这个方法对应的get方法为: public native Object getObjectVolatile(Object obj, long offset); 获取obj对象中offset偏移地址对应的objectfield的值,支持volatile load语义 这两个方法还有与Int、Boolean、Byte、Short、Char、Long、Float、Double等类型对应的相关方法.

  • public native void putObject(Object obj, long offset, Object value); 设置obj对象中offset偏移地址对应的objectfield的值为指定值。与putObject方法对应的是getObject方法。Int、Boolean、Byte、Short、Char、Long、Float、Double等类型都有getXXXputXXX形式的方法。

下面通过一个组合示例来了解一下如何使用它们,详细如下:

public class UnsafeCASTest {
	public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
		// 反射获取Unsafe实例
		Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
		theUnsafe.setAccessible(true);
		Unsafe unsafe = (Unsafe) theUnsafe.get(Unsafe.class);

		// 实例化构造器私有的类 allocateInstance
		Player player = (Player) unsafe.allocateInstance(Player.class);
		player.setName("Jack");
		player.setAge(10);
		for (Field fd : Player.class.getDeclaredFields()) {
			// 对象Field偏移
			System.out.println("[" +fd.getName() + "]对应的内存偏移地址: " + unsafe.objectFieldOffset(fd));
		}

		Field name = Player.class.getDeclaredField("name");
		long nameOffset = unsafe.objectFieldOffset(name);
		Field age = Player.class.getDeclaredField("age");
		long ageOffset = unsafe.objectFieldOffset(age);
		// CAS compareAndSwapInt
		System.out.println(unsafe.compareAndSwapInt(player, ageOffset, 10, 20));
		System.out.println("age修改后的值:" + player.getAge());

		unsafe.putOrderedInt(player, ageOffset, player.getAge() + 2);
		System.out.println("age修改后的值:" + player.getAge());

		unsafe.putObjectVolatile(player, nameOffset, "tom");
		System.out.println("name:" + player.getName());
		System.out.println("name:" + unsafe.getObjectVolatile(player, nameOffset));
	}

	private static class Player {
		private String name;
		private int age;

		private Player() {
		}

		public String getName() {
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}

		public int getAge() {
			return age;
		}

		public void setAge(int age) {
			this.age = age;
		}
	}

}
/*
输出
[name]对应的内存偏移地址: 16
[age]对应的内存偏移地址: 12
true
age修改后的值:20
age修改后的值:22
name:tom
name:tom
*/

4.挂起与恢复线程

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本park方法,但最终都调用了Unsafe.park()方法。

  • public native void park(boolean isAbsolute, long timeout); 阻塞一个线程直到unpark出现、线程被中断或者timeout时间到期。如果一个unpark调用已经出现了,这里只计数。timeout为0表示永不过期。当isAbsolutetrue时,timeout是相对于新纪元之后的毫秒(ms)。否则这个值就是超时前的纳秒数(ns)。这个方法执行时也可能不合理地返回。

  • public native void unpark(Thread thread); 释放被park的一个线程。这个方法也可以被使用来终止一个先前调用park导致的阻塞这个操作。操作是不安全的,因此线程必须保证是活的。这是java代码不是native代码。参数thread指要解除阻塞的线程。

下面来看一下LockSupport类中关于Unsafe.parkUnsafe.unpark的使用:

private static void setBlocker(Thread t, Object arg) {
    // Even though volatile, hotspot doesn't need a write barrier here.
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}
// 恢复阻塞线程
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}
// 一直阻塞当前线程
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L); // 相对,0表示永不过期
    setBlocker(t, null);
}
// 阻塞当前线程nanos纳秒
public static void parkNanos(Object blocker, long nanos) {
    if (nanos > 0) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, nanos); // 相对,nanos纳秒
        setBlocker(t, null);
    }
}

// 阻塞当前线程 直到deadline(绝对,单位ms)
public static void parkUntil(Object blocker, long deadline) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(true, deadline); // 绝对,到deadline时刻(ms)
    setBlocker(t, null);
}

// 一直阻塞当前线程
public static void park() {
    UNSAFE.park(false, 0L); // 相对,0表示永不过期 
}

// 阻塞当前线程nanos纳秒
public static void parkNanos(long nanos) {
    if (nanos > 0)
        UNSAFE.park(false, nanos); // 相对,nanos纳秒
}

// 阻塞当前线程 直到deadline(绝对,单位ms)
public static void parkUntil(long deadline) {
    UNSAFE.park(true, deadline); // 绝对,到deadline时刻(ms)
}

下面是使用LockSupport的示例:

public class LockSupportByUnsafeTest {
	public static void main(String[] args) throws InterruptedException {
		ThreadPark threadPark = new ThreadPark("ThreadPark");
		threadPark.start();
		ThreadUnpark threadUnPark = new ThreadUnpark("ThreadUnpark", threadPark);
		threadUnPark.start();

		// main线程等待ThreadUnpark执行成功
		threadUnPark.join();
		System.out.println(Thread.currentThread().getName() + "--运行成功....");
	}

	private static class ThreadPark extends Thread {
		public ThreadPark(String name) {
			super(name);
		}

		@Override
		public void run() {
			System.out.println(Thread.currentThread().getName() + "--我将被阻塞在这了60s....");
			LockSupport.parkNanos(1000000000L * 60);
			System.out.println(Thread.currentThread().getName() + "--我被恢复正常了....");
		}
	}

	private static class ThreadUnpark extends Thread {
		public Thread thread;

		public ThreadUnpark(String name, Thread thread) {
			super(name);
			this.thread = thread;
		}

		@Override
		public void run() {
			System.out.println(Thread.currentThread().getName() + "--提前恢复阻塞线程ThreadPark");
			// 恢复阻塞线程
			LockSupport.unpark(thread);

			try {
				TimeUnit.SECONDS.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

}
/*
ThreadPark--我将被阻塞在这了60s....
ThreadUnpark--提前恢复阻塞线程ThreadPark
ThreadPark--我被恢复正常了....
main--运行成功....
*/

当然sun.misc.Unsafe中还有一些其它的功能,可以继续深挖。sun.misc.Unsafe提供了可以随意查看及修改JVM中运行时的数据结构,尽管这些功能在JAVA开发本身是不适用的,Unsafe是一个用于研究学习HotSpot虚拟机非常棒的工具,因为它不需要调用C++代码,或者需要创建即时分析的工具。后续有空继续研究这个类。

参考资料

  1. http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/
  2. https://zhuanlan.zhihu.com/p/37579394
  3. https://blog.csdn.net/fenglibing/article/details/17138079
  4. https://blog.csdn.net/dfdsggdgg/article/details/51538601
  5. https://blog.csdn.net/aesop_wubo/article/details/7537278
  6. https://blog.csdn.net/dfdsggdgg/article/details/51543545