前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解Java轻量级并发包Atom系列工具类的设计

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

作者头像
我是攻城师
发布2018-08-03 10:58:30
6360
发布2018-08-03 10:58:30
举报
文章被收录于专栏:我是攻城师

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

代码语言:javascript
复制
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicIntegerFieldUpdater
AtomicLong
AtomicLongArray
AtomicLongFieldUpdater
AtomicMarkableReference
AtomicReference
AtomicReferenceArray
AtomicReferenceFieldUpdater
AtomicStampedReference
DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder

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

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

代码语言:javascript
复制
class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

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

代码语言:javascript
复制
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)的作用在于保持原子性,在这些类工具类里面的核心方法在于:

代码语言:javascript
复制
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的原因。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-07-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 我是攻城师 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总结:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档