深入浅出Java并发包—原子类操作
我们知道,JDK1.5以后引入了并发包(java.util.concurrent)用于解决多CPU时代的并发问题,而并发包中的类大部分是基于Queue的并发类,Queue在大多数情况下使用了原子类(Atomic)操作,因此要了解Concurrent包首先要了解Atomic类。
在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的递增或者递减方案,这个方案一般需要满足以下要求:
1、 简单:操作简单,底层实现简单
2、 高效:占用资源少,操作速度快
3、 安全:在高并发和多线程环境下要保证数据的正确性
Java 中延续了C++带来的++、--操作但是这个不是线程安全的,在前面JVM内存模型中我们已经提到过,执行i++需要3个步骤:
1、 从内存中取出变量,放到线程拷贝内存中
2、 执行++操作
3、 将对应的值写会到之前的内存区域
而这个过程中,当执行++操作的时候,有可能其他线程已经抢先修改了i的值,当i的值写回去的时候,引发另外线程读取数据错误。为了保证这几部运算的原子性,你不得不在方法前加上synchronized关键字,使得某个时刻只能有一个线程在运行!
JDK1.5后增加了AtomicInteger类来解决并发情况提升吞吐率的问题。AtomicInteger主要提供了以下方法:
int addAndGet(int delta)
以原子方式将给定值与当前值相加。 实际上就是等于线程安全版本的i =i+delta操作。
boolean compareAndSet(int expect, int update)
如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。 如果成功就返回true,否则返回false,并且不修改原值。
int decrementAndGet()
以原子方式将当前值减 1。 相当于线程安全版本的--i操作。
int get()
获取当前值。
int getAndAdd(int delta)
以原子方式将给定值与当前值相加。 相当于线程安全版本的t=i;i+=delta;return t;操作。
int getAndDecrement()
以原子方式将当前值减 1。 相当于线程安全版本的i--操作。
int getAndIncrement()
以原子方式将当前值加 1。 相当于线程安全版本的i++操作。
int getAndSet(int newValue)
以原子方式设置为给定值,并返回旧值。 相当于线程安全版本的t=i;i=newValue;return t;操作。
int incrementAndGet()
以原子方式将当前值加 1。 相当于线程安全版本的++i操作。
void lazySet(int newValue)
最后设置为给定值。 延时设置变量值,这个等价于set()方法,但是由于字段是volatile类型的,因此次字段的修改会比普通字段(非volatile字段)有稍微的性能延时(尽管可以忽略),所以如果不是想立即读取设置的新值,允许在“后台”修改值,那么此方法就很有用。如果还是难以理解,这里就类似于启动一个后台线程如执行修改新值的任务,原线程就不等待修改结果立即返回(这种解释其实是不正确的,但是可以这么理解)。
void set(int newValue)
设置为给定值。 直接修改原始值,也就是i=newValue操作。
boolean weakCompareAndSet(int expect, int update)
如果当前值 == 预期值,则以原子方式将该设置为给定的更新值。JSR规范中说:以原子方式读取和有条件地写入变量但不 创建任何 happen-before 排序,因此不提供与除 weakCompareAndSet 目标外任何变量以前或后续读取或写入操作有关的任何保证。大意就是说调用weakCompareAndSet时并不能保证不存在happen-before的发生(也就是可能存在指令重排序导致此操作失败)。但是从Java源码来看,其实此方法并没有实现JSR规范的要求,最后效果和compareAndSet是等效的,都调用了unsafe.compareAndSwapInt()完成操作。
示例代码:
AtomicInteger atomicInteger = new AtomicInteger(); System.out.println(atomicInteger.incrementAndGet()); System.out.println(atomicInteger.incrementAndGet()); System.out.println(atomicInteger.incrementAndGet()); System.out.println(atomicInteger.compareAndSet(3, 4)); System.out.println(atomicInteger.decrementAndGet());
AtomicInteger和AtomicLong、AtomicBoolean、AtomicReference都差不多,我在这里就不一一介绍了。下面我们来看一下数组原子类的操作!
类似的,AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray的API都差不多,我们选择一个有代表性的AtomicIntegerArray来描述这些问题。
int get(int i)
获取位置 i 的当前值。很显然,由于这个是数组操作,就有索引越界的问题(IndexOutOfBoundsException异常)。
对于下面的API起始和AtomicInteger是类似的,这种通过方法、参数的名称就能够得到函数意义的写法是非常值得称赞的。在《重构:改善既有代码的设计》和《代码整洁之道》中都非常推崇这种做法。
void set(int i, int newValue) void lazySet(int i, int newValue) int getAndSet(int i, int newValue) boolean compareAndSet(int i, int expect, int update) boolean weakCompareAndSet(int i, int expect, int update) int getAndIncrement(int i) int getAndDecrement(int i) int getAndAdd(int i, int delta) int incrementAndGet(int i) int decrementAndGet(int i) int addAndGet(int i, int delta)
整体来说,数组的原子操作在理解上还是相对比较容易的,这些API就是有多使用才能体会到它们的好处,而不仅仅是停留在理论阶段。具体的细节和示例我就不一一列举了!
下面我们来看下,如何原子操作类中的属性。JDK提供了基于反射的类AtomicIntegerFieldUpdater<T>、AtomicLongFieldUpdater<T>、AtomicReferenceFieldUpdater<T,V>来修改对应的数据。但是使用以上几个类必须满足以下条件:
1、 字段必须是volatile类型的!
2、 字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。
3、 只能是实例变量,不能是类变量,也就是说不能加static关键字。
4、 只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。
5、 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。
我们来看下对应的一个示例
package com.test; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; public class AtomicClass { public volatile int x; volatile long y; protected volatile int z; private volatile int n; public static void main(String[] args) { AtomicClass demo = new AtomicClass(); System.out.println(AtomicIntegerFieldUpdater.newUpdater(AtomicClass.class, "x").addAndGet(demo, 12)); System.out.println(demo.x); System.out.println(AtomicLongFieldUpdater.newUpdater(AtomicClass.class, "y").incrementAndGet(demo)); System.out.println(demo.y); System.out.println(AtomicIntegerFieldUpdater.newUpdater(AtomicClass.class, "z").compareAndSet(demo, 0, 100)); System.out.println(demo.z); System.out.println(AtomicIntegerFieldUpdater.newUpdater(AtomicClass.class, "n").decrementAndGet(demo)); System.out.println(demo.n); } } package com.test; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.concurrent.atomic.AtomicLongFieldUpdater; public class AtomicClass { public volatile int x; volatile long y; protected volatile int z; private volatile int n; public static void main(String[] args) { AtomicClass demo = new AtomicClass(); System.out.println(AtomicIntegerFieldUpdater.newUpdater(AtomicClass.class, "x").addAndGet(demo, 12)); System.out.println(demo.x); System.out.println(AtomicLongFieldUpdater.newUpdater(AtomicClass.class, "y").incrementAndGet(demo)); System.out.println(demo.y); System.out.println(AtomicIntegerFieldUpdater.newUpdater(AtomicClass.class, "z").compareAndSet(demo, 0, 100)); System.out.println(demo.z); System.out.println(AtomicIntegerFieldUpdater.newUpdater(AtomicClass.class, "n").decrementAndGet(demo)); System.out.println(demo.n); } }
相同的代码,因为上一个示例中main处于AtomicClass内部,而下面是外部另外的一个类,所以第一个所以的资源都可以访问,而第二个类最好一个资源n将不能正常访问。更不能修改对应的数据。
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The field AtomicClass.n is not visible
at com.test.XXClass.main(XXClass.java:18)
AtomicMarkableReference类描述的一个<Object,Boolean>的对,可以原子的修改Object或者Boolean的值,这种数据结构在一些缓存或者状态描述中比较有用。这种结构在单个或者同时修改Object/Boolean的时候能够有效的提高吞吐量。
AtomicStampedReference类维护带有整数“标志”的对象引用,可以用原子方式对其进行更新。对比AtomicMarkableReference类的<Object,Boolean>,AtomicStampedReference维护的是一种类似<Object,int>的数据结构,其实就是对对象(引用)的一个并发计数。但是与AtomicInteger不同的是,此数据结构可以携带一个对象引用(Object),且能够对此对象和计数同时进行原子操作。在后面的章节中会提到“ABA问题”,而AtomicMarkableReference/AtomicStampedReference在解决“ABA问题”上很有用。