解密JUC——非阻塞同步指令CAS

一、为什么使用CAS?CAS是啥?

在多线程中,为了保证一系列的操作具有原子性,独占锁是比较简单实用的同步机制,但它是一项悲观技术,对系统性能有严重的损耗,因为它假设了最坏的情况:如果你不锁门,那么捣蛋鬼就会闯入并搞得一团糟。

对于细粒度的操作,还有另外一种更加高效的办法,可以在不发生干扰的情况下完成更新操作。这种方法需要借助冲突检查机制来判断在更新过程中是否存在来自其它线程的干扰,如果存在,这个操作将会失败,并且可以选择是否重试。这种方法有个好听的名字——乐观锁

现在很多计算机针对多处理器提供了一些特殊指令,用于管理对共享数据的并发访问。采用比较多的方法是实现一个比较并交换的指令,即Conmpare And Swap,简称CAS。

CAS包含了3个操作数——需要读写的内存位置V、进行比较的值A和拟写入的新值B。当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。

二、Java中CAS的实现 

Java对CAS的支持体现在Unsafe类上,里面基本都是本地方法 ,说明它是调用底层操作系统指令实现的。看它的名字:sun.misc.Unsafe,没有源码也没有注释,它不属于JDK的标准,也不推荐用户去搞。

Unsafe类使用了单例模式,但是它的getUnsafe方法有个判断,调用类必须是由BootstrapClassLoader加载的才行。

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

如果我们代码里面直接调用getUnsafe的话是会报异常的。

三、Unsafe类的用法 

虽然我们不能直接调用getUnsafe方法,但是我们可以通过反射暴力访问获取Unsafe实例:可以选择读取字段的值或者构造器创建对象。

        // 方式一
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Assert.assertNotNull(field.get(null));
        // 方式二
        Constructor<?> declaredConstructor = Unsafe.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Assert.assertNotNull(declaredConstructor.newInstance());

Unsafe类对应CAS的方法为compareAndSwapXxx,其中Xxx表示内存中这个值的类型,它们的返回值是表示成功与否的布尔类型。这些方法一共有4个参数,第一个是要修改的对象,第二个是对象指定字段的偏移量,第三个是期望值,第四个是目标值。

下面示范一下使用这个类修改一个对象的指定属性:定义一个只有name属性的Person类并创建它的实例,分别传入对的和错的期望值参数试图去修改它的name属性,结果可以看到,第一次成功了,第二次失败了。

    public void testModifyObjAttr() throws NoSuchFieldException, IllegalAccessException {
        // 获取实例
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe)field.get(Unsafe.class);

        // 获得Person类的name属性的偏移量
        long fieldOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("name"));

        // 修改值:传入正确期望值
        Person person = new Person();
        boolean result = unsafe.compareAndSwapObject(person, fieldOffset, null, "huang");
        Assert.assertEquals("huang", person.name);

        // 修改值:传入错误期望值
        Assert.assertFalse(unsafe.compareAndSwapObject(person, fieldOffset, "zhou", "hua"));
    }

    private static class Person {
        private String name;
    }

四、JUC中用到CAS的地方

Java并发工具包里面的原子类和AQS都用到了Unsafe。

比如AtomicInteger类里面类似i++的原子操作

 

比如AQS里面state值的修改

 

五、CAS的弊端

1、ABA问题

举个栗子:线程1从内存X中取出A,这时候另一个线程2也从内存X中取出A,然后线程2进行了一些操作将内存X中的值变成了B,紧接着线程2又将内存X中的数据变回了A,这时候线程1进行CAS操作发现内存X中仍然是A,所以线程1操作成功。虽然线程1的CAS操作成功,但是整个过程就是有问题的,因为这个过程内存X中的值是变化过的。比如链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。

解决办法:数据增加一个标记,比如版本号之类的,在CAS的时候通过额外对这个标识校验来判断数据是否有过变更。

Java中提供了AtomicStampedReference类和AtomicMarkableReference类来处理会发生ABA问题的场景。

2、不适合高并发的场景

比如很多线程同时要将一个对象的属性由A改成B,那么CAS操作最终只有一个可以成功,但是太多失败的冲突检查带来了不必要的性能损耗,倒不如用独占锁来得简单粗暴。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值