《Java修炼指南:高频源码解析》阅读笔记一Unsafe类

Java不能像C/C++一样直接操作内存区域,需要通过本地方法的方式来操作内存区域,JDK可以通过一个后门——Unsafe类,执行底层硬件级别的CAS原子操作,线程阻塞和唤醒等。
Unsafe位于sun.misc包下,Unsafe类中方法几乎全部都是Native方法,它们使用JNI的方式调用本地的C++类库。

CAS操作

CAS是一种实现并发算法时常用的技术,自旋锁和乐观锁的实现都用到了CAS算法,JUC并发包的绝大多数工具类,如原子类AtomicInteger和重入锁ReentrantLock,他们的源码实现中都有CAS的身影。
CAS是Compare And Swap的简称,即比较再替换。它是计算机处理器提供的一个原子命令,保证了比较和替换两个操作的原子性。CAS操作涉及三个操作数:CAS(V, E, N)

  1. V:要读写的内存地址;
  2. E:进行比较的值E(预期值);
  3. N:拟写入的新值。

CAS操作的含义是:当且仅当内存地址V中的值等于预期值E时,将内存V中的值更新为N,否则不操作(模拟CAS流程,并不是实际函数)
假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值,造成了数据不一致呢?答案是否定的,因为CAS是一条CPU原子指令,在执行过程中不允许被中断,所以不会造成所谓的数据不一致问题。

使用CAS过程中有以下三种问题:

  1. ABA问题

如果一个变量V初次读取的时候是A值,那在赋值的时候检查到它依然是A值,那么是否就能说明它的值就没有被其他线程修改过了呢?这个是不能的,因为在这段时间它的值可能被改成其他值,然后又改回了A,那么CAS操作就会误认为它从来没有被修改过。这个问题就是被称为CAS操作的ABA问题。
ABA问题的产生因为变量值产生了环形更改,即一个变量的值从A改成了B,随后又从B改回了A。如果变量的值只能朝一个方向转换的话,就不会构成“环形”问题了,比如使用版本号或者时间戳机制,版本号机制是每次更改版本号变量时将版本号增加1,这样就不会存在这个问题了。JDK中的AtomicStampedReference类使用的就是时间戳,他给每个变量配备一个时间戳,来避免ABA问题。

  1. 循环时间长开销大

自旋CAS(也就是更新不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。遇到这种情况,就需要对CAS操作限制重试上限,如果重试次数达到最大值,可以通过直接退出或者采用其他方式来代替CAS。比如synchronized同步锁,轻量级锁通过CAS自旋等待锁释放,在线程竞争激烈的情况下,自旋次数达到一定的数量时,synchronized内部会升级为重量级锁。

  1. 只能保证一个共享变量的原子操作

CAS操作只对单个共享变量有效,当操作跨越多个共享变量时CAS无效。

Unsafe重要方法

Unsafe提供了很多与底层相关的操作,主要关注与CAS和线程调度相关的方法。

初始化

Unsafe unsafe = Unsafe.getUnsafe();该方法只可以在JDK中进行使用,在其他包下,只可以通过反射进行初始化,否则会报

Exception in thread “main” java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.bendcap.java.jvm.unsafe.Main.main(Main.java:13)

这是因为 Unsafe 类主要是 JDK 内部使用,并不提供给普通用户调用,也就是其名字所暗示的那样,这些操作不安全。

public static Unsafe createUnsafe() {
        try {
            Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
            Field field = unsafeClass.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);
            return unsafe;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
}

Object allocateInstance(Class<?> var1)

Unsafe 只会分配 Person 对应的内存空间,而不触发构造函数。 : Class 文件中的 clinit 仍然会执行。

void putLongVolatile(Object var1, long var2, long var4)

类似方法有:putLong(Object var1, long var2, long var4)putOrderedLong(Object var1, long var2, long var4)
设置对象var1中内存偏移量地址var2对应的long型field的值为var4,这几个方法不同之处是,putLongVolatile支持volatile写内存语义,保证更新对所有线程立即可见
putOrderedLong该方法并不保证变量值的修改对其他线程立即可见
putLong就是设置值
还有其他的类似方法,针对于其他类型,包括:intlongfloatdoublebytecharshortObject等,其中Object是针对于引用类型,这个引用类型包括Integer等包装类。

Object getObject(Object var1, long var2)

类似方法有:getObjectVolatile(Object var1, long var2)getAndSetObject(Object var1, long var2, Object var4)
获取某个属性的值,其中var1是类实例,var2是offset。和putLongVolatile类似,还有一些其他类型的类似方法。
getAndSetObject是获取当前属性的值并且进行赋值,是一个原子操作,利用了CAS原理,并不是一个native函数,具体:

    public final Object getAndSetObject(Object var1, long var2, Object var4) {
        Object var5;
        do {
            var5 = this.getObjectVolatile(var1, var2);
        } while(!this.compareAndSwapObject(var1, var2, var5, var4));

        return var5;
    }

类似的实现还有getAndAddInt(Object obj, long offset, int delta)getAndAddLong(Object obj, long offset, long delta)getAndSetInt(Object obj, long offset, int update)getAndSetLong(Object obj, long offset, long update)

void throwException(Throwable var1)

通过 unsafe.throwExceptio 创建的异常不会被编译器检查,方法的调用者也不需要处理异常。

long objectFieldOffset(Field var1)

获取指定类中指定字段的内存偏移量地址。Unsafe可以通过类的实例和变量的偏移地址,直接读写实例对象中的值(如上面),一般用于CAS方法中。

try {
    valueOffset = unsafe.objectFieldOffset
        (AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }

boolean compareAndSwapObject(Object obj, long offset, Object expected, Object update)

通过该方法实现CAS操作,也就是 lock-free ,保证更好的性能。它的类似方法还有各种基本数据类型。
当且仅当对象obj中内存偏移量offset的field的值等于预期值expected时,将变量的值替换为uodate。替换成功返回true,否则返回false;

long allocateMemory(long var1)

类似方法:long reallocateMemory(long var1, long var3)void freeMemory(long var1)
Java 中对象分配一般是在 Heap 中进行的(例外是 TLAB等),当应用内存不足的时候,可以通过触发 GC 进行垃圾回收,但是如果有大量对象存活到永久代,并且仍然引用可达,那么我们就需要堆外内存(Off-Heap Memory)来缓解频繁 GC 造成的压力。
Unsafe.allocateMemory 给了我们在直接内存中分配对象的能力,这块内存是非堆内存,因此,不会受到 GC 的频繁分析和干扰。
虽然这样可以缓解大量对象占用内存对 GC 和 JVM 造成的压力,这也就需要我们手动管理内存,因此,在合适的事后我们需要手动调用 freeMemory来释放内存。

实例代码:

public class OffHeapArray {
    private final static int BYTE = 1;
    private long size;
    private long address;
    private Unsafe unsafe;

    public OffHeapArray(long size, Unsafe unsafe) {
        this.size = size;
        this.unsafe = unsafe;
        address = unsafe.allocateMemory(size * BYTE);
    }

    public void set(long i, byte value) {
        设置指定字节的数组
        unsafe.putByte(address + i * BYTE, value);
    }

    public int get(long idx) {
        // 获取指定字节的数据
        return unsafe.getByte(address + idx * BYTE);
    }

    public long size() {
        return size;
    }

    public void freeMemory() {
        unsafe.freeMemory(address);
    }
    public static void main(String[] args) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException {
        Test t = new Test();
        t.value = 1L;
        Class<?> aClass = Class.forName("sun.misc.Unsafe");
        Field theUnsafe = aClass.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe)theUnsafe.get(null);
        long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
        OffHeapArray array = new OffHeapArray(SUPER_SIZE, unsafe);
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            array.set((long) Integer.MAX_VALUE + i, (byte) 127);
            sum += array.get((long) Integer.MAX_VALUE + i);
        }
        System.out.println(sum);
    }
}

线程调度

相关方法:void park(boolean isAbsolute, long time)void unpark(Object thread)
park:阻塞当前线程;

  • isAbsolute:阻塞时间time是否是绝对时间
  • time:阻塞时间

如果isAbsolute=false 且 time =0,表示一直阻塞;
如果isAbsolute=false 且 time >0,表示等待指定时间后线程会被唤醒。time 是相对时间,即当前线程在等待time毫秒后被唤醒;
如果isAbsolute=true且 time >0,表示等待指定时间后线程会被唤醒。time 是绝对时间,是某个时间点换算成相对于新纪元之后的毫秒值;
线程调用park阻塞后被唤醒时机有:

  1. 其他线程以当前线程为参数调用unpark方法,当前线程被唤醒;
  2. 当time>0时,当设置的time时间到了,线程会被唤醒;
  3. 其他线程调用了当前线程的interrupt方法中断了当前线程,当前线程被唤醒。

unpark:唤醒调用park后被阻塞的线程,参数thread为唤醒线程的实例

注:本文为《Java修炼指南:高频源码解析》阅读笔记

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值