Unsafe--Java为什么会引入及如何使用Unsafe

来源:互联网 发布:铁路数据服务平台 编辑:程序博客网 时间:2024/05/28 15:07

转载自:http://www.importnew.com/14511.html



综述

sun.misc.Unsafe至少从2004年Java1.4开始就存在于Java中了。在Java9中,为了提高JVM的可维护性,Unsafe和许多其他的东西一起都被作为内部使用类隐藏起来了。但是究竟是什么取代Unsafe不得而知,个人推测会有不止一样来取代它,那么问题来了,到底为什么要使用Unsafe?

做一些Java语言不允许但是又十分有用的事情

很多低级语言中可用的技巧在Java中都是不被允许的。对大多数开发者而言这是件好事,既可以拯救你,也可以拯救你的同事们。同样也使得导入开源代码更容易了,因为你能掌握它们可以造成的最大的灾难上限。或者至少明确你可以不小心失误的界限。如果你尝试地足够努力,你也能造成损害。

那你可能会奇怪,为什么还要去尝试呢?当建立库时,Unsafe中很多(但不是所有)方法都很有用,且有些情况下,除了使用JNI,没有其他方法做同样的事情,即使它可能会更加危险同时也会失去Java的“一次编译,永久运行”的跨平台特性。

对象的反序列化

当使用框架反序列化或者构建对象时,会假设从已存在的对象中重建,你期望使用反射来调用类的设置函数,或者更准确一点是能直接设置内部字段甚至是final字段的函数。问题是你想创建一个对象的实例,但你实际上又不需要构造函数,因为它可能会使问题更加困难而且会有副作用。

public class A implements Serializable {    private final int num;        public A(int num) {        System.out.println("Hello Mum");        this.num = num;    }     public int getNum() {        return num;    }}
在这个类中,应该能够重建和设置final字段,但如果你不得不调用构造函数时,它就可能做一些和反序列化无关的事情。有了这些原因,很多库使用Unsafe创建实例而不是调用构造函数。

Unsafe unsafe = getUnsafe();Class aClass = A.class;A a = (A) unsafe.allocateInstance(aClass);

调用allocateInstance函数避免了在我们不需要构造函数的时候却调用它。

线程安全的直接获取内存

Unsafe的另外一个用途是线程安全的获取非堆内存。ByteBuffer函数也能使你安全的获取非堆内存或是DirectMemory,但它不会提供任何线程安全的操作。你在进程间共享数据时使用Unsafe尤其有用。

importsun.misc.Unsafe;    importsun.nio.ch.DirectBuffer;     importjava.io.File;    importjava.io.IOException;    importjava.io.RandomAccessFile;    importjava.lang.reflect.Field;    importjava.nio.MappedByteBuffer;    importjava.nio.channels.FileChannel;     publicclass PingPongMapMain {        publicstatic void main(String... args) throwsIOException {            booleanodd;            switch(args.length < 1? "usage": args[0].toLowerCase()) {                case"odd":                    odd = true;                    break;                case"even":                    odd = false;                    break;                default:                    System.err.println("Usage: java PingPongMain [odd|even]");                    return;            }            intruns = 10000000;            longstart = 0;            System.out.println("Waiting for the other odd/even");            File counters = newFile(System.getProperty("java.io.tmpdir"),"counters.deleteme");            counters.deleteOnExit();             try(FileChannel fc = newRandomAccessFile(counters, "rw").getChannel()) {                MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0,1024);                longaddress = ((DirectBuffer) mbb).address();                for(inti = -1; i < runs; i++) {                    for(; ; ) {                        longvalue = UNSAFE.getLongVolatile(null, address);                        booleanisOdd = (value & 1) != 0;                        if(isOdd != odd)// wait for the other side.                            continue;// make the change atomic, just in case there is more than one odd/even process                        if(UNSAFE.compareAndSwapLong(null, address, value, value + 1))                            break;                    }                    if(i == 0) {                        System.out.println("Started");                        start = System.nanoTime();                    }                }            }            System.out.printf("... Finished, average ping/pong took %,d ns%n",                    (System.nanoTime() - start) / runs);        }         staticfinal Unsafe UNSAFE;         static{            try{                Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");                theUnsafe.setAccessible(true);                UNSAFE = (Unsafe) theUnsafe.get(null);            }catch(Exception e) {                thrownew AssertionError(e);            }        }    }

当你分别在两个程序,一个输入odd一个输入even,中运行时,可以看到两个进程都是通过持久化共享内存交换数据的。

在每个程序中,将相同的磁盘缓存映射到进程中。内存中实际上只有一份文件的副本存在。这意味着内存可以共享,前提是你使用线程安全的操作,比如volatile变量和CAS操作。(译注:CAS Compare and Swap 无锁算法

在两个进程之间有83ns的往返时间。当考虑到System V IPC(进程间通信)大约需要2500ns,而且用IPC volatile替代persisted内存,算是相当快的了。

Unsafe适合在工作中使用吗?

个人不建议直接使用Unsafe。它远比原生的Java开发所需要的测试多。基于这个原因建议还是使用经过测试的库。如果你只是想自己用Unsafe,建议你最好在一个独立的类库中进行全面的测试。这限制了Unsafe在你的应用程序中的使用方式,但会给你一个更安全的Unsafe。

总结

Unsafe在Java中是很有趣的一个存在,你可以一个人在家里随便玩玩。它也有一些工作的应用程序特别是在写底层库的时候,但总的来说,使用经过测试的Unsafe库比直接用要好。

原文链接: Peter Lawrey 翻译: ImportNew.com fzr
译文链接: http://www.importnew.com/14511.html

转载请保留原文出处、译者和译文链接。]


=======================================================================
=======================================================================
=======================================================================

public class VO  {      public int a = 0;            public long b = 0;            public static String c= "123";            public static Object d= null;            public static int e = 100;  }  

1.获取实例字段的偏移地址

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 获取实例字段的偏移地址,偏移最小的那个字段(仅挨着头部)就是对象头的大小  
  2. System.out.println(unsafe.objectFieldOffset(VO.class.getDeclaredField("a")));  
  3. System.out.println(unsafe.objectFieldOffset(VO.class.getDeclaredField("b")));  
  4.   
  5. // fieldOffset与objectFieldOffset功能一样,fieldOffset是过时方法,最好不要再使用  
  6. System.out.println(unsafe.fieldOffset(VO.class.getDeclaredField("b")));  


2.获取数组的头部大小和元素大小

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 数组第一个元素的偏移地址,即数组头占用的字节数  
  2. int[] intarr = new int[0];  
  3. System.out.println(unsafe.arrayBaseOffset(intarr.getClass()));  
  4.   
  5. // 数组中每个元素占用的大小  
  6. System.out.println(unsafe.arrayIndexScale(intarr.getClass()));  
Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET等,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE 等,这些常量值是通过arrayIndexScale方法得到的。将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。

3.获取类的静态字段偏移

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 获取类的静态字段偏地址  
  2. System.out.println(unsafe.staticFieldOffset(VO.class.getDeclaredField("c")));  
  3. System.out.println(unsafe.staticFieldOffset(VO.class.getDeclaredField("d")));  
  4.   
  5. // 获取静态字段的起始地址,通过起始地址和偏移地址,就可以获取静态字段的值了  
  6. // 只不过静态字段的起始地址,类型不是long,而是Object类型  
  7. Object base1 = unsafe.staticFieldBase(VO.class);  
  8. Object base2 = unsafe.staticFieldBase(VO.class.getDeclaredField("d"));  
  9. System.out.println(base1==base2);//true  

4.获取操作系统的位数

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //  Report the size in bytes of a native pointer.  
  2. //  返回4或8,代表是32位还是64位操作系统。  
  3. System.out.println(unsafe.addressSize());  
  4. // 返回32或64,获取操作系统是32位还是64位  
  5. System.out.println(System.getProperty("sun.arch.data.model"));  



通过上面的几段代码,我们可以成功获取类中各个字段的偏移地址,这跟jol工具的输出结果和我们的结论是一致的。有了字段的偏移地址,在加上对象的起始地,我们就能够通过Unsafe直接获取字段的值了。


5.读取对象实例字段的值

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. //获取实例字段的属性值  
  2. VO vo = new VO();  
  3. vo.a = 10000;  
  4. long aoffset = unsafe.objectFieldOffset(VO.class.getDeclaredField("a"));  
  5. int va = unsafe.getInt(vo, aoffset);  
  6. System.out.println("va="+va);  

6.获取静态字段的属性值

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. VO.e = 1024;  
  2. Field sField = VO.class.getDeclaredField("e");  
  3. Object base = unsafe.staticFieldBase(sField);  
  4. long offset = unsafe.staticFieldOffset(sField);  
  5. System.out.println(unsafe.getInt(base, offset));//1024  

可以看到Unsafe功能是很强大的,位java语言提供了更底层的功能。





=======================================================================
=======================================================================
0 0