`
春花秋月何时了
  • 浏览: 39415 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

Java并发包基础元件sun.misc.Unsafe

 
阅读更多

前言

         一直以来,Java作为一门高级语言给人的印象是程序员不需要也没有办法直接对内存进行操作,其实Unsafe类就是Java中可以直接操作内存的工具,它属于sun.* 路径下的API,而不属于J2EE的一部分。

         需要说明的是:直接操作内存是很危险的一件事,并且Unsafe是一个平台相关的类,它操作的更是直接内存,所以不能通过Java虚拟机的垃圾回收机制进行内存释放,在使用的时候需要注意内存泄漏和溢出,在实际开发中建议不要直接使用。接下来,我们就Unsafe类中比较重要或者在Java并发包中会使用到的方法进行一个探究,以备将来学习Java并发包的时候更加便于理解,以下代码以JDK8为例。

 

有趣的Unsafe实例化

JDK8中关于sun.misc.Unsafe可供实例化的代码如下所示,其构造方法是私有的,而另外提供了一个getUnsafe静态方法返回Unsafe的静态属性实例“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;
}

 如果你尝试通过调用Unsafe.getUnsafe()方法以期得到一个Unsafe实例,那么很遗憾你会得到一个SecutiryException,通过isSystemDomainLoader源码可以看出因为这个方法只能被JDK核心类库(即<JAVA_HOME>\lib目录下的类库)直接调用(关于类加载器可以参考https://www.cnblogs.com/fingerboy/p/5456371.html)。

public static boolean isSystemDomainLoader(ClassLoader loader) {
    return loader == null; //只有最顶层的启动类加载器加载的<JAVA_HOME>\lib目录下的类库其加载器才为空。
}

 既然它有一个静态的实例成员属性,这个时候我们就要使用反射来得到Unsafe的实例对象,代码如下:

 

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);//获取静态成员可以传null

 

强大的sun.misc.Unsafe方法 

 1. 突破限制创建实例-allocateInstance

 通过allocateInstance()方法,可以直接创建一个类的实例,而不需要执行它的构造函数、初使化代码、各种JVM安全检查以及其它的一些底层的东西,即使其构造方法是私有的甚至带参数的(这将是对单例模式的类的一种噩梦,因为没有办法阻止通过这种方式对单例模式的类创建多个实例)

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()); // 这时候的结果将会是0,而不是12
 
        p.setAge(45); // Let's now set age 45 to un-initialized object
        System.out.println(p.getAge()); // 这次的结果才会是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;
	    }
	}
}

   另外,我们还可以在运行时动态的加载编译好的class文件,然后创建其实例,调用其相关的方法:

//告诉虚拟机定义了一个没有安全检查的类,默认情况下这个类加载器和保护域来着调用者类 
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);

 

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操作

 关于CAS在Java内存模型JMM之七CAS机制一文中已经做了研究,此处不在熬述。在Unsafe类中提供如下三个CAS方法。这将是整个Java并发包的基础核心操作方法。

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。

                ② 在调用park之前或者之后被中断(park返回之后不会清除中断状态)。

                ③ 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中介绍。值得注意的是,不论是在执行park之前还是之后中断了线程,在执行park时都会立即返回,并且不会清除中断状态,所以如果是在一个循环中执行park,需要自己进行中断状态判断,及时跳出循环,否则将出现死循环,不停park不停醒来的高负荷运行。

 

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等方法操作对应类型的变量。还有,getAndAddInt(Object o, long offset, int delta),getAndAddLong(Object o, long offset, long delta),getAndSetXXX(Object o, long offset, int newValue)还能对对象或数组指定偏移位置的值进行增加(int和long型时)或重新赋值(int,long,Object型),它们都是使用CAS进行更新所以能够保证原子性。

 

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修饰的时候最有用,因为如果本身是volatile修饰的变量通过其他方式修改之后会立即可见并且因为相关内存屏障的加入显得效率低下,但当我们使用putOrderedXXX方法去修改那些本身是volatile修饰的变量的时候,它将会移除其volatile的特性变得高效但不立即可见。那么到底如何理解这三个putOrderedXXX方法?它和对应的putXXXVolatile方法到底有何区别?

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

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

        具体putOrderedXXX方法是如何实现的,我翻遍了google也没有找到一个确切的答案,它的实现其实说到底还是跟平台严格相关的,就算你阅读了openJDK的C/C++源码你也未必能找到它的真正实现,因为有一种说法是,Java的即时编译JIT可能会在运行时直接忽略掉其原始的实现而采用与平台相关的更优的策略来实现,但是这些不同的实现方式最终所要完成的功能却是很明确的,所以更重要的是我们还是要记住这些putOrderedXXX方法最终的结论是:它们的写入效率比volatile写更高效,因为它们的写入其实只是更新的本地缓存,没有立即回写至主内存,所以它们的操作结果也不是立即可见的,另外它还附带禁止对该操作前面的其他写操作的重排序,除非后面存在volatile写或者synchronize块导致它们会被立即刷新回主存使其立即对其他处理器可见,否则从理论上来说,其可见性很可能将会被无限期的推迟,当然这最终还是取决于平台的缓存一致性策略。

    下面举一个例子加深一下对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屏障,从而避免了写入操作的重排序。

 当然严格来说上例并不是putOrderedXXX方法的真正使用场景,典型的应用场景例如维护一个内部存在volatile成员的高性能的非阻塞的数据结构时,在并发交互允许“模糊”读数时,延迟写入(例如置null等待垃圾回收等)可以让一个线程在没有其他线程volatile写或者synchronized动作发生前,本地缓存的写操作不必刷回主内存,这时候由于减少了数据多次从本地缓存刷新到主存的一致性操作成本,从而提高了对整个数据结构的维护效率,这在单线程的环境中更是完全没有任何问题,同时还优化了store-load屏障在性能上的损耗。

 

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);
//尝试获取锁,已经没有被使用
public native boolean tryMonitorEnter(Object var1);

//判断是否需要加载一个类
public native boolean shouldBeInitialized(Class<?> c);
//确保类一定被加载 
public native  void ensureClassInitialized(Class<?> c)

 

10. JDK9 中的新篇章

之前一直有传言,未来的java版本将会删除unsafe这个类,但在JDK9中,不但没有删除,反而还被改进的更加易用。

变化一:包路径从sun.msc改为jdk.internal.misc

变化二:Unsafe实例的获取方式不再需要通过反射获取,而是提供了更方便的静态方法可以直接拿到。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics