Unsafe 和 Atomic 详解

·  阅读 794
Unsafe 和 Atomic 详解

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

在JDK 5之后,Java类库中才开始使用CAS操作,该操作由sun.misc.Unsafe类里面的 compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。HotSpot虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程, 或者可以认为是无条件内联进去了。不过由于Unsafe类在设计上就不是提供给用户程序调用的类(Unsafe::getUnsafe()的代码中限制了只有启动类加载器(Bootstrap ClassLoader)加载的Class才能访问它),因此在JDK 9之前只有Java类库可以使用CAS,譬如J.U.C包里面的整数原子类,其中的 compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作来实现。而如果用户程序也有用CAS操作的需求,那要么就采用反射手段突破Unsafe的访问限制,要么就只能通过Java类库API来间接使用它。直到JDK 9之后,Java类库才在VarHandle类里开放了面向用户程序使用的CAS操作。

Unsafe 实现 CAS

核心 API

  1. compareAndSwapObject
  2. compareAndSwapInt
  3. compareAndSwapInt

原子变量提供的原子性来自CAS操作,CAS来自Unsafe,然后由CPU的 cmpxchg 指令来保证。

Atomic 工具包

Atomic 工具类分为一下几类

  • 普通原子类型:提供对boolean、int、long和对象的原子性操作。
    • AtomicBoolean
    • AtomicInteger
    • AtomicLong
    • AtomicReference
  • 原子类型数组:提供对数组元素的原子性操作。
    • AtomicLongArray
    • AtomicIntegerArray
    • AtomicReferenceArray
  • 原子类型字段更新器:提供对指定对象的指定字段进行原子性操作。
    • AtomicLongFieldUpdater
    • AtomicIntegerFieldUpdater
    • AtomicReferenceFieldUpdater
  • 带版本号的原子引用类型:以版本戳的方式解决原子类型的ABA问题。
    • AtomicStampedReference
    • AtomicMarkableReference
  • 原子累加器(JDK1.8):AtomicLong和AtomicDouble的升级类型,专门用于数据统计,性能更高。
    • DoubleAccumulator
    • DoubleAdder
    • LongAccumulator
    • LongAdder

使用案例

多线程累加

下面是创建 1000 个线程分别对 total 进行累加,由于没有对 total 做任何线程安全操作所以我们打印的结果一定是小于或者等于 1000000

public class AtomicTest {

    public static int total = 0;

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch downLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    total++;
                }
                downLatch.countDown();
            }).start();
        }
        downLatch.await();
        System.out.println(total);
    }
}

// 结果输出
// 999843
复制代码

通过 AtomicInteger 实现

我们可以通过 integer 的原子类,AtomicInteger 类来进行自增操作,然后调用 getAndIncrement() 方法,保证每次都是原子操作。最终的结果输出:1000000。

public class AtomicTest2 {

    public static AtomicInteger total = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch downLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    total.getAndIncrement();
                }
                downLatch.countDown();
            }).start();
        }
        downLatch.await();
        System.out.println(total.get());
    }
}

// 结果输出
// 1000000
复制代码

再来看看 getAndIncrement 方法它主要是做了哪些操作呢 ?

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

// 然后调用 unsafe 类的本地方法
// unsafe.getAndAddInt 方法
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

复制代码

对象属性进行原子操作

  1. 获取属性偏移量
  2. 通过 CAS 方式进行修改
public class UnsafeTest {
    public static Unsafe U;

    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            U = (Unsafe)f.get(null);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {

        User user = new User();
        // 获取字段
        Field age = user.getClass().getDeclaredField("age");
        // 获取字段相对Java对象的"起始地址"的偏移量
        long offset = U.objectFieldOffset(age);
        // 设置值
        boolean success = U.compareAndSwapInt(user, offset, 10, 20) ;
        System.out.println("修改结果: " + success ) ;
        // 打印数据
        System.out.println("查询结果: " + user.getAge());
    }


}

class User {
    private int age;

    public User() {
        this.age = 10;
    }

    public int getAge() {
        return age;
    }
}

//输出结果
修改结果: true
查询结果: 20
复制代码

注意事项

ABA 问题

ABA 是在 CAS 场景下面容易出现的问题,当一个数据的值为 A1, 然后再被修改为 B ,最后再修改为 A2。 并且 A1 和 A2 相等;但是另外一个线程查询到的结果是 A1 , 此时更新为 C ,也可以成功。这样的风险就是后者不清楚数据是否被改变,并且能够成功修改。

举个例子:你钱包里面有 100 块钱,然后小偷偷走后又给你换回来了,你看到的还是钱包里面的 100 块钱吗?再比如你老婆和其他的男人出轨了,然后再还回来,你看到的还是你的老婆吗?其实这些问题已经触犯到道德和FL的范围。

针对 ABA 问题,我们可以通过 AtomicStampedReference 解决。

AtomicStampedReference

AtomicStampedReference 维护一个对象引用以及一个整数“stamp”,该整数可以进行原子更新。 实现说明:此实现通过创建表示“装箱”[reference,integer] 对的内部对象来维护戳记引用。 在此之前我们先对 ABA 问题进行一个模拟:

public class ABATest {

    private static AtomicInteger index = new AtomicInteger(10);

    public static void main(String[] args) {
        new Thread(()-> {
            index.compareAndSet(10, 101);
            index.compareAndSet(101, 10);
            System.out.println(Thread.currentThread() + ": 10->101->10");
        }, "Tom").start();

        new Thread(()-> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = index.compareAndSet(10, 1000);
            System.out.println(Thread.currentThread() + ": 更新结果:" + result + ", 更新后的值: " + index.get());
        }, "Jack").start();
    }
}
复制代码

通过 AtomicStampedReference 来解决这个问题: 就是我们在进入第二个线程之前,先读取当前的版本号,然后进入更新。这个读取的版本号可能是一个旧的值。如果出现这种情况,那么我们执行 compareAndSet 就会返回失败。

public class ABATest2 {

    private static AtomicStampedReference index = new AtomicStampedReference(10, 1);

    public static void main(String[] args) {
        new Thread(() -> {
            index.compareAndSet(10, 101, index.getStamp(), index.getStamp() + 1);
            index.compareAndSet(101, 10, index.getStamp(), index.getStamp() + 1);
            System.out.println(Thread.currentThread() + ": 10->101->10");
        }, "Tom").start();

        int stamp = index.getStamp();
        new Thread(() -> {

            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = index.compareAndSet(10, 1000, stamp, stamp + 1);
            System.out.println(Thread.currentThread() + ": 更新结果:" + result + ", 更新后的值: " + index.getReference());
        }, "Jack").start();
    }
}
复制代码

返回结果如下所示:

Thread[Tom,5,main]: 10->101->10
Thread[Jack,5,main]: 更新结果:false, 更新后的值: 10
复制代码

参考文档

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改