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就无法保证操作的原子性,这时就可以用锁,或者把多个共享变量合并成一个共享变量来操作。

 

 

总结

原子操作类是什么?

原子操作类能做什么?

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

我们该如何使用它?

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

......

 

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

 

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

 

 

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

喜欢 1

评论

0条评论

发表评论

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

Title - Artist
0:00