Java concurrent
java concurrent包是常用的jdk包之一,主要用于多线程编程,我认为可以主要由以下几部分构成:
- 线程安全数据结构
- 线程管理
- 线程执行
目录
线程安全数据结构
所谓线程安全的数据结构,即该数据结构的实例在多线程环境下运行时,可以保证每个线程可以正确的读和写该实例,而不会因为线程运行的时间不确定性导致该数据结构的值不可控。
为了达到这个目的,concurrent包里采用了两类方法实现
- CAS(compare and swap)
- 锁
接下来来看这两类实现方法的原理。
CAS
CAS又叫compare and swap, wiki,concurrent中使用这种方法实现的线程安全类有下面几个:
基于CAS的线程安全类 | 含义 |
---|---|
AtomicBoolean | 原子布尔 |
AtomicInteger | 原子整形 |
AtomicIntegerArray | 原子整形数组 |
AtomicLong | 原子长整型 |
AtomicLongArray | 原子长整型数组 |
AtomicReference | 原子指针(需要有一个它指向对象的类型,源码中的V) |
AtomicReferenceArray | 原子指针数组 |
AtomicStampedReference | 原子标记指针(其实就是一个Pair(T reference, int stamp)) |
可以从命名上看出这些类均已Atomic开头,Atomic翻译过来是原子,所以以AtomicInteger为例,可以理解成为这个整形的操作都是原子操作。
如下是AtomicInteger的部分源码,
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
这里的value就是我们平时使用Integer的value。这个变量一定需要声明成volatile的,原因是保证这个变量的值随时可被所有线程可见。volatile关键字的作用主要有两个,一个是保证变量的变化被所有线程可见(具体原因和java内存模型有关:jvm中的主存被所有线程共用,每个线程有自己的工作内存,共用内存的某些变量会在各个线程有一份拷贝,volatile是一个轻量级的同步操作,会将主存中变量的变化通知到各线程中,更改各线程中的拷贝);另一个是防止编译重排序。
如果我们和其他几个Atomic类比较会发现,他们都有一个unsafe 和offset的成员变量。
这里unsafe并不是线程不安全的意思,而是java要通过这个类执行非java语言实现的一些系统功能(比如compare and swap,大部分内核厂商提供了这样的方法接口),这些代码是危险的native方法,所以是unsafe。如注释所说,这个变量主要用于更新value。
offset是当前对象中value的偏移量,大小是这个value要代表的对象的大小,在这个类初始化的时候通过static方法块为其赋初值。
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(int newValue) {
value = newValue;
}
/**
* Eventually sets to the given value.
*
* @param newValue the new value
* @since 1.6
*/
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
AtomicInteger提供了构造方法为value赋初值,而且提供了针对这个value的get,set方法。另外两个很有意思的方法是getAndSet和lasySet。他们分别通过调用unsafe的getAndSetInt和putOrderedInt实现。这两个方法使用场景不多,getAndSet是可以原子的写入这个新值同时返回旧值。而lazySet是直接在对象的偏移地址写入新值,但这个新值因为延迟不会立即被其他线程可见,但终究会被其他线程可见。lazySet的性能要比直接set好,但除非真的有非常高的性能要求,否则不推荐使用,毕竟多线程编程安全性更重要。
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* <p><a href="package-summary.html#weakCompareAndSet">May fail
* spuriously and does not provide ordering guarantees</a>, so is
* only rarely an appropriate alternative to {@code compareAndSet}.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful
*/
public final boolean weakCompareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
然后就是我们最常用的compareAndSet,这个方法有两个输入expect和update,unsafe会比较当前value和expect的值,相等的话更新为update并返回true,不相等不更新并返回false。这里另外一个有意思的方法叫weakCompareAndSet,它的实现和compareAndSet一模一样但注释说这个方法可能失败。猜测应该是jdk以后会补全这个实现。
其余方法及说明如下表:
方法 | 说明 |
---|---|
getAndIncrement() | 返回旧值,并将其加一 |
getAndDecrement() | 返回旧值,并将其减一 |
getAndAdd(int delta) | 返回旧值,并将其加delta |
incrementAndGet() | 将旧值加一,返回新值 |
decrementAndGet() | 将旧值减一,返回新值 |
addAndGet(int delta) | 将旧值加delta,返回新值 |
getAndUpdate(IntUnaryOperator updateFunction) | 返回旧值,并将其执行一个IntUnaryOperator |
updateAndGet(IntUnaryOperator updateFunction) | 将旧值执行一个IntUnaryOperator,返回新值 |
getAndAccumulate(int x,IntBinaryOperator accumulatorFunction) | 返回旧值,并将其执行一个IntBinaryOperator,x为其操作数 |
accumulateAndGet(int x,IntBinaryOperator accumulatorFunction) | 将旧值执行一个IntBinaryOperator,x为其操作数,并返回新值 |
这里的所有方法线程安全,其中前四个使用unsafe的native方法,后四个都调用compareAndSet进行更新操作,不成功的话会一直重试,直至get到的值compare成功。
这里有IntUnaryOperator和IntBinaryOperator可能造成困扰。IntUnaryOperator就是针对自身的一个计算操作,IntBinaryOperator为自身对另一个数的计算操作,如:
IntUnaryOperator i = (x) -> x*x;
System.out.println(i.applyAsInt(2));
4
IntBinaryOperator io = (x,y)->x +y;
System.out.println(io.applyAsInt(2,3));
5
其他Atomic类和AtomicInteger类似,其中AtomicLong和AtomicInteger是基本一样的,而AtomicBoolean会更简单。而AtomicReference中,value变成一个可变类型,但这里有问题,后边会说。
AtomicIntegerArray中通过base和shift进行位运算得到每个数组下标的内存offset,而AtomicInteger中的value在这里替换成int[] array,值得注意的是这个数组不是volatile的,如果一个数组被volatile修饰,那么只能保护这个数组的引用,一旦引用指向的数组改变,这个保护就有问题了。一句话来说,针对数组来说volatile只能保护其引用,不能保护其元素。所以AtomicIntegerArray的方法都有一个i参数,表示要操作的元素下标。直接get时在getRow中通过unsafe.getIntVolatile(array, offset);方法保证单个元素的可见性。
CAS潜在的一些坑
CAS在一些情况下不能保证线程安全,比如常见的ABA场景:先获取了旧值A,假设A是一个对象,其中有一些状态。然后CAS(A,B),操作成功,这时当前值变成B,如果此时有其他线程修改了A的状态,然后CAS(B,A)。当前值变成A,这个A和之前的A引用是一样的,但是实际状态不同了。此时如果有其他线程再来做CAS(A,C),依旧会成功,但这显然是不正确的。AtomicReference就存在这个问题,这也是它不叫AtomicObject的原因,因为这里保护的只有引用。在concurrent里,AtomicStampedReference解决了这一问题,方法很巧妙,它把需要维护的reference和其时间戳做了一个pair放在一起,这样在compare的时候不仅要比较reference,还要比较时间戳,从而解决了ABA问题。