第22篇:java中的UnSafe类

star2017 1年前 ⋅ 589 阅读

本文主要内容

  1. 基本介绍、
  2. 通过反射获取Unsafe实例
  3. Unsafe中的CAS操作
  4. Unsafe中原子操作相关方法介绍
  5. Unsafe中线程调度相关方法
  6. park和unpark示例
  7. Unsafe锁示例
  8. Unsafe中保证变量的可见性
  9. Unsafe中Class相关方法
  10. 示例:staticFieldOffset、staticFieldBase、staticFieldBase
  11. 示例:shouldBeInitialized、ensureClassInitialized
  12. 对象操作的其他方法
  13. 绕过构造方法创建对象
  14. 数组相关的一些方法
  15. 内存屏障相关操作
  16. java高并发系列目录

基本介绍

最近我们一直在学习java高并发,java高并发中主要涉及到类位于java.util.concurrent包中,简称juc,juc中大部分类都是依赖于Unsafe来实现的,主要用到了Unsafe中的CAS、线程挂起、线程恢复等相关功能。所以如果打算深入了解JUC原理的,必须先了解一下Unsafe类。

先上一幅Unsafe类的功能图:

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。

从Unsafe功能图上看出,Unsafe提供的API大致可分为内存操作CASClass相关对象操作线程调度系统信息获取内存屏障数组操作等几类,本文主要介绍3个常用的操作:CAS、线程调度、对象操作。

看一下UnSafe的原码部分:

  1. public final class Unsafe {
  2. // 单例对象
  3. private static final Unsafe theUnsafe;
  4. private Unsafe() {
  5. }
  6. @CallerSensitive
  7. public static Unsafe getUnsafe() {
  8. Class var0 = Reflection.getCallerClass();
  9. // 仅在引导类加载器`BootstrapClassLoader`加载时才合法
  10. if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
  11. throw new SecurityException("Unsafe");
  12. } else {
  13. return theUnsafe;
  14. }
  15. }
  16. }

从代码中可以看出,Unsafe类为单例实现,提供静态方法getUnsafe获取Unsafe实例,内部会判断当前调用者是否是由系统类加载器加载的,如果不是系统类加载器加载的,会抛出SecurityException异常。

那我们想使用这个类,如何获取呢?

可以把我们的类放在jdk的lib目录下,那么启动的时候会自动加载,这种方式不是很好。

我们学过反射,通过反射可以获取到Unsafe中的theUnsafe字段的值,这样可以获取到Unsafe对象的实例。

通过反射获取Unsafe实例

代码如下:

  1. package com.itsoku.chat21;
  2. import sun.misc.Unsafe;
  3. import java.lang.reflect.Field;
  4. /**
  5. * 跟着阿里p7学并发,微信公众号:javacode2018
  6. */
  7. public class Demo1 {
  8. static Unsafe unsafe;
  9. static {
  10. try {
  11. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  12. field.setAccessible(true);
  13. unsafe = (Unsafe) field.get(null);
  14. } catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. public static void main(String[] args) {
  19. System.out.println(unsafe);
  20. }
  21. }

输出:

  1. sun.misc.Unsafe@76ed5528

Unsafe中的CAS操作

看一下Unsafe中CAS相关方法定义:

  1. /**
  2. * CAS 操作
  3. *
  4. * @param o 包含要修改field的对象
  5. * @param offset 对象中某field的偏移量
  6. * @param expected 期望值
  7. * @param update 更新值
  8. * @return true | false
  9. */
  10. public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
  11. public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
  12. public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

什么是CAS? 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作,多个线程同时执行cas操作,只有一个会成功。我们都知道,CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现的, 其实在这一点上还是有排他锁的,只是比起用synchronized, 这里的排他时间要短的多, 所以在多线程情况下性能会比较好。

说一下offset,offeset为字段的偏移量,每个对象有个地址,offset是字段相对于对象地址的偏移量,对象地址记为baseAddress,字段偏移量记为offeset,那么字段对应的实际地址就是baseAddress+offeset,所以cas通过对象、偏移量就可以去操作字段对应的值了。

CAS在java.util.concurrent.atomic相关类、Java AQS、JUC中并发集合等实现上有非常广泛的应用,我们看一下java.util.concurrent.atomic.AtomicInteger类,这个类可以在多线程环境中对int类型的数据执行高效的原子修改操作,并保证数据的正确性,看一下此类中用到Unsafe cas的地方:

JUC中其他地方使用到CAS的地方就不列举了,有兴趣的可以去看一下源码。

Unsafe中原子操作相关方法介绍

5个方法,看一下实现:

  1. /**
  2. * int类型值原子操作,对var2地址对应的值做原子增加操作(增加var4)
  3. *
  4. * @param var1 操作的对象
  5. * @param var2 var2字段内存地址偏移量
  6. * @param var4 需要加的值
  7. * @return
  8. */
  9. public final int getAndAddInt(Object var1, long var2, int var4) {
  10. int var5;
  11. do {
  12. var5 = this.getIntVolatile(var1, var2);
  13. } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  14. return var5;
  15. }
  16. /**
  17. * long类型值原子操作,对var2地址对应的值做原子增加操作(增加var4)
  18. *
  19. * @param var1 操作的对象
  20. * @param var2 var2字段内存地址偏移量
  21. * @param var4 需要加的值
  22. * @return 返回旧值
  23. */
  24. public final long getAndAddLong(Object var1, long var2, long var4) {
  25. long var6;
  26. do {
  27. var6 = this.getLongVolatile(var1, var2);
  28. } while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));
  29. return var6;
  30. }
  31. /**
  32. * int类型值原子操作方法,将var2地址对应的值置为var4
  33. *
  34. * @param var1 操作的对象
  35. * @param var2 var2字段内存地址偏移量
  36. * @param var4 新值
  37. * @return 返回旧值
  38. */
  39. public final int getAndSetInt(Object var1, long var2, int var4) {
  40. int var5;
  41. do {
  42. var5 = this.getIntVolatile(var1, var2);
  43. } while (!this.compareAndSwapInt(var1, var2, var5, var4));
  44. return var5;
  45. }
  46. /**
  47. * long类型值原子操作方法,将var2地址对应的值置为var4
  48. *
  49. * @param var1 操作的对象
  50. * @param var2 var2字段内存地址偏移量
  51. * @param var4 新值
  52. * @return 返回旧值
  53. */
  54. public final long getAndSetLong(Object var1, long var2, long var4) {
  55. long var6;
  56. do {
  57. var6 = this.getLongVolatile(var1, var2);
  58. } while (!this.compareAndSwapLong(var1, var2, var6, var4));
  59. return var6;
  60. }
  61. /**
  62. * Object类型值原子操作方法,将var2地址对应的值置为var4
  63. *
  64. * @param var1 操作的对象
  65. * @param var2 var2字段内存地址偏移量
  66. * @param var4 新值
  67. * @return 返回旧值
  68. */
  69. public final Object getAndSetObject(Object var1, long var2, Object var4) {
  70. Object var5;
  71. do {
  72. var5 = this.getObjectVolatile(var1, var2);
  73. } while (!this.compareAndSwapObject(var1, var2, var5, var4));
  74. return var5;
  75. }

看一下上面的方法,内部通过自旋的CAS操作实现的,这些方法都可以保证操作的数据在多线程环境中的原子性,正确性。

来个示例,我们还是来实现一个网站计数功能,同时有100个人发起对网站的请求,每个人发起10次请求,每次请求算一次,最终结果是1000次,代码如下:

  1. package com.itsoku.chat21;
  2. import sun.misc.Unsafe;
  3. import java.lang.reflect.Field;
  4. import java.util.concurrent.CountDownLatch;
  5. import java.util.concurrent.TimeUnit;
  6. /**
  7. * 跟着阿里p7学并发,微信公众号:javacode2018
  8. */
  9. public class Demo2 {
  10. static Unsafe unsafe;
  11. //用来记录网站访问量,每次访问+1
  12. static int count;
  13. //count在Demo.class对象中的地址偏移量
  14. static long countOffset;
  15. static {
  16. try {
  17. //获取Unsafe对象
  18. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  19. field.setAccessible(true);
  20. unsafe = (Unsafe) field.get(null);
  21. Field countField = Demo2.class.getDeclaredField("count");
  22. //获取count字段在Demo2中的内存地址的偏移量
  23. countOffset = unsafe.staticFieldOffset(countField);
  24. } catch (Exception e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. //模拟访问一次
  29. public static void request() throws InterruptedException {
  30. //模拟耗时5毫秒
  31. TimeUnit.MILLISECONDS.sleep(5);
  32. //对count原子加1
  33. unsafe.getAndAddInt(Demo2.class, countOffset, 1);
  34. }
  35. public static void main(String[] args) throws InterruptedException {
  36. long starTime = System.currentTimeMillis();
  37. int threadSize = 100;
  38. CountDownLatch countDownLatch = new CountDownLatch(threadSize);
  39. for (int i = 0; i < threadSize; i++) {
  40. Thread thread = new Thread(() -> {
  41. try {
  42. for (int j = 0; j < 10; j++) {
  43. request();
  44. }
  45. } catch (InterruptedException e) {
  46. e.printStackTrace();
  47. } finally {
  48. countDownLatch.countDown();
  49. }
  50. });
  51. thread.start();
  52. }
  53. countDownLatch.await();
  54. long endTime = System.currentTimeMillis();
  55. System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);
  56. }
  57. }

输出:

  1. main,耗时:114,count=1000

代码中我们在静态块中通过反射获取到了Unsafe类的实例,然后获取Demo2中count字段内存地址偏移量countOffset,main方法中模拟了100个人,每人发起10次请求,等到所有请求完毕之后,输出count的结果。

代码中用到了CountDownLatch,通过countDownLatch.await()让主线程等待,等待100个子线程都执行完毕之后,主线程在进行运行。CountDownLatch的使用可以参考:java高并发系列 - 第16天:JUC中等待多线程完成的工具类CountDownLatch,必备技能

Unsafe中线程调度相关方法

这部分,包括线程挂起、恢复、锁机制等方法。

  1. //取消阻塞线程
  2. public native void unpark(Object thread);
  3. //阻塞线程,isAbsolute:是否是绝对时间,如果为true,time是一个绝对时间,如果为false,time是一个相对时间,time表示纳秒
  4. public native void park(boolean isAbsolute, long time);
  5. //获得对象锁(可重入锁)
  6. @Deprecated
  7. public native void monitorEnter(Object o);
  8. //释放对象锁
  9. @Deprecated
  10. public native void monitorExit(Object o);
  11. //尝试获取对象锁
  12. @Deprecated
  13. public native boolean tryMonitorEnter(Object o);

调用park后,线程将被阻塞,直到unpark调用或者超时,如果之前调用过unpark,不会进行阻塞,即parkunpark不区分先后顺序。monitorEnter、monitorExit、tryMonitorEnter 3个方法已过期,不建议使用了。

park和unpark示例

代码如下:

  1. package com.itsoku.chat21;
  2. import sun.misc.Unsafe;
  3. import java.lang.reflect.Field;
  4. import java.util.concurrent.TimeUnit;
  5. /**
  6. * 跟着阿里p7学并发,微信公众号:javacode2018
  7. */
  8. public class Demo3 {
  9. static Unsafe unsafe;
  10. static {
  11. try {
  12. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  13. field.setAccessible(true);
  14. unsafe = (Unsafe) field.get(null);
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. /**
  20. * 调用park和unpark,模拟线程的挂起和唤醒
  21. *
  22. * @throws InterruptedException
  23. */
  24. public static void m1() throws InterruptedException {
  25. Thread thread = new Thread(() -> {
  26. System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",start");
  27. unsafe.park(false, 0);
  28. System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",end");
  29. });
  30. thread.setName("thread1");
  31. thread.start();
  32. TimeUnit.SECONDS.sleep(5);
  33. unsafe.unpark(thread);
  34. }
  35. /**
  36. * 阻塞指定的时间
  37. */
  38. public static void m2() {
  39. Thread thread = new Thread(() -> {
  40. System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",start");
  41. //线程挂起3秒
  42. unsafe.park(false, TimeUnit.SECONDS.toNanos(3));
  43. System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",end");
  44. });
  45. thread.setName("thread2");
  46. thread.start();
  47. }
  48. public static void main(String[] args) throws InterruptedException {
  49. m1();
  50. m2();
  51. }
  52. }

输出:

  1. 1565000238474,thread1,start
  2. 1565000243475,thread1,end
  3. 1565000243475,thread2,start
  4. 1565000246476,thread2,end

m1()中thread1调用park方法,park方法会将当前线程阻塞,被阻塞了5秒之后,被主线程调用unpark方法给唤醒了,unpark方法参数表示需要唤醒的线程。

线程中相当于有个许可,许可默认是0,调用park的时候,发现是0会阻塞当前线程,调用unpark之后,许可会被置为1,并会唤醒当前线程。如果在park之前先调用了unpark方法,执行park方法的时候,不会阻塞。park方法被唤醒之后,许可又会被置为0。多次调用unpark的效果是一样的,许可还是1。

juc中的LockSupport类是通过unpark和park方法实现的,需要了解LockSupport可以移步:

Unsafe锁示例

代码如下:

  1. package com.itsoku.chat21;
  2. import sun.misc.Unsafe;
  3. import java.lang.reflect.Field;
  4. import java.util.concurrent.CountDownLatch;
  5. import java.util.concurrent.TimeUnit;
  6. /**
  7. * 跟着阿里p7学并发,微信公众号:javacode2018
  8. */
  9. public class Demo4 {
  10. static Unsafe unsafe;
  11. //用来记录网站访问量,每次访问+1
  12. static int count;
  13. static {
  14. try {
  15. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  16. field.setAccessible(true);
  17. unsafe = (Unsafe) field.get(null);
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. //模拟访问一次
  23. public static void request() {
  24. unsafe.monitorEnter(Demo4.class);
  25. try {
  26. count++;
  27. } finally {
  28. unsafe.monitorExit(Demo4.class);
  29. }
  30. }
  31. public static void main(String[] args) throws InterruptedException {
  32. long starTime = System.currentTimeMillis();
  33. int threadSize = 100;
  34. CountDownLatch countDownLatch = new CountDownLatch(threadSize);
  35. for (int i = 0; i < threadSize; i++) {
  36. Thread thread = new Thread(() -> {
  37. try {
  38. for (int j = 0; j < 10; j++) {
  39. request();
  40. }
  41. } finally {
  42. countDownLatch.countDown();
  43. }
  44. });
  45. thread.start();
  46. }
  47. countDownLatch.await();
  48. long endTime = System.currentTimeMillis();
  49. System.out.println(Thread.currentThread().getName() + ",耗时:" + (endTime - starTime) + ",count=" + count);
  50. }
  51. }

输出:

  1. main,耗时:64,count=1000

monitorEnter、monitorExit都有1个参数,表示上锁的对象。用法和synchronized关键字语义类似。

注意:

  1. monitorEnter、monitorExit、tryMonitorEnter 3个方法已过期,不建议使用了
  2. monitorEnter、monitorExit必须成对出现,出现的次数必须一致,也就是说锁了n次,也必须释放n次,否则会造成死锁

Unsafe中保证变量的可见性

关于变量可见性需要先了解java内存模型JMM,可以移步到:

JMM相关的一些概念

volatile与Java内存模型

java中操作内存分为主内存和工作内存,共享数据在主内存中,线程如果需要操作主内存的数据,需要先将主内存的数据复制到线程独有的工作内存中,操作完成之后再将其刷新到主内存中。如线程A要想看到线程B修改后的数据,需要满足:线程B修改数据之后,需要将数据从自己的工作内存中刷新到主内存中,并且A需要去主内存中读取数据。

被关键字volatile修饰的数据,有2点语义:

  1. 如果一个变量被volatile修饰,读取这个变量时候,会强制从主内存中读取,然后将其复制到当前线程的工作内存中使用
  2. 给volatile修饰的变量赋值的时候,会强制将赋值的结果从工作内存刷新到主内存

上面2点语义保证了被volatile修饰的数据在多线程中的可见性。

Unsafe中提供了和volatile语义一样的功能的方法,如下:

  1. //设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
  2. public native void putIntVolatile(Object o, long offset, int x);
  3. //获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
  4. public native int getIntVolatile(Object o, long offset);

putIntVolatile方法,2个参数:

o:表示需要操作的对象

offset:表示操作对象中的某个字段地址偏移量

x:将offset对应的字段的值修改为x,并且立即刷新到主存中

调用这个方法,会强制将工作内存中修改的数据刷新到主内存中。

getIntVolatile方法,2个参数

o:表示需要操作的对象

offset:表示操作对象中的某个字段地址偏移量

每次调用这个方法都会强制从主内存读取值,将其复制到工作内存中使用。

其他的还有几个putXXXVolatile、getXXXVolatile方法和上面2个类似。

本文主要讲解这些内容,希望您能有所收获,谢谢。

Unsafe中Class相关方法

此部分主要提供Class和它的静态字段的操作相关方法,包含静态字段内存定位、定义类、定义匿名类、检验&确保初始化等。

  1. //获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
  2. public native long staticFieldOffset(Field f);
  3. //获取一个静态类中给定字段的对象指针
  4. public native Object staticFieldBase(Field f);
  5. //判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。
  6. public native boolean shouldBeInitialized(Class<?> c);
  7. //检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。
  8. public native void ensureClassInitialized(Class<?> c);
  9. //定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
  10. public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
  11. //定义一个匿名类
  12. public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

示例:staticFieldOffset、staticFieldBase、staticFieldBase

  1. package com.itsoku.chat21;
  2. import lombok.extern.slf4j.Slf4j;
  3. import sun.misc.Unsafe;
  4. import java.lang.reflect.Field;
  5. import java.util.Arrays;
  6. import java.util.List;
  7. import java.util.concurrent.TimeUnit;
  8. /**
  9. * 跟着阿里p7学并发,微信公众号:javacode2018
  10. */
  11. @Slf4j
  12. public class Demo7 {
  13. static Unsafe unsafe;
  14. //静态属性
  15. private static Object v1;
  16. //实例属性
  17. private Object v2;
  18. static {
  19. //获取Unsafe对象
  20. try {
  21. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  22. field.setAccessible(true);
  23. unsafe = (Unsafe) field.get(null);
  24. } catch (Exception e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. public static void main(String[] args) throws NoSuchFieldException {
  29. Field v1Field = Demo7.class.getDeclaredField("v1");
  30. Field v2Field = Demo7.class.getDeclaredField("v2");
  31. System.out.println(unsafe.staticFieldOffset(v1Field));
  32. System.out.println(unsafe.objectFieldOffset(v2Field));
  33. System.out.println(unsafe.staticFieldBase(v1Field)==Demo7.class);
  34. }
  35. }

输出:

  1. 112
  2. 12
  3. true

可以看出staticFieldBase返回的就是Demo2的class对象。

示例:shouldBeInitialized、ensureClassInitialized

  1. package com.itsoku.chat21;
  2. import lombok.extern.slf4j.Slf4j;
  3. import sun.misc.Unsafe;
  4. import java.lang.reflect.Field;
  5. import java.util.concurrent.TimeUnit;
  6. /**
  7. * 跟着阿里p7学并发,微信公众号:javacode2018
  8. */
  9. public class Demo8 {
  10. static Unsafe unsafe;
  11. static {
  12. //获取Unsafe对象
  13. try {
  14. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  15. field.setAccessible(true);
  16. unsafe = (Unsafe) field.get(null);
  17. } catch (Exception e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. static class C1 {
  22. private static int count;
  23. static {
  24. count = 10;
  25. System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",C1 static init.");
  26. }
  27. }
  28. static class C2 {
  29. private static int count;
  30. static {
  31. count = 11;
  32. System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",C2 static init.");
  33. }
  34. }
  35. public static void main(String[] args) throws NoSuchFieldException {
  36. //判断C1类是需要需要初始化,如果已经初始化了,会返回false,如果此类没有被初始化过,返回true
  37. if (unsafe.shouldBeInitialized(C1.class)) {
  38. System.out.println("C1需要进行初始化");
  39. //对C1进行初始化
  40. unsafe.ensureClassInitialized(C1.class);
  41. }
  42. System.out.println(C2.count);
  43. System.out.println(unsafe.shouldBeInitialized(C1.class));
  44. }
  45. }

输出:

  1. C1需要进行初始化
  2. 1565069660679,mainC1 static init.
  3. 1565069660680,mainC2 static init.
  4. 11
  5. false

代码中C1未被初始化过,所以unsafe.shouldBeInitialized(C1.class)返回true,然后调用unsafe.ensureClassInitialized(C1.class)进行初始化。

代码中执行C2.count会触发C2进行初始化,所以shouldBeInitialized(C1.class)返回false

对象操作的其他方法

  1. //返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
  2. public native long objectFieldOffset(Field f);
  3. //获得给定对象的指定地址偏移量的值,与此类似操作还有:getInt,getDouble,getLong,getChar等
  4. public native Object getObject(Object o, long offset);
  5. //给定对象的指定地址偏移量设值,与此类似操作还有:putInt,putDouble,putLong,putChar等
  6. public native void putObject(Object o, long offset, Object x);
  7. //从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
  8. public native Object getObjectVolatile(Object o, long offset);
  9. //存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
  10. public native void putObjectVolatile(Object o, long offset, Object x);
  11. //有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到,只有在field被volatile修饰符修饰时有效
  12. public native void putOrderedObject(Object o, long offset, Object x);
  13. //绕过构造方法、初始化代码来创建对象
  14. public native Object allocateInstance(Class<?> cls) throws InstantiationException;

getObject相当于获取对象中字段的值,putObject相当于给字段赋值,有兴趣的可以自己写个例子看看效果。

绕过构造方法创建对象

介绍一下allocateInstance,这个方法可以绕过构造方法来创建对象,示例代码如下:

  1. package com.itsoku.chat21;
  2. import sun.misc.Unsafe;
  3. import java.lang.reflect.Field;
  4. /**
  5. * 跟着阿里p7学并发,微信公众号:javacode2018
  6. */
  7. public class Demo9 {
  8. static Unsafe unsafe;
  9. static {
  10. //获取Unsafe对象
  11. try {
  12. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  13. field.setAccessible(true);
  14. unsafe = (Unsafe) field.get(null);
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. static class C1 {
  20. private String name;
  21. private C1() {
  22. System.out.println("C1 default constructor!");
  23. }
  24. private C1(String name) {
  25. this.name = name;
  26. System.out.println("C1 有参 constructor!");
  27. }
  28. }
  29. public static void main(String[] args) throws InstantiationException {
  30. System.out.println(unsafe.allocateInstance(C1.class));
  31. }
  32. }

输出:

  1. com.itsoku.chat21.Demo9$C1@782830e

看一下类C1中有两个构造方法,都是private的,通过new、反射的方式都无法创建对象。但是可以通过Unsafe的allocateInstance方法绕过构造函数来创建C1的实例,输出的结果中可以看出创建成功了,并且没有调用构造方法。

典型应用

  • 常规对象实例化方式:我们通常所用到的创建对象的方式,从本质上来讲,都是通过new机制来实现对象的创建。但是,new机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。
  • 非常规的实例化方式:而Unsafe中提供allocateInstance方法,仅通过Class对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM安全检查等。它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance在java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。

数组相关的一些方法

这部分主要介绍与数据操作相关的arrayBaseOffset与arrayIndexScale这两个方法,两者配合起来使用,即可定位数组中每个元素在内存中的位置。

  1. //返回数组中第一个元素的偏移地址
  2. public native int arrayBaseOffset(Class<?> arrayClass);
  3. //返回数组中一个元素占用的大小
  4. public native int arrayIndexScale(Class<?> arrayClass);

这两个与数据操作相关的方法,在java.util.concurrent.atomic 包下的AtomicIntegerArray(可以实现对Integer数组中每个元素的原子性操作)中有典型的应用,如下图AtomicIntegerArray源码所示,通过Unsafe的arrayBaseOffset、arrayIndexScale分别获取数组首元素的偏移地址base及单个元素大小因子scale。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位,如下图二所示的getAndAdd方法即通过checkedByteOffset方法获取某数组元素的偏移地址,而后通过CAS实现原子性操作。

数组元素定位:

Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,这些常量值是通过arrayIndexScale方法得到的。arrayIndexScale方法也是一个本地方法,可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。

内存屏障相关操作

在Java 8中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。

  1. //内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
  2. public native void loadFence();
  3. //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
  4. public native void storeFence();
  5. //内存屏障,禁止load、store操作重排序
  6. public native void fullFence();

Unsafe相关的就介绍这么多!

最新资料

更多内容请访问:IT源点

相关文章推荐

全部评论: 0

    我有话说: