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

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

  Unsafe类方法众多,一一讲述没太必要,聪明的你们看完这篇文章再看看源码理解其他方法绝对没什么问题。先来看看有compareAndSwap开头的一系列方法,从名字就可以看出这肯定是使用的CAS算法。CAS算法对这里不再详细说明了。在《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);复制代码

  比如有这个一个对象:


    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:

  @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…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值