Java中的指针:Unsafe类

Unsafe(sun.misc.Unsafe)是jdk中自带的一个类,因为是在sun.misc包下,所以一般也不建议直接使用,同时Unsafe提供了一组底层(low-level)、unsafe的操作。Unsafe类注释中也有这样一段描述:

虽然该类及所有方法都是公开的,但使用该类是有限的,因为只有受信任的代码才能获得它的实例。

1.获取Unsafe实例

Unsafe对象不能直接通过构造函数创建,因为Unsafe被设计为单例模式。也不能通过getUnsafe方法获取,因为getUnsafe被设计成只能从引导类加载器(bootstrap class loader)加载。下面是getUnsafe方法代码:

  @CallerSensitive
  public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
      throw new SecurityException("Unsafe");
    } else {
      return theUnsafe;
    }
  }

Unsafe类中的theUnsafe静态成员变量:private static final Unsafe theUnsafe;没错就是自身的引用。并且在源码中存在一段static代码块:

 static {
   ...
    theUnsafe = new Unsafe();
    ...
  }

也就是说该theUnsafe静态成员变量在Unsafe第一次使用时就已经初始化。因此可以通过反射的方式来访问Unsafe类中的theUnsafe静态成员变量。

Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);

Unsafe的大部分API都是native的方法,主要包括以下几类:

  1. class相关主要提供Class和它的静态字段的操作方法。Object相关主要提供Object和它的字段的操作方法。 Arrray相关。主要提供数组及其中元素的操作方法。
  2. 并发相关。主要提供低级别同步原语,如CAS、线程调度、volatile、内存屏障等。
  3. Memory相关。提供了直接内存访问方法(绕过Java堆直接操作本地内存),可做到像C一样自由利用系统内存资源。
2. 对象和类相关

看一个例子来体会一下Unsafe的操作对象

public class UnsafeUser {
  public static void main(String[] args) throws Exception {
    //通过反射实例化Unsafe
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);
    //实例化User
    User player = (User) unsafe.allocateInstance(User.class);
    player.setName("four you");
    System.out.println(player.getName());
  }
}

class User{
  private String name;
  private User(){
    System.out.println("Constracter userd");
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
}

输出的结果为:four you,注意并没有将构造函数中的语句执行。

allocateInstance方法作用是创建对象,但并不会调用其构造方法。如果类未被初始化,将初始化类。看到作者表示一脸蒙蔽,经仔细回想普通创建对象的方式new或者通过反射机制都是需要调用构造函数的也就是new的操作,在字节码层面会执行三条指令:

  1. 根据类型分配一块内存区域

  2. 第一条指令返回的内存地址压入操作数栈顶

  3. 调用类的构造函数

allocateInstance(Class)方法只做了第一步和第二步,即分配内存空间,返回内存地址,没有做第三步调用构造函数。所以Unsafe.allocateInstance()方法创建的对象都是只有初始值,没有默认值也没有构造函数设置的值,因为它完全没有使用new机制,直接操作内存创建了对象。

对象和类相关API还有:

	//获得对象的字段偏移量 
	public native long objectFieldOffset(Field f); 
	//获得给定对象地址偏移量的int值
	public native int getInt(Object o, long offset);
	//设置给定对象地址偏移量的int值
	public native void putInt(Object o, long offset, int x);
	//静态属性的偏移量,用于在对应的Class对象中读写静态属性
	public native long staticFieldOffset(Field f);
	//获得给定对象地址偏移量的int值
	public native int getInt(Object o, long offset);
	//设置给定对象地址偏移量的int值
	public native void putInt(Object o, long offset, int x);

	//获取数组第一个元素的偏移地址
	public native int arrayBaseOffset(Class arrayClass);
	//数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,可定位数组中每个元素在内存中的位置
	public native int arrayIndexScale(Class arrayClass);

练习一下:

public class UnsafeUser{
  public static void main(String[] args) throws Exception {
    //通过反射实例化Unsafe
    Field f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);
    Field f1 = User.class.getDeclaredField("name");
    Field f2 = User.class.getDeclaredField("age");
    //获取普通变量的偏移量
    System.out.println(unsafe.objectFieldOffset(f1));
    //获取静态变量的偏移量
    System.out.println(unsafe.staticFieldOffset(f2));
  }
}

 class User {

   private static Integer age = 22;
   private String name;

   private User() {
     System.out.println("Constracter userd");
   }

   public String getName() {
     return name;
   }

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

   public static Integer getAge() {
     return age;
   }
 }

类似的操作类和对象的方法还有很多,不在此处一一举例了。

3. 并发相关

3.1 CAS相关

CAS:CompareAndSwap,内存偏移地址offset,预期值expected,新值x。如果变量在当前时刻的值和预期值expected相等,尝试将变量的值更新为x。如果更新成功,返回true;否则,返回false。原子类的底层就是调用Unsafe的CAS方法,而unsafe的底层是通过cpu的原子指令来实现。

//第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
//expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  

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

public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

JDK 1.8新增的几个方法,它们的实现是基于上述的CAS方法,如下

 //1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,
 //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
 public final int getAndAddInt(Object o, long offset, int delta) {
     int v;
     do {
         //获取内存中最新值
         v = getIntVolatile(o, offset);
       //通过CAS操作
     } while (!compareAndSwapInt(o, offset, v, v + delta));
     return v;
 }

//1.8新增,方法作用同上,只不过这里操作的long类型数据
 public final long getAndAddLong(Object o, long offset, long delta) {
     long v;
     do {
         v = getLongVolatile(o, offset);
     } while (!compareAndSwapLong(o, offset, v, v + delta));
     return v;
 }

 //1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,
 //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
 public final int getAndSetInt(Object o, long offset, int newValue) {
     int v;
     do {
         v = getIntVolatile(o, offset);
     } while (!compareAndSwapInt(o, offset, v, newValue));
     return v;
 }

// 1.8新增,同上,操作的是long类型
 public final long getAndSetLong(Object o, long offset, long newValue) {
     long v;
     do {
         v = getLongVolatile(o, offset);
     } while (!compareAndSwapLong(o, offset, v, newValue));
     return v;
 }

 //1.8新增,同上,操作的是引用类型数据
 public final Object getAndSetObject(Object o, long offset, Object newValue) {
     Object v;
     do {
         v = getObjectVolatile(o, offset);
     } while (!compareAndSwapObject(o, offset, v, newValue));
     return v;
 }
3.2挂起与恢复

将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。Java对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,其底层实现最终还是使用Unsafe.park()方法和Unsafe.unpark()方法

//线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。  
public native void park(boolean isAbsolute, long time);  

//终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,其底层正是使用这两个方法,  
public native void unpark(Object thread); 

3.3volatile相关读写

Java中的基本类型(boolean、byte、char、short、int、long、float、double)及对象引用类型都有以下方法。

//从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
//相当于getObject(Object, long)的volatile版本
public native Object getObjectVolatile(Object o, long offset);
  
//存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
//相当于putObject(Object, long, Object)的volatile版本
public native void putObjectVolatile(Object o, long offset, Object x);

4.内存相关(非堆内存)

allocateMemory所分配的内存需要手动free(不被GC回收)

//(boolean、byte、char、short、int、long、float、double)都有以下get、put两个方法。 
//获得给定地址上的int值
public native int getInt(long address);
//设置给定地址上的int值
public native void putInt(long address, int x);
//获得本地指针
public native long getAddress(long address);
//存储本地指针到给定的内存地址
public native void putAddress(long address, long x);
  
//分配内存
public native long allocateMemory(long bytes);
//重新分配内存
public native long reallocateMemory(long address, long bytes);
//初始化内存内容
public native void setMemory(Object o, long offset, long bytes, byte value);
//初始化内存内容
public void setMemory(long address, long bytes, byte value) {
 setMemory(null, address, bytes, value);
}
//内存内容拷贝
public native void copyMemory(Object srcBase, long srcOffset,
    Object destBase, long destOffset,
    long bytes);
//内存内容拷贝
public void copyMemory(long srcAddress, long destAddress, long bytes) {
 copyMemory(null, srcAddress, null, destAddress, bytes);
}
//释放内存
public native void freeMemory(long address);

操做这些API感觉像是在写C++…

还有一些:

	//返回指针的大小。返回值为4或8。
	public native int addressSize();
	/** The value of {@code addressSize()} */
	public static final int ADDRESS_SIZE = theUnsafe.addressSize();
	  
	//内存页的大小。
	public native int pageSize();

引用一段话结束本文:

在Java中使用本地内存有它的意义。从延迟的角度来说,直接访问本地内存不会比访问Java堆快。这个结论其实是有道理的,因为跨越JVM屏障肯定是有开销的。这样的结论对使用本地还是堆的ByteBuffer同样适用。使用本地ByteBuffer的速度提升不在于访问这些内存,而是它可以直接与操作系统提供的本地IO进行操作。

你可能感兴趣的:(并发编程)