Java——原子变量和CAS

Java——原子变量和CAS 原子变量和CAS1、AtomicInteger1.1、基本用法1.2、基本原理和思维1.3、实现锁2、ABA问题什么是原子变量为什么需要它们呢我们从synchronized说起。介绍过Counter类使用synchronized关键字保证原子更新操作代码如下publicclassCounter{privateintcount;publicsynchronizedvoidincr(){count;}publicsynchronizedintgetCount(){returncount;}}对于count这种操作来说使用synchronized成本太高了需要先获取锁最后需要释放锁获取不到锁的情况下需要等待还会有线程的上下文切换这些都需要成本。对于这种情况完全可以使用原子变量代替Java并发包中的基本原子变量类型有以下几种。AtomicBoolean原子Boolean类型常用来在程序中表示一个标志位。AtomicInteger原子Integer类型。AtomicLong原子Long类型常用来在程序中生成唯一序列号。AtomicReference原子引用类型用来以原子方式更新复杂类型。限于篇幅主要介绍AtomicInteger。除了这4个类还有一些其他类如针对数组类型的类AtomicLongArray、AtomicReferenceArray以及用于以原子方式更新对象中的字段的类如AtomicIntegerFieldUpdater、AtomicReferenceFieldUpdater等。Java 8增加了几个类在高并发统计汇总的场景中更为适合包括LongAdder、LongAccumulator、Double-Adder和DoubleAccumulator具体可参见API文档。1、AtomicInteger1.1、基本用法AtomicInteger有两个构造方法publicAtomicInteger(intinitialValue)publicAtomicInteger()第一个构造方法给定了一个初始值第二个构造方法的初始值为0。可以直接获取或设置AtomicInteger中的值方法是publicfinalintget()publicfinalvoidset(intnewValue)之所以称为原子变量是因为它包含一些以原子方式实现组合操作的方法部分方法如下//以原子方式获取旧值并设置新值publicfinalintgetAndSet(intnewValue)//以原子方式获取旧值并给当前值加1publicfinalintgetAndIncrement()//以原子方式获取旧值并给当前值减1publicfinalintgetAndDecrement()//以原子方式获取旧值并给当前值加deltapublicfinalintgetAndAdd(intdelta)//以原子方式给当前值加1并获取新值publicfinalintincrementAndGet()//以原子方式给当前值减1并获取新值publicfinalintdecrementAndGet()//以原子方式给当前值加delta并获取新值publicfinalintaddAndGet(intdelta)这些方法的实现都依赖另一个public方法publicfinalbooleancompareAndSet(intexpect,intupdate)compareAndSet是一个非常重要的方法比较并设置我们以后将简称为CAS。该方法有两个参数expect和update以原子方式实现了如下功能如果当前值等于expect则更新为update否则不更新如果更新成功返回true否则返回false。AtomicInteger可以在程序中用作一个计数器多个线程并发更新也总能实现正确性。我们看个例子如代码所示。publicclassAtomicIntegerDemo{privatestaticAtomicIntegercounternewAtomicInteger(0);staticclassVisitorextendsThread{Overridepublicvoidrun(){for(inti0;i1000;i){counter.incrementAndGet();}}}publicstaticvoidmain(String[]args)throwsInterruptedException{intnum1000;Thread[]threadsnewThread[num];for(inti0;inum;i){threads[i]newVisitor();threads[i].start();}for(inti0;inum;i){threads[i].join();}System.out.println(counter.get());}}程序的输出总是正确的为1000000。1.2、基本原理和思维AtomicInteger的使用方法是简单直接的它是怎么实现的呢它的主要内部成员是privatevolatileintvalue;注意它的声明带有volatile这是必需的以保证内存可见性。它的大部分更新方法实现都类似我们看一个方法incrementAndGet其代码为publicfinalintincrementAndGet(){for(;;){intcurrentget();intnextcurrent1;if(compareAndSet(current,next))returnnext;}}代码主体是个死循环先获取当前值current计算期望的值next然后调用CAS方法进行更新如果更新没有成功说明value被别的线程改了则再去取最新值并尝试更新直到成功为止。与synchronized锁相比这种原子更新方式代表一种不同的思维方式。synchronized是悲观的它假定更新很可能冲突所以先获取锁得到锁后才更新。原子变量的更新逻辑是乐观的它假定冲突比较少但使用CAS更新也就是进行冲突检测如果确实冲突了那也没关系继续尝试就好了。synchronized代表一种阻塞式算法得不到锁的时候进入锁等待队列等待其他线程唤醒有上下文切换开销。原子变量的更新逻辑是非阻塞式的更新冲突的时候它就重试不会阻塞不会有上下文切换开销。对于大部分比较简单的操作无论是在低并发还是高并发情况下这种乐观非阻塞方式的性能都远高于悲观阻塞式方式。原子变量相对比较简单但对于复杂一些的数据结构和算法非阻塞方式往往难于实现和理解幸运的是Java并发包中已经提供了一些非阻塞容器我们只需要会使用就可以了比如ConcurrentLinkedQueue和ConcurrentLinkedDeque非阻塞并发队列。ConcurrentSkipListMapConcurrentSkipListSet非阻塞并发Map和Set。但compareAndSet是怎么实现的呢我们看代码publicfinalbooleancompareAndSet(intexpect,intupdate){returnunsafe.compareAndSwapInt(this,valueOffset,expect,update);}它调用了unsafe的compareAndSwapInt方法unsafe是什么呢它的类型为sun.misc. Unsafe定义为privatestaticfinalUnsafeunsafeUnsafe.getUnsafe();它是Sun的私有实现从名字看表示的也是“不安全”​一般应用程序不应该直接使用。原理上一般的计算机系统都在硬件层次上直接支持CAS指令而Java的实现都会利用这些特殊指令。从程序的角度看可以将compareAndSet视为计算机的基本操作直接接纳就好。1.3、实现锁基于CAS除了可以实现乐观非阻塞算法之外还可以实现悲观阻塞式算法比如锁。实际上Java并发包中的所有阻塞式工具、容器、算法也都是基于CAS的不过也需要一些别的支持​。怎么实现锁呢我们演示一个简单的例子用AtomicInteger实现一个锁MyLock如代码所示。publicclassMyLock{privateAtomicIntegerstatusnewAtomicInteger(0);publicvoidlock(){while(!status.compareAndSet(0,1)){Thread.yield();}}publicvoidunlock(){status.compareAndSet(1,0);}}在MyLock中使用status表示锁的状态0表示未锁定1表示锁定lock()、unlock()使用CAS方法更新lock()只有在更新成功后才退出实现了阻塞的效果不过一般而言这种阻塞方式过于消耗CPU我们后续章节介绍更为高效的方式。MyLock只是用于演示基本概念实际开发中应该使用Java并发包中的类如ReentrantLock。2、ABA问题使用CAS方式更新有一个ABA问题。该问题是指假设当前值为A如果另一个线程先将A修改成B再修改回成A当前线程的CAS操作无法分辨当前值发生过变化。ABA是不是一个问题与程序的逻辑有关一般不是问题。而如果确实有问题解决方法是使用AtomicStampedReference在修改值的同时附加一个时间戳只有值和时间戳都相同才进行修改其CAS方法声明为publicbooleancompareAndSet(VexpectedReference,VnewReference,intexpectedStamp,intnewStamp)比如PairpairnewPair(100,200);intstamp1;AtomicStampedReferencePairpairRefnewAtomicStampedReferencePair(pair,stamp);intnewStamp2;pairRef.compareAndSet(pair,newPair(200,200),stamp,newStamp);AtomicStampedReference在compareAndSet中要同时修改两个值一个是引用另一个是时间戳。它怎么实现原子性呢实际上内部AtomicStampedReference会将两个值组合为一个对象修改的是一个值我们看代码publicbooleancompareAndSet(VexpectedReference,VnewReference,intexpectedStamp,intnewStamp){PairVcurrentpair;returnexpectedReferencecurrent.referenceexpectedStampcurrent.stamp((newReferencecurrent.referencenewStampcurrent.stamp)||casPair(current,Pair.of(newReference,newStamp)));}这个Pair是AtomicStampedReference的一个内部类成员包括引用和时间戳具体定义为privatestaticclassPairT{finalTreference;finalintstamp;privatePair(Treference,intstamp){this.referencereference;this.stampstamp;}staticTPairTof(Treference,intstamp){returnnewPairT(reference,stamp);}}AtomicStampedReference将对引用值和时间戳的组合比较和修改转换为了对这个内部类Pair单个值的比较和修改。