Java源码解读系列2—Unsafe类(JDK1.7 )

1 概述

Java与C语言一个较大差别是JVM屏蔽底层细节,使我们开发能够更专注于业务实现。
Unsafe类,顾名思义就说非安全类,属于sun.misc包下,是java开放给用户直接接触底层。网上大部分文章大部分直接讲解API,楼主这篇文章是通过应用场景去讲解Unsafe类,并且每个用途都附上使用用例,不仅教会你懂原理,还让你会使用。但还是由衷告诫你,除非是用来开发基础框架,否则不推荐使用。

2 初始化方法

在ConcurrentHashMap的源码中,我们可以看到Unsafe的示例化方法

private static final sun.misc.Unsafe U;
 static {
        try {
            U = sun.misc.Unsafe.getUnsafe();
         } catch (Exception e) {
            throw new Error(e);
        }
  }

但实际使用这段代码进行示例化时,会抛出非安全异常,这是神马操作?

Exception in thread "main" java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)

点进去看源码,原来是类加载问题。JUC包属于rt.jar,是由根类加载器进行加载,因此不会报错。对类加载不熟的同学可以参考《深入浅出JVM系列1:类加载器及其用法》

构造函数私有化
private Unsafe() {
    }

  @CallerSensitive
 public static Unsafe getUnsafe() {
       //获取调用的类
        Class var0 = Reflection.getCallerClass();
      // 根类加载器一般使用null来表示,而自定义类的类加载器为应用类加载器,因此两者不相等
        if (var0.getClassLoader() != null) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

通过getUnsafe方法获取theUnsafe属性这条路行不通,那么我们只能使用必杀技—反射

public class UnsafeApp {
        private static Unsafe unsafe;


    /**
     * 获取Unsafe类的一个实例
     */
    static {
        try {
            //getDeclaredFiled 获取类本身的属性成员
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            //私有对象必须设置为true,否则会报错
            f.setAccessible(true);
            unsafe = (Unsafe)f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    }

3 操纵对象得属性

提到操作对象的私有属性,基本会使用反射。我们通过Unsfa类的objectFieldOffset方法也能实现获取对象的私有属性

/**
 * 学生类
 */
public class Student {
   //编号
    private int id;
    //姓名
    private String name;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
}

public class UnsafeApp {

   ...省略unsafe示例方法...
   
    public static void main(String[] args) throws  Exception{
       /*
         * 操作私有对象属性
         */
        Student student = new Student(); //unsafe修改私有变量
        Field field = student.getClass().getDeclaredField("id");//getDeclaredField可以获取私有的变量

        //方法1:采用反射修改私有变量
        field.setAccessible(true); //为true时可以访问私有类型变量
        field.set(student, 111);
        System.out.println("student.id: " + student.getId());
        }
        
         //方法2:采用unsafe修改私有变量
        unsafe.putInt(student, unsafe.objectFieldOffset(field), 20);
        System.out.println("student.id: " + student.getId());
   }       

打印结果

student.id: 111
student.id: 20

4 操作堆外内存

JVM通过垃圾回收机制管理Java内存,使我们不需要像C语言一样,每次使用完堆内存还要自己手动释放,否则会造成内存泄漏。Java其实也是可以操作JVM外的内存,称为堆外内存或直接内存,是通过Unsafe实现。
用Unsafe开辟的内存空间不占用JVM Heap空间,也不具有自动内存回收功能,需要自己手动释放。典型的用途是本地缓存缓存。本地缓存相等Redis等内存数据库更为方便快捷,但会给虚拟机带来GC压力,因此可以通过堆外内存将数据缓存起来,如Ehcache。

       /**
         * 操作堆外内存
         */
        byte[] bigObject = "大对象".getBytes();

        //分配堆外内存
        //返回对外内存的地址
        long address = unsafe.allocateMemory(bigObject.length);

        //添加元素到指定位置
        for (int i = 0; i < bigObject.length; i++) {
            unsafe.putByte(address + i, bigObject[i]);
        }

        //获取指定位置的元素
        byte[] byteArray = new byte[bigObject.length];
        for (int i = 0; i < bigObject.length; i++) {
            byteArray[i] = unsafe.getByte(address + i);
        }

        System.out.println(new String(byteArray));
        

打印结果

大对象

5 CAS锁

5.1 原子操作

JDK1.6之前,多线程抢占资源时,使用synchonized关键字会触发系统调用,让没抢到锁的资源进入阻塞状态,后面获得资源后才恢复为RUNNABLE状态,这个操作过程涉及到用户态和内核态的切换,代价比较高。CAS(Compare And Swap,比较并替换)是一个原子操作,需要CPU支持。CAS更新一个变量的时候,只有当变量的预期值expect和地址偏移量offset当中的实际值相同时,才会将offset对应的expect修改为update。

*
* @param obj     需要更新的对象
* @param offset  obj中内存地址的偏移量
* @param expect 旧的值
* @param update 期望值
* @return 更新成功返回true
*/
public native boolean compareAndSwapInt(Object obj, long offset, int expect, int update);

5.2 线程安全

CAS是原子操作,JUC包中并发容器中的底层很多就是使用CAS,如AtomicInteger等,被广泛用于并发编程。
这里模拟两个计数器,一个非线程安全,一个使用CAS保证线程安全

 /**
 * 非安全版本计数器
 */
public class UnSafeCounter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int get() {
        return count;
    }
}


/**
 * 线程安全版本计数器
 */
public class SafeCounter {
    private volatile int count = 0;
    private final static long valueOffset;
    private final static Unsafe unsafe;

    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
            valueOffset = unsafe.objectFieldOffset
                    (SafeCounter.class.getDeclaredField("count"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    public void increment() {
        //无限循环
        for (;;) {
            int current = get();
            int next = current + 1;
            //CAS操作
            if (unsafe.compareAndSwapInt(this, valueOffset, current, next))
                //原子更新成功,跳出无限循环
                break;
        }
    }


    public int get() {
        return count;
    }
}


 
// 测试代码
public static void main(String[] args) throws  Exception{
   ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
                10,
                0,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue());

        int count = 10 * 10000;


        for(int i = 0; i < count; i++)
        executor.execute(new Runnable() {
            @Override
            public void run() {
                unSafeCounter.increment();
            }
        });

        Thread.sleep(5000);

        System.out.println("UnSafeCounter.count: " + unSafeCounter.get());


        for(int i = 0; i < count; i++)
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    safeCounter.increment();
                }
            });



        Thread.sleep(5000);
        System.out.println("SafeCounter.count: " + safeCounter.get());


        //关闭线程池
        executor.shutdown();
        
}

打印结果:

UnSafeCounter.count: 99996
SafeCounter.count: 100000

这里有个大坑,线程池的getActiveCount()不准确问题,还是使用sleep方法,把时间设置长一点从而保证没有活跃线程

    while(executor.getActiveCount() != 0){
             Thread.sleep(1000);
       }

5.3 ABA问题

在并发环境下,线程A和线程B通过CAS在争夺资源,A通过CAS成功将预期值expect改为update后,此时,其他任务的线程C又将update改为expect,这时候B会误以为自己抢占到资源,从而产生脏数据,这就是ABA问题。

网上最常见的解决方法是加个版本号,每次将预期值expect改为update后,版本号就会发生变化。
public class AtomicStampedReferenceApp {

public class AtomicStampedReferenceApp {



      public static void main(String[] args) throws Exception {

        String expert = "gz";
        String update = "sz";
        int initversion = 1;

        AtomicStampedReference reference = new AtomicStampedReference(expert,1);

        //从gz去sz,现在又回到gz
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                reference.compareAndSet(expert,update,initversion,reference.getStamp()+1);
                System.out.println("从"+ expert + "去"  + reference.getReference() + ",现在的版本号:" + reference.getStamp());

                reference.weakCompareAndSet(update,expert,reference.getStamp(), reference.getStamp()+1);
                System.out.println("现在回到" + reference.getReference() + ",现在的版本号:" + reference.getStamp());
            }
        });
      t1.start();
      Thread.sleep(1000);

      //不使用版本做限制,存在aba问题
      Thread t2 = new Thread(new Runnable() {
          @Override
          public void run() {
              boolean ret = reference.compareAndSet(expert,update, reference.getStamp(),reference.getStamp()+1);
              System.out.print(ret? "我没有离开gz" : "我有离开过gz" );
              System.out.println(",现在的版本号:" + reference.getStamp());
          }
      });
      t2.start();
      Thread.sleep(1000);


      //使用指定版本号,发现已经发生变化,CAS更新失败,解决了aba问题
      Thread t3 = new Thread(new Runnable() {
          @Override
          public void run() {
              boolean ret = reference.compareAndSet(expert,update, initversion,reference.getStamp()+1);
              System.out.print(ret? "我没有离开gz" : "我有离开过gz" );
              System.out.println(",现在的版本号:" + reference.getStamp());
          }
      });
      t3.start();

    }

打印结果

从gz去sz,现在的版本号:2
现在回到gz,现在的版本号:3
我没有离开gz,现在的版本号:4
我有离开过gz,现在的版本号:4

如果看过AQS源码,里面还提供一种解决ABA的方法。A,B,C三个线程执行CAS,线程A抢占成功将同步状态从0更新为1,失败的线程被放到FIFO队列中。
后面A处理完将同步状态更新回0,如果此时刚好有线程D执行CAS,不就会获得资源,相当于插队,这不就堆B和C不公平?AQS提供的解决方法是判断队列中是否有线程在排队,如果有则不执行CAS。

6 阻塞和唤醒线程

6.1 阻塞和唤醒的实现原理

JUC中,当一个线程需要等待某个操作时,通过Unsafe的park()方法来阻塞此线程。当线程需要再次运行时,通过Unsafe的unpark()方法来唤醒此线程。如LockSupport.park()/unpark(),它们底层都是调用的Unsafe的这两个方法。

//阻塞线程
public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }
    
 //唤醒线程
 public static void unpark(Thread thread) {
         if (thread != null)
             UNSAFE.unpark(thread);
  }

6.2 阻塞和唤醒的用例

根据源码,我们可以模拟线程的阻塞和唤醒

Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                long startTime = System.currentTimeMillis();
                System.out.println("线程" + Thread.currentThread().getName() + "开始阻塞");
                
                 //第一个参数为true时,第二个参数使用单位为毫秒的绝对时间,表示要阻塞到某个时间点,如unsafe.park(true, System.currentTimeMillis() + 1000)表示阻塞1秒
                  //第一个参数为false时,第二个参数使用单位为纳秒的绝对时间,表示要阻塞的时间间隔, 0L表示永久阻塞,如 unsafe.park(false, 1000000000l)表示阻塞1秒
                unsafe.park(false, 0L); 
                System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
                long endTime = System.currentTimeMillis();
                System.out.println("阻塞时间为:" + (endTime - startTime) / 1000 + "秒");
            }
        });

        t1.setName("park_test_thread");
        t1.start();
        Thread.sleep(3000);            
        unsafe.unpark(t1);

打印结果

线程park_test_thread开始阻塞
线程park_test_thread被唤醒
阻塞时间为:3秒

6.3 破坏阻塞的场景

在调用park()之前调用了unpark或者interrupt则park阻塞失效。

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {

//                unsafe.unpark(Thread.currentThread());
                Thread.currentThread().interrupt();
                long startTime = System.currentTimeMillis();
                System.out.println("线程" + Thread.currentThread().getName() + "开始阻塞");
                unsafe.park(false, 0L);
                System.out.println("线程" + Thread.currentThread().getName() + "被唤醒");
                long endTime = System.currentTimeMillis();
                System.out.println("阻塞时间为:" + (endTime - startTime) / 1000 + "秒");
            }
        });


        t1.setName("park_test_thread");
        t1.start();
        Thread.sleep(3000);
        unsafe.unpark(t1);

打印结果

线程park_test_thread开始阻塞
线程park_test_thread被唤醒
阻塞时间为:0秒

7 可见性

先了解下可见性的使用场景

public class CounterThread extends Thread {
    private   Object isContinue = "true";
    private final static long valueOffset;
    private final static Unsafe unsafe;

    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
            valueOffset = unsafe.objectFieldOffset
                    (CounterThread.class.getDeclaredField("isContinue"));
        } catch (Exception ex) { throw new Error(ex); }
    }


    public void setContinue(Object isContinue) {
        this.isContinue = isContinue;
    }

    public Object isContinue() {
        return isContinue;
    }

    @Override
    public void run() {
        System.out.println("开始统计");
        while (isContinue == "true")
        {

        }
        System.out.println("结束统计");
    }
    
    public class TestCounter {

    public static void main (String[] args) throws Exception{
        CounterThread thread = new CounterThread();
        thread.start();
        TimeUnit.SECONDS.sleep(1);
        thread.setContinue("false");
    }
}

打印结果

开始统计

线程一直处于运行状态,是因为线程每次都是从私有堆栈中拿取isContinue的值,而不是从公共堆栈中获取,导致isContinue的值发生改变,但是线程没响应。
使用Unsafe中的getObjectVolatile方法,线程会立即获取公共堆栈中最新的值。

   public void setContinue(Object isContinue) {
        this.isContinue = isContinue;
    }
    
      @Override
    public void run() {
        System.out.println("开始统计");
        while (unsafe.getObjectVolatile(this, valueOffset) == “true”)
        {
        }
        System.out.println("结束统计");
    }

打印结果

开始统计
结束统计

8 操作数组

Unsafe通过获取数组的起始偏移量和每个元素空间大小,从而直接操作数组,其中结合位运算的方法比乘法运算的效率会更高。

        int[] array = new int[20];
        //数组第一个元素的偏移地址
        int baseOffset = unsafe.arrayBaseOffset(array.getClass());

        //数组中每个元素的空间大小
        int indexScale = unsafe.arrayIndexScale(array.getClass());

        for (int i = 0; i < array.length; i++){
            //数组中第i个元素的偏移量
            //方法1 基于乘法运算
//            long offset = baseOffset +(i * indexScale);

            //方法2 基于位运算

            //numberOfLeadingZeros表示从最左边开始数起连续的0的个数为,如 numberOfLeadingZeros(1)的值为31
            int ssfit = 31 - Integer.numberOfLeadingZeros(indexScale);
            long offset = (i << ssfit) + baseOffset;

            unsafe.putInt(array, offset, i);
        }

        for (int i = 0; i < array.length; i++){
            System.out.println("元素位置:" + i + " 值:" + array[i]);
        }

打印结果

元素位置:0 值:0
元素位置:1 值:1
元素位置:2 值:2
元素位置:3 值:3
元素位置:4 值:4

其中这段代码比较难理解。那么它具有普适性吗?答案是否。

  int ssfit = 31 - Integer.numberOfLeadingZeros(indexScale);
  long offset = (i << ssfit) + baseOffset;

先看下测试用例

 int num1 = 7;
 System.out.println(1 << (31 - Integer.numberOfLeadingZeros(num1)));
 System.out.println(1 * num1);

 int num2 = 8;
 System.out.println(1 << (31 - Integer.numberOfLeadingZeros(num2)));
 System.out.println(1 * num2);

打印结果

4
7
8
8

从结果可以看出这段代码不具备普适性,有且只有当num为2^n时才能成立。
这就好理解,即一个数i * 2^n等价于 i << n

9 参考文献

  1. JDK7在线文档
    https://tool.oschina.net/apidocs/apidoc?api=jdk_7u4
  2. Bruce Eckel,Java编程思想 第4版. 2007, 机械工业出版社ConcurrentHashMap

你可能感兴趣的:(并发编程和网络编程,并发编程,源码)