您的位置:首页 > 编程语言 > Java开发

Java原子变量类

2016-08-14 17:39 92 查看
    以下是对《java并发编程实战》一书中相关部分的总结,加深自己的印象。

    原子变量比锁的粒度更细,量级更轻,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的。原子变量将发生竞争的范围缩小到单个变量上,这是你获得的粒度最细的情况(假设算法能够基于这种细粒度来实现)。更新原子变量的快速(非竞争)路径不会比获取锁的快速路径慢,并且通常会更快,而它的慢速路径肯定比锁的慢速路径快,因为它不需要挂起或重新调度线程。在使用基于原子变量而非锁的算法中,线程在执行时更不易出现延迟,并且如果遇到竞争,也更容易恢复过来。

    原子变量类相当于一种泛化得volatile变量,能够支持原子的和有条件的读-改-写操作。AtomicInteger表示一个int类型的值,并提供了get和set方法,这些Volatile类型的int变量在读取和写入上有着相同的内存语义。它还提供了一个原子的compareAndSet方法(如果该方法成功执行,那么将实现与读取/写入一个volatile变量相同的内存效果),以及原子的增加、递增和递减等方法。

    AtomicInteger表面上非常像一个扩展的Counter类,但在发生竞争的情况下能提供更高的可伸缩性,因为它直接利用了硬件对并发的支持。

    共有12个原子变量类,可分为4组:标量类(Scalar)、更新器类、数组类、复合变量类。最常用的原子变量类就是标量类:AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference。所有这些类都支持CAS,此外,AtomicInteger、AtomicLong还支持算数运算。(要想模拟其它基本类型的原子变量,可以将short或byte等类型与int类型进行转换,以及使用floatToIntBits或doubleToLongBits来转换浮点数)

    原子数组类(只支持Integer、Long和Reference)中的元素可以实现原子更新。原子数组类为数组的元素提供了volatile类型的访问语义,这是普通数组所不具备的特性--volatile类型的数组仅在数组引用上具有volatile语义,而在其元素上则没有。

    尽管原子的标量类扩展了Number类,但并没有扩展一些基本类型的包装类,例如Integer或Long。事实上,它们也不能进行扩展:基本类型的包装类是不可修改的,而原子变量类是可以修改的。在原子变量类中同样没有重新定义hashCode和equals方法,每个实例都是不同的。与其它可变对象相同,它们也不宜用做基于散列的容器中的键值。

1.原子变量是一种“更好的volatile”

通过CAS来维持包含多个变量的不变性条件

public class CasNumberRange {

@Immutable
private static class IntPair{
//不变性条件:lower<=upper
final int lower;
final int upper;//另有两个参数的构造函数
}
private final AtomicReference<IntPair> values = new AtomicReference<IntPair>(new IntPair(0,0));

public int getLower(){
return values.get().lower;
}

public int getUpper(){
return values.get().upper;
}

public void setLower(int i){
while(true){
IntPair oldv = values.get();
if(i>oldv.upper){
throw new IllegalArgumentException(""
+ "Cann't set lower to"+i+">upper");
}
IntPair newv = new IntPair(i,oldv.upper);
if(values.compareAndSet(oldv, newv)){
return;
}
}
}
//对setUpper采用类似的方法
}


2.性能比较:锁与原子变量

    伪随机数生成器(PRNG),在PRNG中,在生成下一个随机数字时需要用到上一个数字,所以在PRNG中必须记录前一个数值并将其作为状态的一部分。

    以下给出了线程安全的PRNG的两种实现,一种使用ReentrantLock,另一种使用AtomicInteger。测试程序将反复调用它们,在每次迭代中将生成一个随机数字(在此过程中将读取并修改共享的seed状态),并执行一些仅在线程本地数据上执行的“繁忙”迭代。这种方法模拟了一些典型的操作,以及一些在共享状态以及线程本地状态上的操作。

基于ReentrantLock实现的随机数生成器

@ThreadSafe
public class ReentrantLockPseudoRandom extends PseudoRandom{

private final Lock lock = new ReentrantLock(false);
private int seed;

ReentrantLockPseudoRandom(int seed){
this.seed = seed;
}
public int nextInt(int n){
lock.lock();
try{
int s = seed;
seed = calculateNext(s);
int remainder = s % n;
return remainder >0 ? remainder : remainder+n;
}finally{
lock.unlock();
}
}
}


基于AtomicInteger实现的随机数生成器

@ThreadSafe
public class AtomicPseudoRandom extends PseudoRandom{

private AtomicInteger seed;
AtomicPseudoRandom(int seed){
this.seed = new AtomicInteger(seed);
}

public int nextInt(int n){
while(true){
int s = seed.get();
int nextSeed = calculateNext(s);
if(seed.compareAndSet(s, nextSeed)){
int remainder = s % n;
return remainder > 0 ? remainder : remainder + n;
}
}
}
}
下图给出了在每次迭代中工作量较高以及适中情况下的吞吐量。如果线程本地的计算量较少,那么在锁和原子变量上的竞争将非常激烈。如果线程本地的计算量较多,那么在锁和原子变量上的竞争将会降低。因为在线程中访问锁和原子变量的频率将降低。

    从这些图中可以看出,在高度竞争的情况下,锁的性能将超过原子变量的性能,但在更真实的竞争情况下,原子变量的性能将超过锁的性能。这是因为锁在发生竞争时会挂起线程,从而降低了CPU的使用率和共享内存总线上的同步通信量(这类似于生产者--消费者设计中的可阻塞生产者,它能降低消费者上的工作负载,使消费者的处理速度赶上生产者的处理速度。)另一方面,如果使用原子变量,那么发出调用的类负责对竞争进行管理。与大多数基于CAS的算法一样,AtomicPseudoRandom在遇到竞争时将立即重试,这通常是一种正确的方法,但在激烈竞争环境下却导致了更多的竞争。下面两个图是粗略图,数值对应没那么精确,但足以表达相互之间的性能高低。



在竞争程度较高情况下的Lock与AtomicInteger的性能




在竞争程度适中情况下的Lock与AtomicInteger的性能

    在批评AtomicPseudoRandom写的太糟糕或者原子变量比锁更糟糕之前,应该意识到一图中竞争级别过高而有些不切实际:任何一个真实的程序都不会除了竞争锁或原子变量,其它什么工作都不做。在实际情况中,原子变量在可伸缩性上要高于锁,因为在应对常见的竞争程度时,原子变量的效率会更高。

    在中低端程度的竞争下,原子变量能提供更高的可伸缩性,而在高强度的竞争下,锁能够更有效地避免竞争。(在单CPU的系统上,基于CAS的算法在性能上同样会超过基于锁的算法,因为CAS在单CPU的系统上通常能执行成功,只有在偶然情况下,线程才会在执行读-改-写的操作过程中被其它线程抢占执行。)

    上两个图中都包含了第三条曲线,它是一个使用了ThreadLocal来保存PRNG状态的PseudoRandom。这种实现方法改变了类的行为,即每个线程都只能看到自己私有的伪随机数字序列,而不是所有线程共享同一个随机数序列,这说明了,如果能够避免使用共享状态,那么开销将会更小。我们可以通过提高处理竞争的效率来提高可伸缩性,但只有完全消除竞争,才能实现真正的可伸缩性。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: