ITPub博客

首页 > > > Java多线程之原子操作类

Java多线程之原子操作类

作者:flynike 时间:2019-09-18 14:14:54 0 删除 编辑

在并发编程中很容易出现并发安全问题,最简单的例子就是多线程更新变量i=1,多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。实际上,在J.U.C下的Atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新多种类型。Atomic包下的这些类都是采用乐观锁策略CAS来更新数据。

CAS原理与问题

CAS操作(又称为无锁操作)是一种乐观锁策略。它假设所有线程访问共享资源的时候不会出现冲突,因此不会阻塞其他线程的操作。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。

CAS的操作过程

举例说明:

Atomic包中的AtomicInteger类,是通过Unsafe类下的native函数compareAndSwapInt自旋来保证原子性,

其中incrementAndGet函数调用的getAndAddInt函数如下所示:

publicfinalintgetAndAddInt(Object var1,longvar2,intvar4){intvar5;do{ var5 =this.getIntVolatile(var1, var2); }while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));returnvar5; }

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

可见只有自旋实现更新数据操作之后,while循环才能够结束。

CAS的问题

自旋时间过长。由compareAndSwapInt函数可知,自旋时间过长会对性能是很大的消耗。

ABA问题。因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C,或使用AtomicStampedReference工具类。

Atomic包的使用

原子更新基本类型

Atomic包中原子更新基本类型的工具类:

AtomicBoolean:以原子更新的方式更新boolean;

AtomicInteger:以原子更新的方式更新Integer;

AtomicLong:以原子更新的方式更新Long;

这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法

addAndGet(int delta):以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果;

incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果;

getAndSet(int newValue):将实例中的值更新为新值,并返回旧值;

getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值;

原理不再赘述,参考上文compareAndSwapInt函数。

AtomicInteger使用示例:

publicclassAtomicExample{privatestaticAtomicInteger atomicInteger =newAtomicInteger(2);publicstaticvoidmain(String[] args){ System.out.println(atomicInteger.getAndIncrement()); System.out.println(atomicInteger.incrementAndGet()); System.out.println(atomicInteger.get()); }}// 2 4 4

LongAdder

为了解决自旋导致的性能问题,JDK8在Atomic包中推出了LongAdder类。LongAdder采用的方法是,共享热点数据分离的计数:将一个数字的值拆分为一个数组。不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多;要得到这个数字的话,就要把这个值加起来。相比AtomicLong,并发量大大提高。

优点:有很高性能的并发写的能力

缺点:读取的性能不是很高效,而且如果读取的时候出现并发写的话,结果可能不是正确的

原子更新数组类型

Atomic包中提供能原子更新数组中元素的工具类:

AtomicIntegerArray:原子更新整型数组中的元素;

AtomicLongArray:原子更新长整型数组中的元素;

AtomicReferenceArray:原子更新引用类型数组中的元素

这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法:

addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加;

getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1;

compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新

AtomicIntegerArray与AtomicInteger的方法基本一致,只不过在前者的方法中会多一个指定数组索引位i。

AtomicIntegerArray使用示例:

publicclassAtomicExample{privatestaticint[]value=newint[]{1,2,3};privatestaticAtomicIntegerArray integerArray =newAtomicIntegerArray(value);publicstaticvoidmain(String[] args){//对数组中索引为2的位置的元素加3intresult = integerArray.getAndAdd(2,3); System.out.println(integerArray.get(2)); System.out.println(result); }}// 6 3

原子更新引用类型

如果需要原子更新引用类型变量的话,为了保证线程安全,Atomic也提供了相关的类:

AtomicReference:原子更新引用类型;

AtomicReferenceFieldUpdater:原子更新引用类型里的字段;

AtomicMarkableReference:原子更新带有标记位的引用类型;

AtomicReference使用示例:

publicclassAtomicExample{privatestaticAtomicReference reference =newAtomicReference ();publicstaticvoidmain(String[] args){ User user1 =newUser("a",1); reference.set(user1); User user2 =newUser("b",2); User user = reference.getAndSet(user2); System.out.println(user); System.out.println(reference.get()); }staticclassUser{privateString userName;privateintage;publicUser(String userName,intage){this.userName = userName;this.age = age; } @OverridepublicStringtoString(){return"User{"+"userName='"+ userName +'\''+", age="+ age +'}'; } }}// User{userName='a', age=1}// User{userName='b', age=2}

AtomicReferenceFieldUpdater使用示例:

publicclassAtomicExample{publicstaticvoidmain(String[] args) { AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Dog.class, String.class,"name"); Dog dog1 =newDog(); updater.compareAndSet(dog1, dog1.name,"cat"); System.out.println(dog1.name); }}classDog{volatileString name ="dog1";}

原子更新字段类型

如果需要更新对象的某个字段,Atomic同样也提供了相应的原子操作类:

AtomicIntegeFieldUpdater:原子更新整型字段类;

AtomicLongFieldUpdater:原子更新长整型字段类;

要想使用原子更新字段需要两步操作:

原子更新字段类型类都是抽象类,只能通过静态方法newUpdater来创建一个更新器,并且需要设置想要更新的类和属性;

更新类的属性必须使用public volatile进行修饰;

AtomicIntegerFieldUpdater使用示例:

publicclassAtomicExample{privatestaticAtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");publicstaticvoidmain(String[] args){ User user =newUser("a",1); System.out.println(updater.getAndAdd(user,5)); System.out.println(updater.addAndGet(user,1)); System.out.println(updater.get(user)); }staticclassUser{privateString userName;publicvolatileintage;publicUser(String userName,intage){this.userName = userName;this.age = age; } @OverridepublicStringtoString(){return"User{"+"userName='"+ userName +'\''+", age="+ age +'}'; } }}

解决CAS的ABA问题

AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号,从而解决CAS的ABA问题

AtomicStampedReference使用示例:

publicclassAtomicExample{publicstaticvoidmain(String[] args){ Integer init1 =1110;// Integer init2 = 126;AtomicStampedReference reference =newAtomicStampedReference (init1,1);intcurent1 = reference.getReference();// Integer current2 = reference.getReference();reference.compareAndSet(reference.getReference(), reference.getReference() +1, reference.getStamp(), reference.getStamp() +1);//正确写法// reference.compareAndSet(current2, current2+1, reference.getStamp(), reference.getStamp() + 1);//正确写法// reference.compareAndSet(1110, 1111, reference.getStamp(), reference.getStamp() + 1);//错误写法// reference.compareAndSet(curent1, curent1+1, reference.getStamp(), reference.getStamp() + 1);//错误写法// reference.compareAndSet(current2, current2 + 1, reference.getStamp(), reference.getStamp() + 1);System.out.println("reference.getReference() = "+ reference.getReference()); }}

AtomicStampedReference踩过的坑

参考上面的代码,分享一个笔者遇到的一次坑。AtomicStampedReference的compareAndSet函数中,前两个参数是使用包装类的。所以当参数超过128时,而且传入参数并不是reference.getReference()获取的话,会导致expectedReference == current.reference为false,则无法进行更新。

publicbooleancompareAndSet(V expectedReference, V newReference,intexpectedStamp,intnewStamp){ Pair current = pair;returnexpectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/2618/viewspace-3164479/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2019-07-26

  • 博文量
    205
  • 访问量
    9820