关于Java并发自增计数的探讨

关于Java并发自增计数的探讨

前言
关于并发,一直都有所了解,但并没有真正认真深入的学习过。今天开始探索一下Java并发篇,玩一下多线程。了解了一下关于计数器的多线程下的四种情况。

第一种情况
编写一个类Counter里面有一个成员变量count,写一段简单的i++的代码完成计数的功能,为了暴露多线程下的问题,让每次自增之前睡100ms

开三个线程测试下会出现什么情况

根据代码逻辑,我们很清楚的知道count是一个共享变量,最终结果如果是理想情况不出错的情况下,开三个线程,count最终结果应该是600。然而理想情况一般是不存在的,三个线程避免不了打架,运行情况看下面两张图

关于Java并发自增计数的探讨

关于Java并发自增计数的探讨

根据这两张图很明显,中间出现了错误的情况,首先是515线程号12,14输出了两次,而且最终结果是547

这就影响了程序运行的正确性,这就是线程不安全的实例,并发情况出现了。当然了,这种情况解决很简单,应该每个了解过Java多线程的程序员都会知道,一个synchronized关键字就可以解决这样的问题。

synchronized小概念,内部锁,可重入锁。根据JVM计数器实现。
同一个类中相同或不同同步方法,在被调用的时候,调用者是同一个对象,可以重复进入方法体。

第二种情况,我将add()方法代码改为如下:

继续测试结果:

关于Java并发自增计数的探讨

关于Java并发自增计数的探讨

关于Java并发自增计数的探讨

如图可见,三个线程规规矩矩的依次执行完,那么这样避免了线程不安全的出现,但是就诞生了另外一个问题,大家想一下,这个是不是跟串行差不了太多了,三个线程依次执行,最终结果虽然没有出错,但是效率也近乎增加了三倍,总耗时应该是三次累计,比起刚才慢了三倍。其实没必要同步整个方法,只需要同步count++就可以了,count在多线程术语中被称为竞态条件。

第三种情况,我再修改下add()方法,这次锁count++,当然输出也要锁到,不然虽然count最终不会出错,但输出有可能会出错:

关于Java并发自增计数的探讨

看到输出结果,count又被三个线程交错增加,输出结果时间几乎同步,效率不会像刚才那种锁方法的低,而且保证了线程安全,用synchronized还是不要无脑锁方法,会影响效率的,但是如果锁实例的时候就要判断好竞态条件,保证加了锁之后最终是一致的,保证线程安全。

此外还有一种方式,第四种情况,利用AtomicInteger原子类对象自增:

测试结果仍然保证了线程安全。

关于Java并发自增计数的探讨

AtomicInteger是Java并发包下面的一个Integer原子操作类。通过命名也可以看得出来,它可以保证原子操作,那么,什么是原子操作?

如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。

原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。将整个操作视作一个整体是原子性的核心特征。

在这里我的理解,在这个程序里就是,稳定+1,没有其他操作可以妨碍我+1,在程序执行过程中,我这个+1不管是一个步骤还是多个步骤,该线程总要给我+1之后才会执行其他的部分。

还有一种老生常谈的说法是,原子操作(atomic operation)是不需要synchronized的。所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作是不可分割的,在执行完毕之前不会被任何其它任务或事件中断。

通过这些解释,可以说明AtomicInteger是可以保证线程安全的。那么再探索一下AtomicInteger的自增方法实现原理吧…

首先点进AtomicInteger的有参构造,注释的意思是:创建一个AtomicInteger(原子整数),给一个初始的value

看下AtomicInteger的成员属性

有一个Unsafe类型的成员变量,上面有一句注释,建立去用Unsafe对象去比较和转换Int为了更新,翻译的有点蹩脚,意思就是利用Unsafe对象去比较转换Int值的变化。其实就是负责与CAS(compare and swap)相关的操作实现。

valueOffset,从命名上来看是一个偏移量,实际上是用来记录内存首地址的偏移量。

value,这个是用来存放int值的。

静态块,是用来给偏移量初始化,在该原子类加载的时候利用字节码对象反射去获取该value在内存上存储的首地址偏移量,意思可以理解为就是该整型变量在内存上存储的位置的一个记录(标识)。

valatile关键字,在这里保证其对其他线程可见。

我调用那个自增方法是incrementAndGet,注释:每个当前value原子的增加1

点到Unsafe类的方法 getAndAddInt(this, valueOffset, 1) 实现

this.getIntVolatile(paramObject, paramLong);方法是利用内存偏移量获取到当前value的值,然后一直利用CAS比较转换,直到CAS比较转换成功。incrementAndGet对应++i,是返回自增之后的值。同样AtomicInteger类中也包含i++,i–,–i之类对应的方法。

说到CAS就会谈到ABA。是CAS引发了ABA问题,CAS也并非完美。就是说,当前内存的值一开始是A,被另外一个线程先改为B然后再改为A,那么当前线程访问的时候发现是A,则认为它没有被其他线程访问过。在某些场景下这样是存在错误风险的。比如在链表中。

ABA问题如何解决?

两种优化思路:

AtomicStampedReference 本质是有一个int 值作为版本号,每次更改前先取到这个int值的版本号,等到修改的时候,比较当前版本号与当前线程持有的版本号是否一致,如果一致,则进行修改,并将版本号+1(当然加多少或减多少都是可以自己定义的),在zookeeper中保持数据的一致性也是用的这种方式;

AtomicMarkableReference则是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false,修改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会降低ABA问题发生的几率而已;

具体实现见对应源码,本篇就不再解读这两个类的源码了。

0

发表评论

邮箱地址不会被公开。