J.U.C高并发之原子类

  • 内容
  • 评论
  • 相关

 

J.U.C并发包,即java.util.concurrent包,是JDK的核心工具包,是JDK1.5之后,由 Doug Lea实现并引入。

整个java.util.concurrent包,按照功能可以大致划分如下:

  1. juc-locks 锁框架
  2. juc-atomic 原子类框架
  3. juc-sync 同步器框架
  4. juc-collections 集合框架
  5. juc-executors 执行器框架

 

今天来给大家分享原子类的源码解析,有不对的地方请指点。

 

(gitee源码:https://gitee.com/java-web/concurrent-demo

 

 

 

对于原子包java.unit.concurrent.atomic下有许多的原子类,分别有:

AtomicBoolean:其实里面就是有一个int类型的value,只有0和1,类比AtomicInteger。

AtomicInteger:对一个数值类型的变量进行原子操作。

AtomicIntegerArray:对一个数值数组类型的变量进行源自操作,类比AtomicInteger。

AtomicIntegerFieldUpdater:对一个对象有Integer类型的原子操作。

对于使用AtomicIntegerFieldUpdater的对象有如下几个约束:

(1)字段必须是volatile类型

(2)字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。

(3)只能是实例变量,不能是类变量,也就是说不能加static关键字。

(4)只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。

(5)对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。

AtomicLong:类比AtomicInteger。

AtomicLongArray:类比AtomicIntegerArray。

AtomicLongFieldUpdater:类比AtomicIntegerFieldUpdater。

AtomicMarkableReference:用来解决ABA问题,内部原理是一个boolean值,不能从根本上解决问题,准确的说,应该是降低ABA发生的概率,并不能阻止ABA问题的发生。

AtomicReference:使用对象作为原子操作的对象。

AtomicReferenceArray:使用对象数组作为原子操作的对象。

AtomicReferenceFieldUpdater:对一个对象包含另一个对象使用原子操作。

AtomicStampedReference:使用版本号从根本上解决ABA问题的原子操作类。

DoubleAccumulator:Double类型的算数原子操作类。

DoubleAdder:Double类型的算数原子操作类。

LongAccumulator:Long类型的算数原子操作类。

LongAdder:Long类型的算数原子操作类。

Striped64:对于算数原子操作类的支持类。

 

接下来说一个典型的AtomicInteger对象的具体使用以及注意事项,其他的原子操作类基本上可以以此为基础进行类比。

 

一、创造AtomicInteger对象

AtomicInteger存在于java.util.concurrent.atomic包下,AtomicInteger继承Number类并且实现java.io.Serializable接口,代码如下:

 

AtomicInteger提供了两个构造器,使用默认构造器时,内部int类型的value值为0。AtomicInteger类的内部并不复杂,所有的操作都针对内部的int值——value,并通过Unsafe类来实现线程安全的CAS操作,具体相关操作请继续往下看。

 

二、AtomicInteger的使用

 

对于AtomicInteger,我们为什么要使用它?它能给我们带来什么好处呢?

在实际的操作过程中,我们经常可能会遇到多线程对同一资源的竞争,导致数据最终不是一致的,为了解决这一问题,Java提供了许多的并发操作,而并发包下的原子操作类就是其中一种。

它使用的原理是CAS,即Compare And Swap,即比较并交换。你可以理解为修改密码,当你输入的old password正确后才允许你将新的password更新到你的信息中。

 

我们先来看一下不使用原子操作类的时候对同一资源的竞争:

结果可能是小于10000的,接下来再来看一下使用原子操作类的时候对同一资源的竞争:

结果是我们想要的。在上面的方法中,我们使用了incrementAndGet()方法,它是实现一个自增的操作。接下来看看AtomicInteger还有什么其他方法吧。

 

AtomicInteger的其他方法:

方法名称 说明
AtomicInteger(int)  有参构造
AtomicInteger()  无参构造
get():int  get
set(int):void  set
lazySet(int):void  不保证修改后其他线程可见的set
getAndSet(int):int  先获取value后set
compareAndSet(int, int):boolean  比较后set
weakCompareAndSet(int, int):boolean  比较后set,在1.8之前与compareAndSet一摸一样;在JDK1.9中稍有些不同,多了一个注解@HotSpotIntrinsicCandidate。

1、底层调用的native方法的实现中,cmpxchgb指令前都会有“lock”前缀了(在JDK 8中,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。只有在CPU是多处理器(multi processors)的时候,会添加一个lock前缀)。
2、同时多了一个@HotSpotIntrinsicCandidate注解,该注解是特定于Java虚拟机的注解。通过该注解表示的方法可能( 但不保证 )通过HotSpot VM自己来写汇编或IR编译器来实现该方法以提供性能。 它表示注释的方法可能(但不能保证)由HotSpot虚拟机内在化。如果HotSpot VM用手写汇编和/或手写编译器IR(编译器本身)替换注释的方法以提高性能,则方法是内在的。 也就是说虽然外面看到的在JDK9中weakCompareAndSet和compareAndSet底层依旧是调用了一样的代码,但是不排除HotSpot VM会手动来实现weakCompareAndSet真正含义的功能的可能性。
getAndIncrement():int  先获取value后自增,相当于value++
 getAndDecrement():int  先获取value后自减,相当于value--
 getAndAdd(int):int  先获取value后增加
 incrementAndGet():int  自增后,获取最新的value
decrementAndGet():int  自减后,获取最新的value
andAndGet(int):int  先增加后获取最新的value
getAndUpdate(IntUnaryOperator):int  先获取value后更新操作
updateAndGet(IntUnaryOperator):int  先更新操作后获取最新的value
getAndAccumulate(int,IntUnaryOperator):int  先获取value后更新操作
 accumulateAndGet(int,IntUnaryOperator):int  先更新操作后获取最新的value
 toString():String  toString输出
 initValue(int):Number  init value
 longValue():long  cast to long
 floatValue():float  cast to float
doubleValue():double  cast to double

 

 

三、AtomicInteger的特殊方法lazySet

 

我们先来对比看一下set方法和lazySet方法:

lazySet方法是set方法的不可见版本。什么意思呢?

我们知道,使用volatile修饰的变量具有可见性和有序性。可见性就是在修改后强制刷新主存,其他线程立马可见;有序性便是禁止指令重排序,保证最终一致性。

lazySet内部调用了Unsafe类的putOrderedInt方法,通过该方法对共享变量值的改变,不一定能被其他线程立即看到。也就是说以普通变量的操作方式来写变量。

这么做有什么意义呢?假设我们现在有这样一个场景:

由于有Lock的存在,我们已经保证了线程安全问题,我们是不是就没有必要再做额外的工作了?当然,这个前提是,我们需要保证变量必须存在锁里执行,在准确点应该说同一个锁里执行,否则,依然会出现线程安全。

所以,如果我们已经用外部的某些行为(如Lock、如synchronized等)保证了线程安全,我们就没必要再去消耗额外的资源,从而提高代码效率。

 

四、ABA问题

首先,需要解释一下的是:什么是ABA问题?现在我们已经使用原子操作类能够保证线程安全问题,它的原理是基于CAS,即比较并交换。在前面介绍的比较潦草,一笔带过,在这里重新复述一下什么是CAS,CAS是一个方法,compareAndSet,它有三个参数,分别为V,E,N。V代表变量,也可以理解为内存地址,E代表预期值,可以理解为oldValue,N代表需要修改的新值,可以理解为newValue。当V==E时,才会将V改变成N,否则,将自旋重试。

 

那么我们回到本次的ABA问题,什么是ABA问题?

现在我们有这么一个场景:现在有一个共享变量value = 0,线程1读取value,线程2读取value,线程1将value改成10(需要确认此时value==0),线程1读取value又将它改成了0(需要确认此时value == 10),线程2继续执行,将value改成20(需要确认value == 0,而此时value已经经过了 0 -> 10 -> 0 的改造),此时线程2是可以正常执行的,但是value已经发生了改变,这便是ABA问题。此时我们可以用AtomicStampedReference来解决该问题,对于AtomicStampedReference,请参考《还没想好题目》。

 

五、CAS的优缺点

优点:CAS由于是在硬件层面保证的原子性,不会锁住当前线程,它的效率是很高的。

缺点:

1、可能会导致ABA问题;

2、并发越高,失败越频繁,自旋频率增高,增大CPU的开销,效率越低,因此CAS不适合竞争十分频繁的场景;

3、只能保证一个共享变量的原子操作。当对多个共享变量操作时,CAS就无法保证操作的原子性,这时就可以用锁,或者把多个共享变量合并成一个共享变量来操作。

 

 

总结

原子操作类是什么?

原子操作类能做什么?

在没有它出现的时候,我们遇到了什么问题,它的出现能给我们带来什么好处?也就是说,我们为什么要学习它?

我们该如何使用它?

我们在使用它时需要注意什么事项?

......

 

诸如上面的问题,心中是否有了答案呢?

 

(你的关注,是我的荣幸)

 

 

为了未来好一点,现在苦一点有什么。

喜欢 2

评论

2条评论
  1. web-dl 回复

    Very good post! We are linking to this great post on our site. Keep up the good writing. Davida Hermon Rickart

  2. hd film izle 回复

    Great post! We will be linking to this particularly great article on our website. Keep up the great writing. Liane Lutero Stoeber

发表评论

邮箱地址不会被公开。 必填项已用*标注

Title - Artist
0:00