Java Unsafe类 学习

0 参考资料

1 Unsafe类介绍

JDK 的 rt.jar 包中的 sum.msic.Unsafe 类提供了硬件级别的原子性操作,Unsafe 类中的方法都是native 方法,它们使用 JNI 的方式访问本地C++ 实现库。

Unsafe类不属于Java标准,但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Hadoop、Kafka等。

使用Unsafe可用来直接访问系统内存资源并进行自主管理,Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。

Unsafe可认为是Java中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等。

1.1 Unsafe的使用建议

建议先看这个知乎帖子第一楼R大的回答:为什么JUC中大量使用了sun.misc.Unsafe 这个类,但官方却不建议开发者使用

使用Unsafe要注意以下几个问题:

  • 1、Unsafe有可能在未来的Jdk版本移除或者不允许Java应用代码使用,这一点可能导致使用了Unsafe的应用无法运行在高版本的Jdk。
  • 2、Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接crash掉。
  • 3、Unsafe提供的直接内存访问的方法中使用的内存不受JVM管理(无法被GC),需要手动管理,一旦出现疏忽很有可能成为内存泄漏的源头。

暂时总结出以上三点问题。Unsafe 在JUC (java.util.concurrent ) 包中大量使用 (主要是CAS),在netty中方便使用直接内存,还有一些高并发的交易系统为了提高CAS的效率也有可能直接使用到Unsafe。总而言之,Unsafe类是一把双刃剑。

官方并不建议使用Unsafe。

下面是使用Unsafe的一些例子。

1.1 实例化私有类

import java.lang.reflect.Field; 
import sun.misc.Unsafe; 

public class UnsafePlayer {
    public static void main(String[] args) throws Exception { 
        //通过反射实例化Unsafe 
        Field f = Unsafe.class.getDeclaredField("theUnsafe"); 
        f.setAccessible(true); 
        Unsafe unsafe = (Unsafe) f.get(null); 
        //实例化Player 
        Player player = (Player) unsafe.allocateInstance(Player.class); 
        player.setName("li lei"); 
        System.out.println(player.getName()); 
    } 
} 

class Player{ 

    private String name; 
    
    private Player(){}
    
    public String getName() { 
   		return name; 
    } 
    public void setName(String name) { 
    	this.name = name; 
    } 
}

1.2 CAS操作,通过内存偏移地址修改变量值

java并发包中的SynchronousQueue中的TransferStack中使用CAS更新栈顶。

/ Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long headOffset;

static {
    try {
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> k = TransferStack.class;
        headOffset = UNSAFE.objectFieldOffset(k.getDeclaredField("head"));
    } catch (Exception e) {
        throw new Error(e);
    }
}

//栈顶
volatile SNode head;
//更新栈顶
boolean casHead(SNode h, SNode nh) {
    return h == head && UNSAFE.compareAndSwapObject(this, headOffset, h, nh);
}

1.3 直接内存访问

Unsafe的直接内存访问:用Unsafe开辟的内存空间不占用Heap空间,当然也不具有自动内存回收功能。做到像C一样自由利用系统内存资源。

2 Unsafe类源码分析

Unsafe中一共有82个 public native 修饰的方法,还有几十个基于这82个public native方法的其他方法。

Unsafe的大部分API都是native的方法,主要包括以下几类:

1)Class相关。主要提供 Class 和 它的静态字段的操作方法。

2)Object相关。主要提供 Object 和 它的字段的操作方法。

3)Arrray相关。主要提供数组及其中元素的操作方法。

4)(多线程)并发相关。主要提供低级别同步原语,如CAS、线程调度、volatile、内存屏障等。

5)Memory相关。提供了直接内存访问方法(绕过Java堆直接操作本地内存),可做到像C一样自由利用系统内存资源。

6)系统相关。主要返回某些低级别的内存信息,如地址大小、内存页大小。

初始化代码

private static native void registerNatives();
static {
    registerNatives();
    sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
}

private Unsafe() {}

private static final Unsafe theUnsafe = new Unsafe();

@CallerSensitive
public static Unsafe getUnsafe() {
    Class<?> caller = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(caller.getClassLoader()))
        throw new SecurityException("Unsafe");
    return theUnsafe;
}

初始化的代码主要包括调用 JVM本地方法 registerNatives()sun.reflect.Reflection#registerMethodsToFilter。然后新建一个Unsafe 实例命名为 theUnsafe,通过静态方法 getUnsafe() 获取,获取的时候需要做权限判断。

由此可见,Unsafe使用了单例设计 ( 可见构造私有化了 ) 。Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器 ( BootStrap ClassLoader ) 加载的类才能调用这个类中的方法。最简单的使用方式是基于反射获取Unsafe实例。

我们知道 Unsafe 类是 rt.jar包提供的, rt.jar 包里面的类是使用 Bootstrap 类加载器加载的,静态方法 getUnsafe() 获取实例的时候需要做权限判断。

而我们平时写的业务类代码一般是能过AppClassLoader 加载的。Unsafe 类可以直接操作内存,这是不安全的,所以JDK 开发组特意做了这个限制, 不让开发人员在正规渠道使用Unsafe。既然从正规渠道访问不了,那么就玩点黑科技,使用万能的反射来获取Unsafe 实例方法。

普通开发者获取Unsafe实例的方法如下:

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

2.1 Class 相关

//静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);

//返回给定的静态属性的位置,配合 staticFieldOffset 方法使用
public native Object staticFieldBase(Field f);

//判断是否需要初始化一个类
public native boolean shouldBeInitialized(Class<?> c);

//确保类被初始化
public native void ensureClassInitialized(Class<?> c);

//定义一个类,可用于动态创建类
public native Class<?> defineClass(String name, byte[] b, int off, int len,
     ClassLoader loader,
     ProtectionDomain protectionDomain);

//定义一个匿名类,可用于动态创建类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

2.2 Object相关

Java中的基本类型(boolean、byte、char、short、int、long、float、double)及对象引用类型都有以下方法。

//获得对象的字段偏移量 
public native long objectFieldOffset(Field f); 

//获得给定对象地址偏移量的int值
public native int getInt(Object o, long offset);

//设置给定对象地址偏移量的int值
public native void putInt(Object o, long offset, int x);

//创建对象,但并不会调用其构造方法。如果类未被初始化,将初始化类。
public native Object allocateInstance(Class<?> cls) throws InstantiationException;

2.3 数组相关


//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);

//boolean、byte、short、char、int、long、float、double,及对象类型均有以下方法
public static final int ARRAY_BOOLEAN_BASE_OFFSET = theUnsafe.arrayBaseOffset(boolean[].class);

//返回数组中每一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);
 
//boolean、byte、short、char、int、long、float、double,及对象类型均有以下方法
public static final int ARRAY_BOOLEAN_INDEX_SCALE = theUnsafe.arrayIndexScale(boolean[].class);

通过arrayBaseOffset和arrayIndexScale可定位数组中每个元素在内存中的位置。

2.4 (多线程)并发相关

2.4.1 CAS相关

CAS:CompareAndSwap,内存偏移地址offset,预期值expected,新值 x。
如果变量在当前时刻的值和预期值expected相等,尝试将变量的值更新为 x。
如果更新成功,返回true;否则,返回false。

/**
* 比较obj的offset处内存位置中的值和期望的值,如果相同则更新。此更新是不可中断的。
* 
* @param obj 需要更新的对象
* @param offset obj中整型field的偏移量
* @param expect 希望field中存在的值
* @param x 如果期望值expect与field的当前值相同,设置filed的值为这个新值
* @return 如果field的值被更改返回true
*/
public final native boolean compareAndSwapObject(Object o, long offset,
       Object expected,
       Object x);
 
public final native boolean compareAndSwapInt(Object o, long offset,
      int expected,
      int x);
 
public final native boolean compareAndSwapLong(Object o, long offset,
      long expected,
      long x);

什么是Compare And Swap(CAS) ?

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。

如果内存位置的值(V) 与 预期原值(A)相匹配,那么处理器会自动将该位置值更新为新值(B)。否则,处理器不做任何操作。

无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了 sun.misc.Unsafe 类方法进行CAS操作。

从Java 8开始,Unsafe中提供了以下方法:

//增加
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

public final long getAndAddLong(Object o, long offset, long delta) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!compareAndSwapLong(o, offset, v, v + delta));
    return v;
}

//设置
public final int getAndSetInt(Object o, long offset, int newValue) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, newValue));
    return v;
}

public final long getAndSetLong(Object o, long offset, long newValue) {
    long v;
    do {
        v = getLongVolatile(o, offset);
    } while (!compareAndSwapLong(o, offset, v, newValue));
    return v;
}

public final Object getAndSetObject(Object o, long offset, Object newValue) {
    Object v;
    do {
        v = getObjectVolatile(o, offset);
    } while (!compareAndSwapObject(o, offset, v, newValue));
    return v;

2.4.2 线程调度相关

//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁
public native void monitorEnter(Object o);
//释放对象锁
public native void monitorExit(Object o);

//尝试获取对象锁,返回true或false表示是否获取成功
public native boolean tryMonitorEnter(Object o);

2.4.3 volatile 相关读写

Java中的基本类型(boolean、byte、char、short、int、long、float、double)及对象引用类型都有以下方法。

//从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
//相当于getObject(Object, long)的volatile版本
public native Object getObjectVolatile(Object o, long offset);
 
//存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
//相当于putObject(Object, long, Object)的volatile版本
public native void putObjectVolatile(Object o, long offset, Object x);

/**
 * Version of {@link #putObjectVolatile(Object, long, Object)}
 * that does not guarantee immediate visibility of the store to
 * other threads. This method is generally only useful if the
 * underlying field is a Java volatile (or if an array cell, one
 * that is otherwise only accessed using volatile accesses).
 */
public native void putOrderedObject(Object o, long offset, Object x);
 
/** Ordered/Lazy version of {@link #putIntVolatile(Object, long, int)} */
public native void putOrderedInt(Object o, long offset, int x);
 
/** Ordered/Lazy version of {@link #putLongVolatile(Object, long, long)} */
public native void putOrderedLong(Object o, long offset, long x);

2.4.4 内存屏障相关

Java8 引入,用于定义内存屏障,避免代码重排序。

//内存屏障,禁止load操作重排序,即屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();

//内存屏障,禁止store操作重排序,即屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();

//内存屏障,禁止load、store操作重排序
public native void fullFence();

2.5 直接内存访问(非堆内存)

allocateMemory 所分配的内存需要手动 free(不被GC回收)

//(boolean、byte、char、short、int、long、float、double)都有以下get、put两个方法。 
//获得给定地址上的int值
public native int getInt(long address);
//设置给定地址上的int值
public native void putInt(long address, int x);
//获得本地指针
public native long getAddress(long address);
//存储本地指针到给定的内存地址
public native void putAddress(long address, long x);

//分配内存
public native long allocateMemory(long bytes);
//重新分配内存
public native long reallocateMemory(long address, long bytes);
//初始化内存内容
public native void setMemory(Object o, long offset, long bytes, byte value);

//初始化内存内容
public void setMemory(long address, long bytes, byte value) {
    setMemory(null, address, bytes, value);
}
//内存内容拷贝
public native void copyMemory(Object srcBase, long srcOffset,
                              Object destBase, long destOffset,
                              long bytes);
//内存内容拷贝
public void copyMemory(long srcAddress, long destAddress, long bytes) {
    copyMemory(null, srcAddress, null, destAddress, bytes);
}

//释放内存
public native void freeMemory(long address);

2.6 系统相关

//返回指针的大小。返回值为4或8。
public native int addressSize();
 
/** The value of {@code addressSize()} */
public static final int ADDRESS_SIZE = theUnsafe.addressSize();
 
//内存页的大小。
public native int pageSize();

3 示例代码

示例1:

package com.aop8.unsafe;

import java.lang.reflect.Field;

import sun.misc.Unsafe;


/**
 * <pre.
 * 输出结果说明了:
 * staticFieldOffset 只能使用在静态属性,
 * objectFieldOffset 只能使用在非静态属性。
 *</pre>
 */
public class Main {

    public static void main(String[] args) throws Exception {
    	
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");//使用反射
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);//通过反射,获得Unsafe实例
        
        Class<Person> personClass = Person.class;
        
        Field name = personClass.getField("NAME");
        Field age = personClass.getField("age");
        try {
            System.out.println("objectFieldOffset  name -->" + unsafe.objectFieldOffset(name));
        } catch (Exception e) {
        	//e.printStackTrace();
            System.out.println(e.getMessage());
        }
        
        try {
            System.out.println("objectFieldOffset age -->" + unsafe.objectFieldOffset(age));
        } catch (Exception e) {
        	//e.printStackTrace();
            System.out.println(e.getMessage());
        }
        try {
            System.out.println("staticFieldOffset name -->" + unsafe.staticFieldOffset(name));
        } catch (Exception e) {
        	//e.printStackTrace();
            System.out.println(e.getMessage());
        }
        try {
            System.out.println("staticFieldOffset age -->" + unsafe.staticFieldOffset(age));
        } catch (Exception e) {
        	//e.printStackTrace();
            System.out.println(e.getMessage());
        }
    }
}

运行结果:

null
objectFieldOffset age -->12
staticFieldOffset name -->104
null

示例2:(黑科技)

package com.aop8.unsafe2;

import java.lang.reflect.Field;
import sun.misc.Unsafe;

public class UnsafePlayer {
	public static void main(String[] args) throws Exception {
		// 通过反射实例化Unsafe
		Field field = Unsafe.class.getDeclaredField("theUnsafe");
		field.setAccessible(true);
		
		Unsafe unsafe = (Unsafe) field.get(null);
		// 实例化Player
		Player player = (Player) unsafe.allocateInstance(Player.class);
		player.setName("li lei");
		System.out.println(player.getName());
	}
}

class Player {
	private String name;

	private Player() {
	}

	public String getName() {
		return name;
	}

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

运行结果:

li lei

示例3:

package com.aop8.unsafe2;

import java.lang.reflect.Field;

import sun.misc.Unsafe;

public class TestUnsafe {

	// 获取Unsafe实例
	static final Unsafe unsafe;
	String name = "lisi";
	int age = 4;

	// 记录state在类TestUsafe中的偏移值
	static final long stateOffset;
	// 变量
	public volatile long result = 0;

	public int[] arr = { 1, 2, 3, 4, 5, 6 };
	static {
		try {
			// 获取成员变量
			Field field = Unsafe.class.getDeclaredField("theUnsafe");
			// 设置为可访问
			field.setAccessible(true);
			// 是静态字段,用null来获取Unsafe实例
			unsafe = (Unsafe) field.get(null);
			// 获取state变量在类中的偏移值
			stateOffset = unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("result"));
		} catch (Exception e) {
			System.out.println(e.getLocalizedMessage());
			throw new Error(e);
		}
	}

	public static void main(String[] args) {
		TestUnsafe testUnsafe = new TestUnsafe();
		
		// 执行并返回结果
		for (int i = 0; i < 1000; i++){
			unsafe.getAndAddLong(testUnsafe, stateOffset, 3L); // 获取对象obj中偏移量为offset 的变量volatile语义的当前值, 并设置变量值为原始值 +addValue
		}
		
		System.out.println("getAndAddLong后,result的值 = "+testUnsafe.result);
		System.out.println("arrayBaseOffset 获取数组中第一个元素的地址 = "+unsafe.arrayBaseOffset(testUnsafe.arr.getClass())); // 获取数组中第一个元素的地址
		System.out.println("arrayIndexScale 获取数组中一个元素占用的字节 = "+unsafe.arrayIndexScale(testUnsafe.arr.getClass())); // 获取数组中一个元素占用的字节
		
		//比较对象obj 中偏移量为offset 的变量的值是否与expect 相等, 相等则使用update
		//值更新, 然后返回tru巳,否则返回false 。
		System.out.println("result CAS是否成功 = "+unsafe.compareAndSwapLong(testUnsafe, stateOffset, 3000, 4000));
		
		//获取对象obj中偏移量为offset 的变量对应volatile吾义的值。
		System.out.println("result 的值 = "+unsafe.getLongVolatile(testUnsafe, stateOffset));
		
		//设置obj对象中offset偏移的类型为long的field的值为value,支持volatile语义。
		unsafe.putLongVolatile(testUnsafe, stateOffset, 5000);
		System.out.println("putLongVolatile 后,result的值= "+testUnsafe.result);
		
		// 设置obj对象中offset偏移地址对应的long 型field的值为value 。
		// 这是一个有延迟的putLongvolatile 方法,并且不保证值修改对其他线程立刻可见。
		// 只有在变量使用volatile 修饰并且预计会被意外修改时才使用该方法。
		unsafe.putOrderedLong(testUnsafe, stateOffset, 5500);
		System.out.println("putOrderedLong 后,result的值 = "+testUnsafe.result);

		Thread thread1 = new Thread() {
			public void run() {
				System.out.println("线程1开始沉睡");
				long start = System.currentTimeMillis();
				long end = System.currentTimeMillis() + 8000;
				unsafe.park(true, end);//阻塞当前线程
				System.out.println("主线程在" + (System.currentTimeMillis() - start) + "ms后被线程2唤醒");
			}
		};

		Thread thread2 = new Thread() {
			public void run() {
				try {
					
					System.out.println("线程2 休眠3s");
					sleep(3000);
					unsafe.unpark(thread1);//唤醒线程1。如果没有此方法时,线程1将阻塞至少8s后才能启动
					System.out.println("线程2 打印");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		};
		thread1.start();
		thread2.start();
	}
}

运行结果:

getAndAddLong后,result的值 = 3000
arrayBaseOffset 获取数组中第一个元素的地址 = 16
arrayIndexScale 获取数组中一个元素占用的字节 = 4
result CAS是否成功 = true
result 的值 = 4000
putLongVolatile 后,result的值= 5000
putOrderedLong 后,result的值 = 5500
线程1开始沉睡
线程2 休眠3s
线程2 打印
主线程在3001ms后被线程2唤醒

分析:

  1. 类中首先声明了4个变量:静态final类型的 Unsafe实例、long类型的变量偏移量stateOffset、可见的long变量result、int类型的数组arr。

  2. 在static代码中使用反射获取Unsafe的实例,设置为可访问,并使用实例来获取result变量的偏移量,因为这些是静态的,所以在类加载的时候这些事情都做完了。

  3. 接下来我调用了 unsafe的 getAndAddLong(testUnsafe,stateOffset ,3L); 对变量reslut进行了1000次的加3操作,最终输出结果是3000。

  4. unsafe.getAndAddLong(testUnsafe,stateOffset ,3L);:这是一个原子操作,输入是参数是所在的类对象,变量在内存中的偏移量,加多少(3L表示加3,L表示这是一个long类型的变量)。“偏移量”这个参数就足够体现出这是一个原子操作了,直接对指定位置赋值。

  5. stateOffset=unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField(“result”));:这个方法是获取指定成员变量在内存中的偏移地址,参数是Field,这里必须通过反射机制获取。

  6. unsafe.arrayBaseOffset(testUnsafe.arr.getClass()) :获取数组中第一个元素的地址。

  7. unsafe.arrayIndexScale(testUnsafe.arr.getClass()):获取数组中一个元素占用的字节。

  8. unsafe.compareAndSwapLong(testUnsafe, stateOffset, 3000, 4000):比较并更新。比较指定偏移量下的变量值是否和3000这个expect值相等,如果相等则把它更新为4000,这是原子操作,由硬件提供可靠性。

  9. unsafe.getLongVolatile(testUnsafe, stateOffset):获取指定对象中某偏移量下的volatile值,这里需要输入一个对象是因为,在多个对象的情况下,每个对象都是被分配存储空间的,因此,指定对象下的某个变量才是唯一的。

  10. unsafe.putLongVolatile(testUnsafe, stateOffset, 5000);:设置指定对象某偏移量下的long类型的field值为5000,支持volatile语义。

  11. unsafe.putOrderedLong(testUnsafe, stateOffset, 5500);:作用和上面这个方法一样,只是它是一个有延迟的putLongVolatile方法,对变量的修改不会对别的线程立刻可见,一般在希望在这期间变量被意外修改才使用它。

  12. unsafe.park(true,end);:阻塞当前线程一段时间。true表示绝对时间(单位ms),false表示相对时间(单位ns),当false和0作为输入时表示一直阻塞。

  13. unsafe.unpark(thread1);:唤醒指定线程。thread1.interrupt();也可以达到相同的效果。

猜你喜欢

转载自blog.csdn.net/xiaojin21cen/article/details/89880627