流行的原子

<wbr style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">流行的原子</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">问题:线程之间的协调</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"> 如果线程之间不需要协调,那么几乎没有任务可以真正地并行。以线程池为例,其中执行的任务通常相互独立。<br style="line-height:25px"> 如果线程池利用公共工作队列,则从工作队列中删除元素或向工作队列添加元素的过程必须是线程安全的,<br style="line-height:25px"> 并且这意味着要协调对头、尾或节点间链接指针所进行的访问。正是这种协调导致了所有问题。<br style="line-height:25px"><br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">标准方法:锁定<br style="line-height:25px"></wbr></span><wbr style="line-height:25px">在Java语言中,协调对共享字段的访问的传统方法是使用同步,确保完成对共享字段的所有访问,<br style="line-height:25px"> 同时具有适当的锁定。通过同步,可以确定(假设类编写正确)具有保护一组给定变量的锁定的所有线程都将拥有对这些变量的独占访问权,<br style="line-height:25px"> 并且以后其他线程获得该锁定时,将可以看到对这些变量进行的更改。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">弊端是如果锁定竞争太厉害(线程常常在其他线程具有锁定时要求获得该锁定),会损害吞吐量,因为竞争的同步非常昂贵。<br style="line-height:25px"></wbr></span><wbr style="line-height:25px">(PublicServiceAnnouncement:对于现代JVM而言,无竞争的同步现在非常便宜。)<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">基于锁定的算法的另一个问题是:如果延迟具有锁定的线程</wbr></span><wbr style="line-height:25px">(因为页面错误、计划延迟或其他意料之外的延迟),<br style="line-height:25px"> 则<span style="line-height:25px"><wbr style="line-height:25px">没有要求获得该锁定的线程可以继续运行</wbr></span><wbr style="line-height:25px">。<br style="line-height:25px"> 还可以使用可变变量来以比同步更低的成本存储共享变量,但它们有局限性。<br style="line-height:25px"> 虽然可以保证其他变量可以立即看到对可变变量的写入,但无法呈现原子操作的读-修改-写顺序,<br style="line-height:25px"> 这意味着(比如说)可变变量无法用来可靠地实现互斥(互斥锁定)或计数器。<br style="line-height:25px"><br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">使用锁定实现计数器和互斥</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"> 假如开发线程安全的计数器类,那么这将暴露get()、increment()和decrement()操作。<br style="line-height:25px"> 清单1显示了如何使用锁定(同步)实现该类的例子。注意所有方法,甚至需要同步get(),使类成为线程安全的类,<br style="line-height:25px"> 从而确保没有任何更新信息丢失,所有线程都看到计数器的最新值。<br style="line-height:25px"> 清单1.同步的计数器类<br style="line-height:25px"> publicclassSynchronizedCounter{<br style="line-height:25px"> privateintvalue;<br style="line-height:25px"> publicsynchronizedintgetValue(){returnvalue;}<br style="line-height:25px"> publicsynchronizedintincrement(){return++value;}<br style="line-height:25px"> publicsynchronizedintdecrement(){return--value;}<br style="line-height:25px"> }<br style="line-height:25px"> increment()和decrement()操作是原子的读-修改-写操作,为了安全实现计数器,必须使用当前值,<br style="line-height:25px"> 并为其添加一个值,或写出新值,所有这些均视为一项操作,其他线程不能打断它。<br style="line-height:25px"> 否则,如果两个线程试图同时执行增加,操作的不幸交叉将导致计数器只被实现了一次,而不是被实现两次。<br style="line-height:25px"> (注意,通过使值实例变量成为可变变量并不能可靠地完成这项操作。)<br style="line-height:25px"> 许多并发算法中都显示了原子的读-修改-写组合。清单2中的代码实现了简单的互斥,<br style="line-height:25px"> acquire()方法也是原子的读-修改-写操作。要获得互斥,必须确保没有其他人具有该互斥(curOwner=Thread.currentThread()),<br style="line-height:25px"> 然后记录您拥有该互斥的事实(curOwner=Thread.currentThread()),所有这些使其他线程不可能在中间出现以及修改curOwnerfield。<br style="line-height:25px"> 清单2.同步的互斥类<br style="line-height:25px"> publicclassSynchronizedMutex{<br style="line-height:25px"> privateThreadcurOwner=null;<br style="line-height:25px"> publicsynchronizedvoidacquire()throwsInterruptedException{<br style="line-height:25px"> if(Thread.interrupted())thrownewInterruptedException();<br style="line-height:25px"> while(curOwner!=null)<br style="line-height:25px"> wait();<br style="line-height:25px"> curOwner=Thread.currentThread();<br style="line-height:25px"> }<br style="line-height:25px"> publicsynchronizedvoidrelease(){<br style="line-height:25px"> if(curOwner==Thread.currentThread()){<br style="line-height:25px"> curOwner=null;<br style="line-height:25px"> notify();<br style="line-height:25px"> }else<br style="line-height:25px"> thrownewIllegalStateException("notownerofmutex");<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"> 清单1中的计数器类可以可靠地工作,在竞争很小或没有竞争时都可以很好地执行。然而,在竞争激烈时,这将大大损害性能,<br style="line-height:25px"> 因为JVM用了更多的时间来调度线程,管理竞争和等待线程队列,而实际工作(如增加计数器)的时间却很少。<br style="line-height:25px"> 您可以回想上月专栏中的图,该图显示了一旦多个线程使用同步竞争一个内置监视器,吞吐量将如何大幅度下降。<br style="line-height:25px"> 虽然该专栏说明了新的ReentrantLock类如何可以更可伸缩地替代同步,但是对于一些问题,还有更好的解决方法。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">锁定问题<br style="line-height:25px"></wbr></span><wbr style="line-height:25px">使用锁定,如果一个线程试图获取其他线程已经具有的锁定,那么该线程将被阻塞,直到该锁定可用。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">此方法具有一些明显的缺点,其中包括当线程被阻塞来等待锁定时,它无法进行其他任何操作。<br style="line-height:25px"> 如果阻塞的线程是高优先级的任务,那么该方案可能造成非常不好的结果(称为优先级倒置的危险)。<br style="line-height:25px"> 使用锁定还有一些其他危险,如死锁</wbr></span><wbr style="line-height:25px">(当以不一致的顺序获得多个锁定时会发生死锁)。<br style="line-height:25px"> 甚至没有这种危险,锁定也仅是相对的粗粒度协调机制,同样非常适合管理简单操作,如增加计数器或更新互斥拥有者。<br style="line-height:25px"> 如果有更细粒度的机制来可靠管理对单独变量的并发更新,则会更好一些;在大多数现代处理器都有这种机制。<br style="line-height:25px"><br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">硬件同步原语</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"> 如前所述,大多数现代处理器都包含对多处理的支持。当然这种支持包括多处理器可以共享外部设备和主内存,<br style="line-height:25px"> 同时它通常还包括对指令系统的增加来支持多处理的特殊要求。<br style="line-height:25px"> 特别是,几乎每个现代处理器都有通过可以检测或阻止其他处理器的并发访问的方式来更新共享变量的指令。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">比较并交换(CAS)</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"> 支持并发的第一个处理器提供原子的测试并设置操作,通常在单位上运行这项操作。<br style="line-height:25px"> 现在的处理器(包括Intel和Sparc处理器)使用的最通用的方法是实现名为比较并转换或CAS的原语。<br style="line-height:25px"> (在Intel处理器中,比较并交换通过指令的cmpxchg系列实现。PowerPC处理器有一对名为“加载并保留”和“条件存储”的指令,它们实现相同的目地;MIPS与PowerPC处理器相似,除了第一个指令称为“加载链接”。)<br style="line-height:25px"> CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。(在CAS的一些特殊情况下将仅返回CAS是否成功,而不提取当前值。)CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”<br style="line-height:25px"> 通常将CAS用于同步的方式是从地址V读取值A,执行多步计算来获得新值B,然后使用CAS将V的值从A改为B。如果V处的值尚未同时更改,则CAS操作成功。<br style="line-height:25px"> 类似于CAS的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时修改变量,因为如果其他线程修改变量,那么CAS会检测它(并失败),算法可以对该操作重新计算。清单3说明了CAS操作的行为(而不是性能特征),但是<span style="line-height:25px"><wbr style="line-height:25px">CAS的价值是它可以在硬件中实现,并且是极轻量级的(在大多数处理器中)</wbr></span><wbr style="line-height:25px">:<br style="line-height:25px"> 清单3.说明比较并交换的行为(而不是性能)的代码<br style="line-height:25px"> publicclassSimulatedCAS{<br style="line-height:25px"> privateintvalue;<br style="line-height:25px"> publicsynchronizedintgetValue(){returnvalue;}<br style="line-height:25px"> publicsynchronizedintcompareAndSwap(intexpectedValue,intnewValue){<br style="line-height:25px"> if(value==expectedValue)<br style="line-height:25px"> value=newValue;<br style="line-height:25px"> returnvalue;<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">使用CAS实现计数器</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">基于CAS的并发算法称为无锁定算法,因为线程不必再等待锁定</wbr></span><wbr style="line-height:25px">(有时称为互斥或关键部分,这取决于线程平台的术语)。无论CAS操作成功还是失败,在任何一种情况中,它都在可预知的时间内完成。如果CAS失败,调用者可以重试CAS操作或采取其他适合的操作。清单4显示了重新编写的计数器类来使用CAS替代锁定:<br style="line-height:25px"> 清单4.使用比较并交换实现计数器<br style="line-height:25px"> publicclassCasCounter{<br style="line-height:25px"> privateSimulatedCASvalue;<br style="line-height:25px"> publicintgetValue(){<br style="line-height:25px"> returnvalue.getValue();<br style="line-height:25px"> }<br style="line-height:25px"> publicintincrement(){<br style="line-height:25px"> intoldValue=value.getValue();<br style="line-height:25px"> while(value.compareAndSwap(oldValue,oldValue+1)!=oldValue)<br style="line-height:25px"> oldValue=value.getValue();<br style="line-height:25px"> returnoldValue+1;<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">无锁定且无等待算法</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"> 如果每个线程在其他线程任意延迟(或甚至失败)时都将持续进行操作,就可以说该算法是无等待的。<br style="line-height:25px"> 与此形成对比的是,无锁定算法要求仅某个线程总是执行操作。<br style="line-height:25px"> (无等待的另一种定义是保证每个线程在其有限的步骤中正确计算自己的操作,而不管其他线程的操作、计时、交叉或速度。<br style="line-height:25px"> 这一限制可以是系统中线程数的函数;例如,如果有10个线程,每个线程都执行一次CasCounter.increment()操作,<br style="line-height:25px"> 最坏的情况下,每个线程将必须重试最多九次,才能完成增加。)<br style="line-height:25px"> 再过去的15年里,人们已经对无等待且无锁定算法(也称为无阻塞算法)进行了大量研究,<br style="line-height:25px"> 许多人通用数据结构已经发现了无阻塞算法。无阻塞算法被广泛用于操作系统和JVM级别,进行诸如线程和进程调度等任务。<br style="line-height:25px"> 虽然它们的实现比较复杂,但相对于基于锁定的备选算法,<br style="line-height:25px"> 它们有许多优点:可以避免优先级倒置和死锁等危险,竞争比较便宜,协调发生在更细的粒度级别,允许更高程度的并行机制等等。<br style="line-height:25px"><br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">原子变量类</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"> 在JDK5.0之前,如果不使用本机代码,就不能用Java语言编写无等待、无锁定的算法。<br style="line-height:25px"> 在java.util.concurrent.atomic包中添加原子变量类之后,这种情况才发生了改变。<br style="line-height:25px"> 所有原子变量类都公开比较并设置原语(与比较并交换类似),这些原语都是使用平台上可用的最快本机结构<br style="line-height:25px"> (比较并交换、加载链接/条件存储,最坏的情况下是旋转锁)来实现的。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">java.util.concurrent.atomic包中提供了原子变量的9种风格(AtomicInteger;AtomicLong;AtomicReference;AtomicBoolean;<br style="line-height:25px"> 原子整型;长型;引用;及原子标记引用和戳记引用类的数组形式,其原子地更新一对值</wbr></span><wbr style="line-height:25px">)。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">原子变量类可以认为是volatile变量的泛化</wbr></span><wbr style="line-height:25px">,它扩展了可变变量的概念,来支持原子条件的比较并设置更新。<br style="line-height:25px"> 读取和写入原子变量与读取和写入对可变变量的访问具有相同的存取语义。<br style="line-height:25px"> 虽然原子变量类表面看起来与清单1中的SynchronizedCounter例子一样,但相似仅是表面的。<br style="line-height:25px"> 在表面之下,原子变量的操作会变为平台提供的用于并发访问的硬件原语,比如比较并交换。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">更细粒度意味着更轻量级</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"> 调整具有竞争的并发应用程序的可伸缩性的通用技术是降低使用的锁定对象的粒度,<br style="line-height:25px"> 希望更多的锁定请求从竞争变为不竞争。从锁定转换为原子变量可以获得相同的结果,<br style="line-height:25px"> 通过切换为更细粒度的协调机制,竞争的操作就更少,从而提高了吞吐量。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">java.util.concurrent中的原子变量<br style="line-height:25px"></wbr></span><wbr style="line-height:25px">无论是直接的还是间接的,几乎java.util.concurrent包中的所有类都使用原子变量,而不使用同步。<br style="line-height:25px"> 类似ConcurrentLinkedQueue的类也使用原子变量直接实现无等待算法,<br style="line-height:25px"> 而类似ConcurrentHashMap的类使用ReentrantLock在需要时进行锁定。<br style="line-height:25px"> 然后,ReentrantLock使用原子变量来维护等待锁定的线程队列。<br style="line-height:25px"> 如果没有JDK5.0中的JVM改进,将无法构造这些类,这些改进暴露了(向类库,而不是用户类)接口来访问硬件级的同步原语。<br style="line-height:25px"> 然后,java.util.concurrent中的原子变量类和其他类向用户类公开这些功能。<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">使用原子变量获得更高的吞吐量<br style="line-height:25px"></wbr></span><wbr style="line-height:25px">清单5显示了使用同步的PRNG实现和使用CAS备选实现来实现线程安全。注意,要在循环中执行CAS,<br style="line-height:25px"> 因为它可能会失败一次或多次才能获得成功,使用CAS的代码总是这样。<br style="line-height:25px"> publicclassPseudoRandomUsingSynchimplementsPseudoRandom{<br style="line-height:25px"> privateintseed;<br style="line-height:25px"> publicPseudoRandomUsingSynch(ints){seed=s;}<br style="line-height:25px"> publicsynchronizedintnextInt(intn){<br style="line-height:25px"> ints=seed;<br style="line-height:25px"> seed=Util.calculateNext(seed);<br style="line-height:25px"> returns%n;<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"> publicclassPseudoRandomUsingAtomicimplementsPseudoRandom{<br style="line-height:25px"> privatefinalAtomicIntegerseed;<br style="line-height:25px"> publicPseudoRandomUsingAtomic(ints){<br style="line-height:25px"> seed=newAtomicInteger(s);<br style="line-height:25px"> }<br style="line-height:25px"> publicintnextInt(intn){<br style="line-height:25px"> for(;;){<br style="line-height:25px"> ints=seed.get();<br style="line-height:25px"> intnexts=Util.calculateNext(s);<br style="line-height:25px"> if(seed.compareAndSet(s,nexts))<br style="line-height:25px"> returns%n;<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"> }<br style="line-height:25px"><br style="line-height:25px"> 注1:<br style="line-height:25px"><span style="line-height:25px"><wbr style="line-height:25px">ABA问题</wbr></span><wbr style="line-height:25px"><br style="line-height:25px"> 因为在更改V之前,CAS主要询问“V的值是否仍为A”,所以在第一次读取V以及对V执行CAS操作之前,<br style="line-height:25px"> 如果将值从A改为B,然后再改回A,会使基于CAS的算法混乱。在这种情况下,CAS操作会成功,<br style="line-height:25px"> 但是在一些情况下,结果可能不是您所预期的。<br style="line-height:25px"> (注意,清单1和清单2中的计数器和互斥例子不存在这个问题,但不是所有算法都这样。)这类问题称为ABA问题,<br style="line-height:25px"> 通常通过将标记或版本编号与要进行CAS操作的每个值相关联,并原子地更新值和标记,来处理这类问题。<br style="line-height:25px"> AtomicStampedReference类支持这种方法。<br style="line-height:25px"> 注2:<br style="line-height:25px"> 文章来源:<a target="_blank" rel="nofollow" href="https://www.ibm.com/developerworks/cn/java/j-jtp11234/" style="color:rgb(207,121,28); line-height:25px; text-decoration:none">https://www.ibm.com/developerworks/cn/java/j-jtp11234/</a> </wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr></wbr>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值