跟我一块儿剖析 Java 并发源码之 Unsafe

  本篇文章是系列文章《跟我一块儿剖析Java并发源码》的第一篇文章。之后会每周保持定时更新。这些系列文章算是对《java并发编程系统与模型》的一些补充吧,真心但愿你们能支持这本书,您的支持就是我最大的动力!
  在java并发相关的源代码学习中,有一个类常常出现,这个类就是位于sun.misc包中的Unsafe类。好比,属于Java并发包中最重要的类之一的AbstractQueuedSynchronizer中就常常调用这个类的方法。
  今天就来简单剖析一下这个类。
  Unsafe类是一个很低级别的类,执行低级别的不安全的操做。因此使用的时候要当心,只有那些得到信任的代码才能调用。为何说它是比较低级的呢?由于它能直接操做任意的内存。那为何它是危险的呢?由于它能直接操做任意的内存。javascript

  Unsafe类方法众多,一一讲述没太必要,聪明的大家看完这篇文章再看看源码理解其余方法绝对没什么问题。先来看看有compareAndSwap开头的一系列方法,从名字就能够看出这确定是使用的CAS算法。CAS算法对这里再也不详细说明了。在《java并发编程系统与模型》这本书有详细叙述。java

  随便拿一个compareAndSwapInt举例:算法

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);复制代码

  这个方法能够在在对象o的内存偏移offset后与指望值比较,若是等于指望值,就更新为x。因为是直接操做内存的。好比要这样更新一个对象的某个属性,就要获得这个属性在内存中的偏移量。unsafe提供了objectFieldOffset方法来获得某个属性在对象中的偏移量:编程

public native long objectFieldOffset(Field f);复制代码

  好比有这个一个对象:bootstrap

class User{

   private  String   name

      }复制代码

  要获得属性name 偏移量, 就可使用安全

nameOffset = unsafe.objectFieldOffset(User.class.getDeclaredField("name"));复制代码

  还有一些put方法,好比并发

public native void putOrderedInt(Object obj, long offset, int value);复制代码

就是直接把这个对象内存偏移offset而后直接赋int值。性能

Unsafe类是一个受保护的类,是不能直接在程序中使用的。直接的使用会抛出SecurityException异常,下面来测验一下(import sun.misc.Unsafe 须要手工添加,Eclispe或者其余IDE并不会直接提示):学习

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

public class UnSafeTest {
    public static void main(String[] args) {
        try {
            Unsafe unsafe = Unsafe.getUnsafe();
            User user = new User();
            long ageOffset = unsafe.objectFieldOffset(filed);
            unsafe.putInt(user, ageOffset, 10);
            System.out.println(user.getAge());
            System.out.println(unsafe);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}复制代码

查看获取实例的方法中有一个VM.isSystemDomainLoader检测,若是不是的话,会抛出SecurityException:ui

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

  来看看M.isSystemDomainLoader内部的方法:

public static boolean isSystemDomainLoader(ClassLoader loader) {
        return loader == null;
    }复制代码

  在Java中,若是一个对象的classLoader等于null,这就说明这个对象的类加载器是boostrap classloader,那么若是类是由bootstrap classloader加载的话,那么它就是受信任的代码。

  能够直接打印String的ClassLoader来检测一下结论是否正确:

System.out.println(String.class.getClassLoader());

  理论上有两种方法能够打破这种限制。一种就是将User类变成SystemDomainLoader,JAVA自己的类加载机制致使了改变SystemDomainLoader方法暂时较难作到,通用的都是经过反射的方法。一种是经过反射其实体变量theUnsafe:

public class UnSafeTest {
    public static void main(String[] args) {
        try {                
            User user = new User();
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            Unsafe UNSAFE = (Unsafe) theUnsafe.get(null);
            System.out.println(UNSAFE);
            Field filed = user.getClass().getDeclaredField("age");
            long ageOffset = UNSAFE.objectFieldOffset(filed);
            UNSAFE.putInt(user, ageOffset, 10);
            System.out.println(user.getAge());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}复制代码

  打印user对象的age属性,结果输出了10。

  另一种就是经过其构造器反射,从新获得一个实例:

public class UnSafeTest {
    public static void main(String[] args) {
        try {

            Constructor<Unsafe> con = Unsafe.class.getDeclaredConstructor();
            // 用该私有构造方法建立对象
            // IllegalAccessException:非法的访问异常。
            // 暴力访问
            con.setAccessible(true);// 值为true则指示反射的对象在使用时应该取消Java语言访问检查。

            User user = new User();

            System.out.println(UNSAFE);
// Unsafe unsafe =(Unsafe) clazz.newInstance();
            Field filed = user.getClass().getDeclaredField("age");
            long ageOffset = UNSAFE.objectFieldOffset(filed);
            UNSAFE.putInt(user, ageOffset, 10);
            System.out.println(user.getAge());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}复制代码

其中

Constructor<Unsafe> con = Unsafe.class.getDeclaredConstructor();复制代码

也能够替换成class.forname的形式:

Constructor<Unsafe> con = (Constructor<Unsafe>) Class.forName("sun.misc.Unsafe").getDeclaredConstructor();复制代码

为何要直接操做内存呢?

由于快啊!

若是说被修改的属性是一个基本类型,那么直接操做内存的优点并不大。可是若是被修改的属性是一个对象,差异就比较大了。不信来作一个很是简单的比较:

public class User {
    private Integer age;

    public int getAge() {
        return age;
    }

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



}复制代码
public class UnSafeTest {
    public static void main(String[] args) {
        try {
            Constructor<Unsafe> con = (Constructor<Unsafe>) Class.forName("sun.misc.Unsafe").getDeclaredConstructor();
            con.setAccessible(true);
            User user = new User();
            Unsafe UNSAFE = con.newInstance(null);
            Field filed = user.getClass().getDeclaredField("age");
            long s1=System.currentTimeMillis();
            for(int i=0;i<1000000;i++){
                user.setAge(i);
            }
            System.out.println(System.currentTimeMillis()-s1);
            long ageOffset = UNSAFE.objectFieldOffset(filed);
            long s2=System.currentTimeMillis();
            for(int i=0;i<1000000;i++){
                UNSAFE.putInt(user, ageOffset, i);
            }
            System.out.println(System.currentTimeMillis()-s2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}复制代码

  set方法和putInt各执行一百万次,性能差了好几倍。若是被修改的属性是一个很是复杂的对象的话,性能差距会更大。由于每次set值的时候,JVM内部依旧会每次去找这个对象属性的内存偏移量。如今我直接将偏移量拿出来了,不用每次找偏移量了,速度加快那是必然滴,固然被修改的对象确定是一个对象。

  在下一周分析AbstractQueuedSynchronizer类的时候,还会结合AbstractQueuedSynchronizer类实现中如何具体的使用Unsafe类进行说明,这里就暂时告一段落了。

小参考:

bandrzejczak.com/blog/2015/0…
stackoverflow.com/questions/1…
stackoverflow.com/questions/2…

相关文章
相关标签/搜索