如何在java中实现小数点自增_关于Java并发自增计数的探讨

前言

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

第一种情况

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

public class Counter {

private int count;

private static Map systemTimeMillis = new HashMap();

public void add(){

systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());

try {

for(int i=0;i<200;i++){

Thread.sleep(100);

this.count++;

System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count);

}

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));

}

public Map getSystemTimeMillis() {

return systemTimeMillis;

}

public void setSystemTimeMillis(Map systemTimeMillis) {

this.systemTimeMillis = systemTimeMillis;

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

publicclassCounter{

privateintcount;

privatestaticMapsystemTimeMillis=newHashMap();

publicvoidadd(){

systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());

try{

for(inti=0;i<200;i++){

Thread.sleep(100);

this.count++;

System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count);

}

}catch(InterruptedExceptione){

e.printStackTrace();

}

System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));

}

publicMapgetSystemTimeMillis(){

returnsystemTimeMillis;

}

publicvoidsetSystemTimeMillis(MapsystemTimeMillis){

this.systemTimeMillis=systemTimeMillis;

}

}

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

public static void main(String[] args) {

final Counter counter = new Counter();

new Thread(new Runnable() {

@Override

public void run() {

counter.add();

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

counter.add();

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

counter.add();

}

}).start();

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

publicstaticvoidmain(String[]args){

finalCountercounter=newCounter();

newThread(newRunnable(){

@Override

publicvoidrun(){

counter.add();

}

}).start();

newThread(newRunnable(){

@Override

publicvoidrun(){

counter.add();

}

}).start();

newThread(newRunnable(){

@Override

publicvoidrun(){

counter.add();

}

}).start();

}

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

fef25065c7b3859a12a3b4670515f444.png

5dd88c22fe5c3b60487cd1782308a4c1.png

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

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

synchronized小概念,内部锁,可重入锁。根据JVM计数器实现。

同一个类中相同或不同同步方法,在被调用的时候,调用者是同一个对象,可以重复进入方法体。

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

public synchronized void add(){

systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());

try {

for(int i=0;i<200;i++){

Thread.sleep(100);

this.count++;

System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count);

}

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

publicsynchronizedvoidadd(){

systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());

try{

for(inti=0;i<200;i++){

Thread.sleep(100);

this.count++;

System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count);

}

}catch(InterruptedExceptione){

e.printStackTrace();

}

System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));

}

继续测试结果:

1f5662ceafc6e8d2a6137ae621dd04ff.png

efff8b014ea1bbf8551a3d2c151503db.png

f1c78ef806d38c8d815c2df0189838f6.png

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

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

public void add(){

systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());

try {

for(int i=0;i<200;i++){

Thread.sleep(100);

synchronized (this){

this.count++;

System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count);

}

}

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

publicvoidadd(){

systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());

try{

for(inti=0;i<200;i++){

Thread.sleep(100);

synchronized(this){

this.count++;

System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count);

}

}

}catch(InterruptedExceptione){

e.printStackTrace();

}

System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));

}

89a0a1842cb72e2e5a5b38a9631baf62.png

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

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

private AtomicInteger count = new AtomicInteger(0);

1

2

privateAtomicIntegercount=newAtomicInteger(0);

public void add(){

systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());

try {

for(int i=0;i<200;i++){

Thread.sleep(100);

synchronized (this){

System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count.incrementAndGet());

}

}

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

publicvoidadd(){

systemTimeMillis.put(Thread.currentThread().getId(),System.currentTimeMillis());

try{

for(inti=0;i<200;i++){

Thread.sleep(100);

synchronized(this){

System.out.println("ThreadId:"+Thread.currentThread().getId()+":"+count.incrementAndGet());

}

}

}catch(InterruptedExceptione){

e.printStackTrace();

}

System.out.println("ThreadId:"+Thread.currentThread().getId()+",耗时:"+(System.currentTimeMillis()-(Long)systemTimeMillis.get(Thread.currentThread().getId())));

}

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

fdbe5e88ce7ff679f7cc01d1cf440f27.png

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

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

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

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

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

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

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

/**

* Creates a new AtomicInteger with the given initial value.

*

* @param initialValue the initial value

*/

public AtomicInteger(int initialValue) {

value = initialValue;

}

1

2

3

4

5

6

7

8

9

/**

* Creates a new AtomicInteger with the given initial value.

*

* @param initialValue the initial value

*/

publicAtomicInteger(intinitialValue){

value=initialValue;

}

看下AtomicInteger的成员属性

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

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

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

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

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

// setup to use Unsafe.compareAndSwapInt for updates

private static final Unsafe unsafe = Unsafe.getUnsafe();

private static final long valueOffset;

static {

try {

valueOffset = unsafe.objectFieldOffset

(AtomicInteger.class.getDeclaredField("value"));

} catch (Exception ex) { throw new Error(ex); }

}

private volatile int value;

1

2

3

4

5

6

7

8

9

10

11

12

13

// setup to use Unsafe.compareAndSwapInt for updates

privatestaticfinalUnsafeunsafe=Unsafe.getUnsafe();

privatestaticfinallongvalueOffset;

static{

try{

valueOffset=unsafe.objectFieldOffset

(AtomicInteger.class.getDeclaredField("value"));

}catch(Exceptionex){thrownewError(ex);}

}

privatevolatileintvalue;

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

/**

* Atomically increments by one the current value.

*

* @return the updated value

*/

public final int incrementAndGet() {

return unsafe.getAndAddInt(this, valueOffset, 1) + 1;

}

1

2

3

4

5

6

7

8

9

/**

* Atomically increments by one the current value.

*

* @return the updated value

*/

publicfinalintincrementAndGet(){

returnunsafe.getAndAddInt(this,valueOffset,1)+1;

}

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

public final int getAndAddInt(Object paramObject, long paramLong, int paramInt) {

int i;

do {

i = this.getIntVolatile(paramObject, paramLong);

} while(!this.compareAndSwapInt(paramObject, paramLong, i, i + paramInt));

return i;

}

1

2

3

4

5

6

7

8

9

publicfinalintgetAndAddInt(ObjectparamObject,longparamLong,intparamInt){

inti;

do{

i=this.getIntVolatile(paramObject,paramLong);

}while(!this.compareAndSwapInt(paramObject,paramLong,i,i+paramInt));

returni;

}

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问题发生的几率而已;

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

浏览量:

50

0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值