理解Java轻量级并发包Atom系列工具类的设计

在Java的高级别并发工具包里面,有一系列由Atomic开头组成的工具类如下:

AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicIntegerFieldUpdater
AtomicLong
AtomicLongArray
AtomicLongFieldUpdater
AtomicMarkableReference
AtomicReference
AtomicReferenceArray
AtomicReferenceFieldUpdater
AtomicStampedReference
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder

他们的主要功能是提供轻量级的同步能力从而帮助我们避免内存一致性错误,从源码中观察这些工具类其设计主要利用了CAS原语+volatile的功能。我们知道volatile虽然是轻量级的同步工具,但由于其不保证单个变量更新原子性,所以一直不能大展身手,现在有了CAS提供的lock-free的原子性,两者一结合便造了Atomic开头的这些轻量级的工具类。

下面先从我们熟悉的并发问题累加开始:

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

上面的这个累加器,在多线程环境下是不安全的,如果加上了volatile关键字依然是不安全的,如果想让他们安全执行,那么简单的方法就是需要我们在每个方法前面加上synchronized关键字来建立线程互斥关系,从而完美解决线程安全问题,但是缺点是互斥会带来线程的上下文切换,从而影响性能,更完美的方法就是使用Atomic系列的轻量级并发工具类来解决:

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private AtomicInteger c = new AtomicInteger(0);

    public void increment() {
        c.incrementAndGet();
    }

    public void decrement() {
        c.decrementAndGet();
    }

    public int value() {
        return c.get();
    }

}

上面的代码没有任何同步操作,同时在多线程下执行也是安全的,并且效率比加锁同步更高,其中主要的原因是因为Atomic变量底层使用的是CAS原语,这个在上篇文章详细介绍过,这里就不再过多讲解CAS相关的内容了,大家记住使用CAS会通过操作系统的指令来保证原子性即可,所谓的原子性指的是同一个操作,要么成功,要么失败,不存在其他的状态,硬件系统底层会通过总线锁定或者CPU缓存锁定来建立内存屏障从而保证原子性,在Java里面所有关于CAS操作的类是在一个叫Unsafe的类,这个类大部分方法都是native修饰的,也就是说它调用的是底层系统的方法,此外这个类一般不建议大家直接使用从名字就能看出来它在强调不安全。

前面说过这个包的实现主要是基于CAS+volatile来完成的,volatile的语义在于内存可见性和禁止指令重排序,而CAS(compare and swap)的作用在于保持原子性,在这些类工具类里面的核心方法在于:

boolean compareAndSet(expectedValue, updateValue);

在上篇文章中提到过CAS原理是先读取原数据v,在更新的时候在读取一次原数据x,如果v和x相等(这里相等指的是内存引用),那么就意味着没有数据冲突,就会把y更新到最新的数据里面,如果不相等,那么会再循环几个周期直到写入成功。

上面方法的第一个参数,就是我们说的x,要updateValue就是我们说的y,同构这样一种无锁方式,来保证了原子性,再辅以volatile保证了可见性,所以在多线程环境下是非常轻量级的同步操作。

其中AtomicBoolean, AtomicInteger, AtomicLong, 和 AtomicReference其中了单个变量的操作的原子性。其中:

get和set方法分别和volatile的读写具有一样的语义,这个很容易理解,在源码里面核心的数据结构其实都是volatile修饰的。

接着AtomicReferenceFieldUpdater, AtomicIntegerFieldUpdater和AtomicLongFieldUpdater 基于反射能力可以在指定的类里面指定的volatile字段上做更新,这里有种场景是链表的数据结构里面有Node节点,每个Node都会指向下一个Node节点,如果想要保证轻量级同步,就意味每个节点都要使用Atomic来使用,而这三个类可以通过反射来灵活的操作,当然这是有代价的,在启动介绍的性能会消耗较多。

然后AtomicIntegerArray, AtomicLongArray, and AtomicReferenceArray 可以用来保证数组元素和数组引用的更新原子性,这里需要的注意的是仅仅元素的原子性,并不能保证对整个数组操作的原子性,所以我们反复再强调Atomic工具类的原子性是在单个变量的操作上。

接着AtomicMarkableReference 和 AtomicStampedReference前者可以用来使用boolean值标记一个引用的逻辑删除,后者可以用来通过版本号解决CAS的ABA问题。

最后DoubleAdder和LongAdder是JDK8中用来解决线程竞争激烈时候的累加器,性能比AtomicInteger和AtomicLong要高,但如果竞争不激烈那么两者的性能相似,底层原理使用的是分段更新功能,从而增加了更好的吞吐量。

总结:

(1)Atomic系列的工具类,在大多数时候可以提供无锁同步,但依赖于硬件平台,并不能严格保证总是没有阻塞的。

(2)Atomic类设计主要是构建阻塞但实现非阻塞的一种数据结构,这种实现并不能完全替代锁同步,它仅仅用于当临界区更新的是单个变量的情况下。

(3)Atomic类也不是为了替代java.lang.Integer相关的类,他们没有定义equals和hashCode等方法,因为原子变量是可以变化的,所以他们很少用来做Hashtable的key。

(4)还有一些基础类如Byte,Float,Double类型的Atomic变量这里没有不过这些都可以通过变相的方法获得,如果是byte可以直接用int接受。 Float.floatToRawIntBits(float) Double.doubleToRawLongBits(double)

(5)Atomic变量本身最好是final修饰,否则就需要volatile修饰其本身,此外AtomicReference里面对象的成员变量,最好也就是final类型的,这样就能避免在赋值以后,在多线程的环境下再次修改引用。

(6)Atomic仅仅保证单个变量的原子性,如果我们用其来存实体类,那么在CAS的时候一定是替换引用而不是替换引用的属性,如果是多线程改变属性,那么 改对象的状态就会不一致。这也是这里强调为什么实体类所有的属性最好声明final的原因。

原文发布于微信公众号 - 我是攻城师(woshigcs)

原文发表时间:2018-07-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏进击的程序猿

The Clean Architecture in PHP 读书笔记(五)The Clean Architecture in PHP 读书笔记(五)

上篇最重要的是介绍了去耦的工具之一依赖注入,本篇将继续介绍去耦工具:接口和适配器,本文是The Clean Architecture in PHP的第5篇。

662
来自专栏Java呓语

设计模式开篇

设计模式这玩意平日里让我思绪万千,可真要提起笔来却顾头顾尾、不得要义。于是乎就有了书写设计模式系列的想法,一来是彻头彻尾的归纳总结一遍,二来也希望尽自己力量生产...

1172
来自专栏Android机器圈

Java设计模式总汇二(小白也要飞)

PS:上一篇我介绍了适配器设计模式、单例设计模式、静态代理设计模式、简单工厂设计模式,如果没有看过第一篇的小火鸡可以点这个看看http://www.cnblog...

3439
来自专栏JavaEdge

Java8 原子弹类之LongAdder源码分析add使用场景 LongAdder是否能够替换AtomicLong

4656
来自专栏博客园

Asp.Net Web API(四)

    如果Web API控制器抛出一个未捕捉的异常,会发生什么呢?在默认情况下,大多数异常都会转换为一个带有状态码500的内部服务器错误的HTTP响应。

1732
来自专栏MasiMaro 的技术博文

Vista 及后续版本的新线程池

在上一篇的博文中,说了下老版本的线程池,在Vista之后,微软重新设计了一套线程池机制,并引入一组新的线程池API,新版线程池相对于老版本的来说,它的可控性更高...

1503
来自专栏逸鹏说道

C#进阶系列——WebApi 接口参数不再困惑:传参详解 下

(1)基础类型数组 var arr = ["1", "2", "3", "4"]; $.ajax({ type: "post", ...

2966
来自专栏武培轩的专栏

迅雷面经汇总

实现多态的技术称为 :动态绑定,是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

1261
来自专栏老付的网络博客

爬取菜鸟裹裹的数据

菜鸟裹裹是阿里旗下的一个物流数据的整合平台,数据准确、及时.前几天在关注菜鸟和顺丰的争端,因为在前一天我刚刚爬到菜鸟上面的快递数据,第二天看到二者出现了摩擦,在...

2422
来自专栏JavaEdge

探究CAS原理(基于JAVA8源码分析)define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "define LOCK_IF_MP(mp) _

3496

扫码关注云+社区

领取腾讯云代金券