Java concurrency base element sun.misc.Unsafe

foreword

         For a long time, Java, as a high-level language, gives people the impression that programmers do not need and have no way to directly operate memory. In fact, the Unsafe class is a tool in Java that can directly operate memory. It belongs to the API under the sun.* path. Not part of J2EE.

         It should be noted that it is very dangerous to directly manipulate memory, and Unsafe is a platform-related class, which operates more direct memory, so memory cannot be released through the garbage collection mechanism of the Java virtual machine. When you need to pay attention to memory leaks and overflows, it is recommended not to use them directly in actual development. Next, we will explore the methods that are more important in the Unsafe class or that will be used in Java concurrent packages, so as to be easier to understand when learning Java concurrent packages in the future. The following code takes JDK8 as an example.

 

Interesting Unsafe instantiation

The code available for instantiation of sun.misc.Unsafe in JDK8 is as follows, its constructor is private, and a getUnsafe static method is provided to return Unsafe's static property instance "theUnsafe".

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;
}

 If you try to get an Unsafe instance by calling the Unsafe.getUnsafe() method, unfortunately you will get a SecutiryException, which can be seen from the isSystemDomainLoader source code because this method can only be used by the JDK core class library (ie <JAVA_HOME>\lib directory) The class library under) is called directly (for the class loader, please refer to https://www.cnblogs.com/fingerboy/p/5456371.html).

public static boolean isSystemDomainLoader(ClassLoader loader) {
    return loader == null; //Only the class library under the <JAVA_HOME>\lib directory loaded by the top-level startup class loader has an empty loader.
}

 Since it has a static instance member property, at this time we need to use reflection to get the Unsafe instance object, the code is as follows:

 

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);//Get static members can pass null

 

Powerful sun.misc.Unsafe method 

 1. Break through the limit to create an instance - allocateInstance

 Through the allocateInstance() method, an instance of a class can be created directly without executing its constructor, initialization code, various JVM security checks, and other low-level things, even if its constructor is private or even with Parametric (this would be a nightmare for singleton classes, as there is no way to prevent multiple instances of singleton classes from being created this way)

public class UnsafeTest {

	public static void main(String[] args) throws Exception {
		Field f = Unsafe.class.getDeclaredField("theUnsafe");
		f.setAccessible(true);
		Unsafe unsafe = (Unsafe) f.get(null);
		
		Player p = (Player) unsafe.allocateInstance(Player.class);
        System.out.println(p.getAge()); // this time the result will be 0, not 12
 
        p.setAge(45); // Let's now set age 45 to un-initialized object
        System.out.println(p.getAge()); // This time the result will be 45
	}

	class Player {
	    private int age = 12;
	 
	    private Player(int age) {
	        this.age = age;
	    }
	 
	    public int getAge() {
	        return this.age;
	    }
	 
	    public void setAge(int age) {
	        this.age = age;
	    }
	}
}

   In addition, we can also dynamically load the compiled class file at runtime, then create its instance and call its related methods:

//Tell the virtual machine to define a class without security checks. By default, this class loader and protection domain come from the caller class
public native Class<?> defineClass(String name, byte[] b, int off, int len,
                                       ClassLoader loader,
                                       ProtectionDomain protectionDomain);
// load an anonymous class
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

 

byte[] classContents = getClassContent();
Class c = getUnsafe().defineClass(null,classContents,0,classContents.length);
c.getMethod("a").invoke(c.newInstance(),null);// 1

private static byte[] getClassContent() throws Exception{
  File f = new File("/home/mishadoff/tmp/A.class");
  FileInputStream input = new FileInputStream(f);
  byte[] content = new byte[(int)f.length()];
  input.read(content);
  input.close();
  return content;
}

 

2. CAS operation

 The research on CAS has been done in the article on the Java Memory Model JMM 7 CAS Mechanism , which will not be discussed here. The following three CAS methods are provided in the Unsafe class. This will be the underlying core operation method for the entire Java concurrent package.

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);
 

3. 线程的挂起与恢复

 将一个线程挂起是通过park方法实现的,调用 park后,当前线程将阻塞除非出现如下情况:

                ① 在调用park之前或者之后调用过unpark。

                ② 当前线程被打断。

                ③ isAbsolute是false,time不为0,给定的时间超时。

                ④ isAbsolute是true,time给定的截止时间超时。

                ⑤ 无理由的虚假的唤醒(也就是传说中“Spurious wakeup”,具体详见下一章的LockSupport介绍)。

 

unpark可以终止一个挂起的线程,使其恢复正常。这也是整个Java并发包的基础核心操作方法。Unsafe提供的park/unpark被封装成LockSupport的各种版本的park方法,其本质最终还是调用的Unsafe的实现。

public native void unpark(Object thread);

public native void park(boolean isAbsolute, long time);

 根据JavaDoc,当park方法的参数isAbsolute是true的时候,表示是绝对时间,超时时间参数time的单位是毫秒,一般用当前时间的毫秒数+超时时间毫秒数作为参数(System.currentTimeMillis() + 超时时长毫秒数)。

                          当isAbsolute为false时,表示是相对时间,超时时间参数time的单位是纳秒,1毫秒等于1000000纳秒。特别的,当isAbsolute为false,time为0表示一直阻塞,没有超时时间。

这里只介绍park/unpark在unsafe类中的API简单说明,更深入的研究将在下一章的LockSupport中介绍。

 

4. 直接操作类的对象成员属性

Unsafe能够通过类成员属性的内存偏移地址进行直接修改/读取成员属性的值,即使是私有的、静态的、final修饰的。示例如下:

 

public class UnsafeTest {

	public static void main(String[] args) throws Exception {
		Field f = Unsafe.class.getDeclaredField("theUnsafe");
		f.setAccessible(true);
		Unsafe unsafe = (Unsafe) f.get(null);
		
		long offset = unsafe.objectFieldOffset(Player.class.getDeclaredField("age"));
		long nameOffset = unsafe.staticFieldOffset(Player.class.getDeclaredField("name"));
		Object playerClass = unsafe.staticFieldBase(Player.class.getDeclaredField("name"));
		
		Player p = new Player(20);
		unsafe.putInt(p, offset, 100);
		unsafe.putObject(playerClass, nameOffset, "New Player");
		System.out.println(unsafe.getInt(p, offset));
		System.out.println(unsafe.getObject(playerClass, nameOffset));
	}

	static class Player {
		private static final String name = "My Player";
		
	    private int age = 12;
	 
	    public Player(int age) {
	        this.age = age;
	    }
	}
}

    在上例中,通过 objectFieldOffset(Field f)和staticFieldOffset(Field f)可以分别获取类的实例属性和静态属性字段相对Java对象的“起始地址”的偏移量。通过putInt(Object o, long offset, int x)和putObject(Object o, long offset, Object x)直接对两个成员属性的值进行了修改,最后通过getInt(Object o, long offset)和getObject(Object o, long offset)直接又对这两个属性的值进去了读取。

    需要说明的是,针对静态属性的修改/读取,必须使用staticFieldBase(Field f)方法获取类对象实例进行操作而不能直接使用对象实例进行操作,否则可能将会抛出异常。另外,Unsafe也提供了putLong、putFloat、putDouble、putChar、putByte、putShort、putBoolean等方法操作对应类型的变量。

 

5. 延迟/立即可见读写

除了在上例中的getInt(Object o, long offset)和putInt(Object o, long offset, int x)等针对不同类型的成员的读写操作方法之外,在Unsafe中还提供如下一些方法:

public native Object  getObjectVolatile(Object o, long offset);
public native void    putObjectVolatile(Object o, long offset, Object x);

public native int     getIntVolatile(Object o, long offset);
public native void    putIntVolatile(Object o, long offset, int x);

.....
public native void    putOrderedObject(Object o, long offset, Object x);
public native void    putOrderedInt(Object o, long offset, int x);
public native void    putOrderedLong(Object o, long offset, long x);

    其中getXXXVolatile和putXXXVolatile很好理解,因为方法名携带了Volatile关键字。这些方法就是实现了Volatile语义的加强版的getXXX和putXXX方法,即,getXXXVolatile方法总是能立即获取最新的值(可能其他线程刚刚修改过该共享变量),putXXXVolatile方法对变量的修改会立即回写到主存从而可能对其他线程立即可见。

    另外三个putOrderedXXX方法,JDK注释说相当于更新无法保证立即对其他线程可见的对应的putXXXVolatile方法,除非字段本身被Volatile关键字修饰。那么到底如何理解这三个putOrderedXXX方法?它和对应的putXXXVolatile方法到底有何区别?

    这要从Volatile关键字的特性开始说起,通过Java内存模型JMM之四volatile关键字一文中对Volatile底层实现的描述我们知道,在每一个volatile写操作后面编译器为了防止重排序会插入StoreStore和StoreLoad内存屏障,StoreStore内存屏障可以禁止写重排序从而保证写入操作被顺序执行并立即回写到主存,StoreLoad内存屏障不但包含了StoreStore屏障的功能,并且还能禁止对Volatile变量的写和后面的Volatile变量读操作的重排序,从而不但能保证写入/读取操作的顺序执行和写入操作立即回写到主存,而且能保证后面的读取操作立即重新从主存中加载以获取最新的值。从这两种屏障可以看出,StoreStore屏障只保证写入的顺序执行,但不保证会立即重新从主存加载最新值,所以无法保证写入的立即可见性。而StoreLoad屏障就能满足所有的立即可见的要求,但是这也导致了StoreLoad屏障的巨大开销和性能的损耗。

     说了这么多,和我们这里讨论的putOrderedXXX方法有什么关系呢?其实Java编译器会在生成这三个方法相应的指令后面加上StoreStore指令,从而避免写入操作的重排序但是并不保证对其他线程立即可见,可能这也是方放名中携带有Ordered字样的体现吧。这样的方法比其相应的putXXXVolatile方法性能更好,在能够容忍低延迟场景中比较有用,它能够达到快速的非阻塞的写入存储,特别是在某些天然会避免写入重排序的架构中(例如Intel 64/IA-32),更是连StoreStore屏障都能省略,其性能更高。

    下面举一个例子加深一下对putOrderedXXX方法方法的理解:假设有这样一个场景,一个容器可以放一个东西,容器支持create方法来创建一个新的东西并放到容器里,支持get方法取到这个容器里的东西,简单的代码实现如下。

public class Container {

	private SomeThing object;
 
    public static class SomeThing {
        private int status;

        public SomeThing() {
            status = 1;
        }

        public int getStatus() {
            return status;
        }
    }

    public void create() {
        object = new SomeThing();
    }

    public SomeThing get() {
        while (object == null) {
            Thread.yield(); //避免出现大量的死循环
        }
        return object;
    }
}

        上面的示例在单线程下执行没有问题,但是在多线程并发运行时,由不同的线程调用create和get时,存在和DCL的单例模式相同的问题,即SomeThing的构建与将SomeThing的引用赋值给object变量这两个操作可能会发生重排序,导致get()中可能会拿到一个正在被构建中的不完整的对象SomeThing实例。结合解决DCL机制时其中一种办法是使用volatile修饰object变量,这不仅避免了重排序,并且还能保证对object变量的写入立即对其他线程可见,而且也比使用synchronize同步锁性能损耗更小。

       但是如果使用场景对object的内存可见性并不要求非常及时,也就是说当object被创建之后,其他线程可以稍等一会再get拿到它,中间的延迟可以被接受的话,为了更好的提升create的性能,可以有更好的解决办法。毕竟volatile对象在写入操作的后面添加的StoreLoad屏障其性能损耗也是毕较大的,此时我们只要保证SomeThing的构建与将SomeThing的引用赋值给object变量这两个操作不被重排序,这样虽然对object变量的更改不能立即对其他线程可见,但是却能够保证当其他线程延迟拿到object对象的时候至少是一个绝对完整的对象。这时候我们就可以利用putOrderedXXX方法提供的StoreStore屏障来达到这样的目的:

public class Container {

	private SomeThing object;

    public static class SomeThing {
        private int status;

        public SomeThing() {
            status = 1;
        }

        public int getStatus() {
            return status;
        }
    }

    private Object value;
    private static final Unsafe unsafe = getUnsafe();
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset(Container.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
	
	public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            return (Unsafe)f.get(null);
        } catch (Exception e) {
        }
        return null;
    }

    public void create() {
        SomeThing temp = new SomeThing();
        unsafe.putOrderedObject(this, valueOffset, null);    //将value赋null值只是一项无用操作,实际利用的是这条语句的内存屏障
        object = temp;
    }

    public SomeThing get() {
        while (object == null) {
            Thread.yield();
        }
        return object;
    }
}

 可以见到在上面的代码里面我们利用unsafe.putOrderedObject(this, valueOffset, null)这一句无用操作使编译器在此处插入StoreStore屏障,从而避免了写入操作的重排序。

 

6. 内存屏障

其实在JDK8中,还提供了如下三个直接设置内存屏障,避免代码重排序的方法。

//在该方法之前的所有读操作,一定在load屏障之前执行完成
public native void loadFence();

//在该方法之前的所有写操作,一定在store屏障之前执行完成
public native void storeFence();

//在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个的合体功能
public native void fullFence();

所以在上面第四部分利用unsafe.putOrderedObject达到使用内存屏障的地方,其实也可以直接使用storeFence()方法,也能达到同样的效果。      

 

7. 直接内存操作

通过unsafe不仅能直接操作对象实例属性成员(上面第4部分),还能直接操作某个内存地址,其相应的方法如下:

//分配内存指定大小的内存
public native long allocateMemory(long bytes);
//根据给定的内存地址address设置重新分配指定大小的内存
public native long reallocateMemory(long address, long bytes);
//用于释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long address);
//将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
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);
//这个方法实现还是调用的上面的对象拷贝,只不过对象为null
public void copyMemory(long srcAddress, long destAddress, long bytes)
//设置给定内存地址的值
public native void putAddress(long address, long x);
//获取指定内存地址的值
public native long getAddress(long address);

//设置给定内存地址的long值
public native void putLong(long address, long x);
//获取指定内存地址的long值
public native long getLong(long address);
//设置或获取指定内存的byte值
public native byte  getByte(long address);
public native void  putByte(long address, byte x);
//其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同

//操作系统指针的字节长度,返回值是4或者8,分别对应了32位和64位的系统
public native int addressSize();

//操作系统的内存页大小,这个值永远都是2的幂次方 
public native int pageSize();

   这里就不一一举例其使用方法了,只针对部分方法列出示例代码:

   

public static void showBytes() {
    try {
       Unsafe unsafe = getUnsafe();
       // 在堆外分配给定大小(1个byte)的一块内存空间
       long memoryAddress = unsafe.allocateMemory(1L);
       // 对分配的内存空间进行写入
       unsafe.putAddress(memoryAddress, (byte)100); // or putByte
       //从内存地址空间读取数据
       long readValue = unsafe.getAddress(memoryAddress); // or getByte
       //重新分配一个long,返回新的内存起始地址偏移量
       memoryAddress = unsafe.reallocateMemory(allocatedAddress, 8L);
       unsafe.putLong(memoryAddress, 1024L);
       long longValue = unsafe.getLong(memoryAddress);
       //释放内存
       unsafe.freeMemory(memoryAddress);
 
    } catch (Exception e) {
       e.printStackTrace();
    }
}

    java中数组的最大长度为Integer.MAX_VALUE,正常情况下如果想创建一个大于Integer.MAX_VALUE的数组是做不到的,但是Unsafe可以,通过对内存进行直接分配实现。

class SuperArray {
    private final static int BYTE = 1;
    private long size;
    private long address;
      
    public SuperArray(long size) {
        this.size = size;
        //得到分配内存的起始地址
        address = getUnsafe().allocateMemory(size * BYTE);
    }
    public void set(long i, byte value) {
        getUnsafe().putByte(address + i * BYTE, value);
    }
    public int get(long idx) {
        return getUnsafe().getByte(address + idx * BYTE);
    }
    public long size() {
        return size;
    }
}

 

8. 数组操作

关于数组操作的相关方法如下。

//获取数组第一个元素的偏移地址
public native int arrayBaseOffset(Class arrayClass);
//数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,可定位数组中每个元素在内存中的位置
public native int arrayIndexScale(Class arrayClass);

示例代码如下:

Unsafe u = getUnsafeInstance();

int[] arr = {1,2,3,4,5,6,7,8,9,10};

int base = u.arrayBaseOffset(int[].class);

int scale = u.arrayIndexScale(int[].class);

u.putInt(arr, (long)base+scale*9, 1);

for(int i=0;i<10;i++){
    int v = u.getInt(arr, (long)b+s*i);
    System.out.print(v+“ ”);
}

//打印结果:1 2 3 4 5 6 7 8 9 1 ,可以看到,成功读出了数组中的值,而且最后一个值由10改为了1。

   

public class UnsafeTest {
	
	public static Unsafe unsafe;
	
	static{
		try {
			Field f = Unsafe.class.getDeclaredField("theUnsafe");
			f.setAccessible(true);
			unsafe = (Unsafe) f.get(null);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws Exception {
		Player p = new Player(20);
		//获取Player对象实例的偏移地址
		long addressOfPlayer = addressOf(p);
		//获取age字段的偏移量
		long offset = unsafe.objectFieldOffset(Player.class.getDeclaredField("age"));
		
		//通过实例内存地址+属性成员偏移量 设置和获取成员属性的值
		unsafe.putInt(addressOfPlayer + offset, 100);
		int age = unsafe.getInt(addressOfPlayer + offset);
		System.out.println("age="+age);//打印结果 100
	}
	/**
	 * 获取对象的偏移地址.
	 * 需要将目标对象设为辅助数组的第一个元素(也是唯一的元素)。
	 * 由于这是一个复杂类型元素(不是基本数据类型),它的地址存储在数组的第一个元素。
	 * 然后,获取辅助数组的基本偏移量。数组的基本偏移量是指数组对象的起始地址与数组第一个元素之间的偏移量。
	 * @param o
	 * @return
	 */
	public static long addressOf(Object o){
		Object helperArray[] = new Object[]{o};
		long baseOffset = unsafe.arrayBaseOffset(Object[].class);
		int addressSize = unsafe.addressSize();
		switch (addressSize) {
		case 4:
			return unsafe.getInt(helperArray, baseOffset);
		case 8:
			return unsafe.getLong(helperArray, baseOffset);
		default:
			throw new Error("unsupported address size: "+addressSize);
		}
	}

	static class Player {
	    private int age = 12;
	 
	    public Player(int age) {
	        this.age = age;
	    }
	}
}

 

9. 其他方法

unsafe包含的方法还有很多,没有列举的就不再一一列举了。

//获取持有锁,已经没有被使用
public native void monitorEnter(Object var1);
//释放锁,已经没有被使用
public native void monitorExit(Object var1);
// Attempt to acquire the lock, it has not been used
public native boolean tryMonitorEnter(Object var1);

// Determine if a class needs to be loaded
public native boolean shouldBeInitialized(Class<?> c);
// make sure the class must be loaded
public native  void ensureClassInitialized(Class<?> c)

 

10. A new chapter in JDK9

There have been rumors before that the unsafe class will be deleted in future java versions, but in JDK9, not only has it not been deleted, but it has been improved to make it easier to use.

Change 1: The package path is changed from sun.msc to jdk.internal.misc

Change 2: The way to obtain Unsafe instances no longer needs to be obtained through reflection, but provides a more convenient static method to obtain them directly.

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326063295&siteId=291194637