Unsafe类详解

Java 不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe 类提供了硬件级别的原子操作。

Unsafe 类在 sun.misc 包下,不属于 Java 标准。很多 Java 的基础类库,包括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发,比如 Netty、Hadoop、Kafka 等。

  • Unsafe 是用于在实质上扩展 Java 语言表达能力、便于在更高层(Java 层)代码里实现原本要在更低层(C 层)实现的核心库功能用的。
  • 这些功能包括裸内存的申请/释放/访问,低层硬件的 atomic/volatile 支持,创建未初始化对象等。
  • 它原本的设计就只应该被标准库使用,因此不建议在生产环境中使用。

获取实例

Unsafe 对象不能直接通过 new Unsafe() 或调用 Unsafe.getUnsafe() 获取。

Unsafe 被设计成单例模式,构造方法私有。

getUnsafe 被设计成只能从引导类加载器(bootstrap class loader)加载。

private Unsafe() {
}

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

非启动类加载器直接调用 Unsafe.getUnsafe() 方法会抛出 SecurityException 异常。

解决办法有两个:

  • 令代码 " 受信任 "

运行程序时,通过 JVM 参数设置 bootclasspath 选项,指定系统类路径加上使用的一个 Unsafe 路径。

java -Xbootclasspath:/usr/jdk1.7.0/jre/lib/rt.jar:. com.mishadoff.magic.UnsafeClient
  • Java 反射机制

通过将 private 单例实例暴力设置 accessible 为 true,然后通过 Field 的 get 方法,直接获取一个 Object 强制转换为 Unsafe。

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

在 IDE 中,这些方法会被标志为 Error,可以通过以下设置解决:

Preferences -> Java -> Compiler -> Errors/Warnings -> Deprecated and restricted API -> Forbidden reference -> Warning

常用方法

Unsafe 的大部分 API 都是 native 的方法。

Classes/Objects

创建类对象,不进行初始化:

//实例化对象,但是不进行初始化,类初始化和实例初始化都不调用
Object allocateInstance(Class cls)

对象和字段的地址获取:

//字段在内存中的地址相对于实例对象内存地址的偏移量
public long objectFieldOffset(Field f)
public long objectFieldOffset(Class c, String name)
//静态字段在类对象中的偏移
public long staticFieldOffset(Field f)
//获得静态字段所对应类对象,等同于f.getDeclaringClass()
public Object staticFieldBase(Field f)

读取/修改对象上指定偏移位置的值,其他基本数据类型(boolean,char,byte,short,float,double,long)类似:

public native int getInt(Object o, long offset);
public native void putInt(Object o, long offset, int x);

此外还可以读取/设定引用值reference value,指针native pointer

//获得给定对象偏移量上的引用类型的值
public native Object getObject(Object o, long offset);
//设置给定对象偏移量上的引用类型的值
public native void putObject(Object o, long offset, Object x);
//获取、设定本地指针的值,等同于获取、设定int或者是long类型的数据
public long getAddress(Object o, long offset)
public void putAddress(Object o, long offset, long x)

以上方法都是通过两个参数引用到一个变量,叫做double-register地址模式,
当Object引用为null时,把offset作为绝对地址,使用绝对地址的API,叫做single-register地址模式,例如:

public void putInt(long address, int x) {
	putInt(null, address, x);
}

此时address可以指向通过Unsafe.allocateMemory获取的内存块中的地址,类似于C语言中的指针,直接指明实际要读写的地址

数组相关

数组头部还存储有数组的长度信息,索引访问数组元素时需要知道第一个元素与起始位置的偏移地址,Unsafe类包含所有的基本数据类型和Object类型的偏移的常量值,名称为ARRAY_***_BASE_OFFSET

同时访问数组第i个元素,对应偏移的确定需要乘以一个比例值,即数组中单个元素的长度,Unsafe中对应的常量为ARRAY_***_INDEX_SCALE

// 初始偏移
public int arrayBaseOffset(Class arrayClass)

public static final int ARRAY_BOOLEAN_BASE_OFFSET
        = theUnsafe.arrayBaseOffset(boolean[].class);

public static final int ARRAY_OBJECT_BASE_OFFSET
        = theUnsafe.arrayBaseOffset(Object[].class);
// 比例值
public int arrayIndexScale(Class arrayClass)

public static final int ARRAY_BOOLEAN_INDEX_SCALE
        = theUnsafe.arrayIndexScale(boolean[].class);

public static final int ARRAY_OBJECT_INDEX_SCALE
        = theUnsafe.arrayIndexScale(Object[].class);

两个信息配合使用,就可以得到数组中每个元素相对内存起点的偏移,然后进行读写

// java8中的ConcurrentHashMap
Class ak = Node[].class;
ABASE = U.arrayBaseOffset(ak);
int scale = U.arrayIndexScale(ak);
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);

//具体使用,i代表索引
static final  boolean casTabAt(Node[] tab, int i,
									Node c, Node v) {
	return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

CAS 相关

compareAndSwap,内存偏移地址 offset,预期值 expected,新值 x。如果变量在当前时刻的值和预期值 expected 相等,尝试将变量的值更新为 x。如果更新成功,返回 true;否则,返回 false。

//更新变量值为x,如果当前值为expected
//o:对象 offset:偏移量 expected:期望值 x:新值
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);

compareAndSwapObject方法中的第一个参数和第二个参数,用于确定待操作对象在内存中的具体位置的,然后取出值和第三个参数进行比较,如果相等,则将内存中的值更新为第四个参数的值,同时返回true,表明原子更新操作完毕。反之则不更新内存中的值,同时返回false,表明原子操作失败。

同样的,compareAndSwapInt方法也是相似的道理,第一个,第二个参数用来确定当前操作对象在内存中的存储值,然后和第三个expect value比较,如果相等,则将内存值更新为第四个update value值。

JDK 1.8 中基于 CAS 扩展。作用都是,通过 CAS 设置新的值,返回旧的值。

//增加
public final int getAndAddInt(Object o, long offset, int delta) {
 int v;
 do {
 v = getIntVolatile(o, offset);
 } while (!compareAndSwapInt(o, offset, v, v + delta));
 return v;
}
  
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;
}
//设置
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;
}
  
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;
}

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;
  • ABA 问题

在多线程环境中,使用 CAS,如果一个线程对变量修改 2 次,第 2 次修改后的值和第 1 次修改前的值相同,其他线程对此一无所知,这类现象称为 ABA 问题。

ABA 问题可以使用 JDK 并发包中的 AtomicStampedReference 和 AtomicMarkableReference 处理。

AtomicStampedReference 是通过版本号(时间戳)来解决 ABA 问题的,也可以使用版本号(verison)来解决 ABA,即乐观锁每次在执行数据的修改操作时,都带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行 +1 操作,否则执行失败。

AtomicMarkableReference 则是将一个 boolean 值作是否有更改的标记,本质就是它的版本号只有两个,true 和 false,修改的时候在两个版本号之间来回切换,虽然这样做并不能解决 ABA 的问题,但是会降低 ABA 问题发生的几率。

线程调度相关

主要包括监视器锁定、解锁等。

//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁
public native void monitorEnter(Object o);
//释放对象锁
public native void monitorExit(Object o);
//尝试获取对象锁,返回 true 或 false 表示是否获取成功
public native boolean tryMonitorEnter(Object o);

volatile 相关读写

使用volatile机制加载读取数据,保证可见性,API包含所有的基本数据类型和Object类型

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

此外还有一种惰性设定值的方式,通常出现在AtomicXXX.lazySet,它允许volatile变量和后续的内存操作重排序,就像普通非volatile变量的写操作,所以其他线程可能不能立即获取到新的值

putOrderedObject(Object o, long offset, Object x)
putOrderedInt(Object o, long offset, int x)
putOrderedLong(Object o, long offset, long x)

内存屏障相关

JDK 1.8 引入 ,用于定义内存屏障,避免代码重排序。

//内存屏障,禁止 load 操作重排序,即屏障前的load操作不能被重排序到屏障后,屏障后的 load 操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止 store 操作重排序,即屏障前的 store 操作不能被重排序到屏障后,屏障后的 store 操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止 load、store 操作重排序
public native void fullFence();

内存管理(非堆内存)

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 long allocateMemory(long bytes)
//根据给定的内存地址address调整内存大小
public long reallocateMemory(long address, long bytes)

//将内存块中的所有字节设置为固定值,类似于C中的memoryset函数
public void setMemory(Object o, long offset, long bytes, byte value)
public void setMemory(long address, long bytes, byte value)

//内存复制,支持两种地址模式
public void copyMemory(Object srcBase, long srcOffset,
                        Object destBase, long destOffset,
                        long bytes)
public void copyMemory(long srcAddress, long destAddress, long bytes)

//释放allocateMemory和reallocateMemory申请的内存
public native void freeMemory(long l);

系统相关

//返回指针的大小。返回值为 4 或 8。
public native int addressSize();

/** The value of {@code addressSize()} */
public static final int ADDRESS_SIZE = theUnsafe.addressSize();
  
//内存页的大小。
public native int pageSize();

其他

//获取系统的平均负载值,loadavg 这个 double 数组将会存放负载值的结果,nelems 决定样本数量,nelems 只能取值为 1 到 3,分别代表最近 1、5、15 分钟内系统的平均负载。
//如果无法获取系统的负载,此方法返回 -1,否则返回获取到的样本数量(loadavg 中有效的元素个数)。
public native int getLoadAverage(double[] loadavg, int nelems);
//绕过检测机制直接抛出异常。
public native void throwException(Throwable ee);

Unsafe 类的使用场景

避免初始化

当想要绕过对象构造方法、安全检查器或者没有 public 的构造方法时,allocateInstance() 方法变得非常有用。

编写一个简单的 Java 类。

public class TestA {
    private int a = 0; 

    public TestA() {
        a = 1;
    }

    public int getA() {
        return a;
    }
}

构造方法、反射方法和 allocateInstance 方法的不同实现。

将 public 构造方法修改为 private,allocateInstance 方法可以得到同样的结果。

// constructor
TestA constructorA = new TestA();
System.out.println(constructorA.getA()); //print 1

// reflection
try {
     TestA reflectionA = TestA.class.newInstance();
     System.out.println(reflectionA.getA()); //print 1
} catch (InstantiationException e) {
     e.printStackTrace();
} catch (IllegalAccessException e) {
     e.printStackTrace();
}

// unsafe
Field f = null;
try {
     f = Unsafe.class.getDeclaredField("theUnsafe");
     f.setAccessible(true);
     Unsafe unsafe = (Unsafe) f.get(null);
     TestA unsafeA = (TestA) unsafe.allocateInstance(TestA.class);
     System.out.println(unsafeA.getA()); //print 0
} catch (NoSuchFieldException e) {
     e.printStackTrace();
} catch (IllegalAccessException e) {
     e.printStackTrace();
} catch (InstantiationException e) {
     e.printStackTrace();
}

内存修改

Unsafe 可用于绕过安全的常用技术,直接修改内存变量。

反射也可以实现相同的功能。但是 Unsafe 可以修改任何对象,甚至没有这些对象的引用。

编写一个简单的 Java 类。

public class TestA {

    private int ACCESS_ALLOWED = 1;

    public boolean giveAccess() {
        return 40 == ACCESS_ALLOWED;
    }
}

在正常情况下,giveAccess 总会返回 false。

通过计算内存偏移,并使用 putInt() 方法,类的 ACCESS_ALLOWED 被修改。

在已知类结构的时候,数据的偏移总是可以计算出来(与 c++ 中的类中数据的偏移计算是一致的)。

// constructor
TestA constructorA = new TestA();
System.out.println(constructorA.giveAccess()); //print false

// unsafe
Field f = null;
try {
    f = Unsafe.class.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe unsafe = (Unsafe) f.get(null);
    TestA unsafeA = (TestA) unsafe.allocateInstance(TestA.class);
    Field unsafeAField = unsafeA.getClass().getDeclaredField("ACCESS_ALLOWED");
    unsafe.putInt(unsafeA, unsafe.objectFieldOffset(unsafeAField), 40); // memory corruption
    System.out.println(unsafeA.giveAccess()); //print true
} catch (NoSuchFieldException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (InstantiationException e) {
    e.printStackTrace();
}

动态类

可以在运行时创建一个类,比如从已编译的 .class 文件中将类内容读取为字节数组,并正确地传递给 defineClass 方法。

当必须动态创建类,而现有代码中有一些代理,这非常有用。

编写一个简单的 Java 类。

public class TestA {

    private int a = 1;

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }
}

动态创建类。

byte[] classContents = new byte[0];
try {
      classContents = getClassContent();
      Class c = getUnsafe().defineClass(null, classContents, 0, classContents.length);
      System.out.println(c.getMethod("getA").invoke(c.newInstance(), null)); //print 1
} catch (Exception e) {
      e.printStackTrace();
}

private static Unsafe getUnsafe() {
        Field f = null;
        Unsafe unsafe = null;
        try {
            f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return unsafe;
}

private static byte[] getClassContent() throws Exception {
        File f = new File("/home/test/TestA.class");
        FileInputStream input = new FileInputStream(f);
        byte[] content = new byte[(int) f.length()];
        input.read(content);
        input.close();
        return content;
}

大数组

Java 数组大小的最大值为 Integer.MAX_VALUE。使用直接内存分配,创建的数组大小受限于堆大小。

Unsafe 分配的内存,分配在非堆内存,因为不执行任何边界检查,所以任何非法访问都可能会导致 JVM 崩溃。

在需要分配大的连续区域、实时编程(不能容忍 JVM 延迟)时,可以使用它。java.nio 使用这一技术。

创建一个 Java 类。

public class SuperArray {

    private final static int BYTE = 1;

    private long size;
    private long address;

    public SuperArray(long size) {
        this.size = size;
        address = getUnsafe().allocateMemory(size * BYTE);
    }

    public void set(long i, byte value) {
        getUnsafe().putByte(address + i * BYTE, value);
    }

    public int get(long idx) {
        return getUnsafe().getByte(address + idx * BYTE);
    }

    public long size() {
        return size;
    }

    private static Unsafe getUnsafe() {
        Field f = null;
        Unsafe unsafe = null;
        try {
            f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return unsafe;
    }
}

使用大数组。

long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
SuperArray array = new SuperArray(SUPER_SIZE);
System.out.println("Array size:" + array.size()); //print 4294967294
int sum = 0;
for (int i = 0; i < 100; i++) {
     array.set((long) Integer.MAX_VALUE + i, (byte) 3);
     sum += array.get((long) Integer.MAX_VALUE + i);
}
System.out.println("Sum of 100 elements:" + sum);  //print 300

并发应用

compareAndSwap 方法是原子的,并且可用来实现高性能的、无锁的数据结构。

创建一个 Java 类。

public class CASCounter {

    private volatile long counter = 0;
    private Unsafe unsafe;
    private long offset;

    public CASCounter() throws Exception {
        unsafe = getUnsafe();
        offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
    }

    public void increment() {
        long before = counter;
        while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
            before = counter;
        }
    }

    public long getCounter() {
        return counter;
    }

    private static Unsafe getUnsafe() {
        Field f = null;
        Unsafe unsafe = null;
        try {
            f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return unsafe;
    }
}

使用无锁的数据结构。

public static void main(String[] args) {
        final TestB b = new TestB();
        Thread threadA = new Thread(new Runnable() {
            @Override public void run() {
                b.counter.increment();
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override public void run() {
                b.counter.increment();
            }
        });
        Thread threadC = new Thread(new Runnable() {
            @Override public void run() {
                b.counter.increment();
            }
        });
        threadA.start();
        threadB.start();
        threadC.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(b.counter.getCounter()); //print 3
}

private static class TestB {
        private CASCounter counter;

        public TestB() {
            try {
                counter = new CASCounter();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
}

挂起与恢复

public native void unpark(Thread jthread);  
public native void park(boolean isAbsolute, long time); // isAbsolute 参数是指明时间是绝对的,还是相对的。

将一个线程进行挂起是通过 park 方法实现,调用 park 后,线程将一直阻塞直到超时或者中断等条件出现。

unpark 可以终止一个挂起的线程,使其恢复正常。

整个并发框架中对线程的挂起操作被封装在 LockSupport 类中,LockSupport 类中有各种版本 pack 方法,但最终都调用的 Unsafe.park() 方法。

unpark 函数为线程提供 " 许可(permit)",线程调用 park 函数则等待 " 许可 "。

这个有点像信号量,但是这个 " 许可 " 不能叠加,是一次性的。

比如线程 B 连续调用了三次 unpark 函数,当线程 A 调用 park 函数就使用掉这个 " 许可 ",如果线程 A 再次调用 park,则进入等待状态。

Thread currThread = Thread.currentThread();
getUnsafe().unpark(currThread);
getUnsafe().unpark(currThread);
getUnsafe().unpark(currThread);

getUnsafe().park(false, 0);
getUnsafe().park(false, 0);
System.out.println("execute success"); // 线程挂起,不会打印。

unpark 函数可以先于 park 调用(但最好别这样做),比如线程 B 调用 unpark 函数,给线程 A 发了一个 " 许可 ",那么当线程 A 调用 park 时,发现已经有 " 许可 ",会马上再继续运行。

park 遇到线程终止时,会直接返回(不同于 Thread.sleep,Thread.sleep 遇到 thread.interrupt() 会抛异常)。

unpark 无法恢复处于 sleep 中的线程,只能与 park 配对使用,因为 unpark 发放的许可只有 park 能监听到。

因为 park 的特性,可以不用担心 park 的时序问题。

park / unpark 模型真正解耦了线程之间的同步,线程之间不再需要一个 Object 或者其它变量来存储状态,不再需要关心对方的状态。

参考资料

https://www.jianshu.com/p/2e5b92d0962e
https://www.jianshu.com/p/96ccc5dbd8c5

你可能感兴趣的:(Java,Java多线程,Java,Unsafe,CAS)