Java并发编程之无锁
在谈论无所之前先来看看乐观派和悲观派。对于乐观派而言,他们总认为事情总会朝着好的方向发展,总认为几乎不会坏事,我已可以随意的去做事。但是对于悲观派来说,他们认为出错是一种常态,所以无论事情大小都会考虑的面面俱到,滴水不漏。
在两种派别对应在并发中就是加锁和无锁,也就是说加锁是一种悲观的策略,而无锁是一种乐观的策略。对于锁,如果有多个线程同事访问一个临界资源,宁可牺牲性能让线程等待,也不会让一个线程不加锁访问临界资源。对于无锁,它会假定对资源的访问是没有冲突的。也就是多个线程对临界资源的访问是没有冲突的。既然没有冲突,那么就不需要等待,所有的线程都可以不需要等待的执行下去。如果遇到了冲突,怎么办?这里无锁策略使用了一种称为CAS的技术来保证线程执行的安全性。下面我们来具体讨论一下CAS。
无锁解决冲突的办法:CAS
CAS的全称是Compare And Swap即比较和交换。
CAS算法的过程是这样的:它包含三个参数的变量CAS(V, E, N)。作用如下:
- V 表示要更新的值。
- E 表示预期的值。
- N 表示新值。
仅当V值等于E值的,才会将V值设置为N值,如果V值和E值不同,则说明已经有其他线程做了更新,当前线程什么都不做。CVS返回的是当前V的真实值。
CAS是乐观派,总认为自己可以操作成功。当多个线程同时使用CAS来给一个变量设置时,只有一个会成功,其它的都会失败,但是CAS很乐观,失败了就失败了,可以再次尝试。
Java中的指针:Unsafe类
What? Java中也有指针。Unsfae类就像它的名字一样,不安全,里面有一些向C语言的指针一样直接操作内存的方法。并且官方也不推荐直接使用Unsafe类。但是CAS的实现用到了这个类。
下面来看看Unsafe中的一些方法:
// 分配内存
public native long allocateMemory(long var1);
// 重新分配内存
public native long reallocateMemory(long var1, long var3);
// 拷贝内存
public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);
// 释放内存
public native void freeMemory(long var1);
// 获取起始地址
public native long getAddress(long var1);
// 获取操作系统内存页大小
public native int pageSize();
.....
Unsafe中有两个方法可以将线程挂起和恢复,如下:
// 线程调用方法,将线程挂起
public native void unpark(Object var1);
// 线程恢复
public native void park(boolean var1, long var2);
Unsafe中的关于CAS的操作:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
CAS的操作的核心实现是这三个方法。
三个方法的参数都类似,分别为:
- CAS需要更改变量的对象;
- 对象内存的偏移量;
- 期望值
- 需要设置的值
其中偏移量可以通过Unsafe类中的objectFieldOffset()方法获取到,这些方法如下:
public native long objectFieldOffset(Field field);
因为int,long,boolean类型的相关操作不是原子性的,所以JDK在1.5之后提供了atomic包(具体在java.util.concurrent.atomic
中)来将这些操作变成原子操作。
下面的图片中的是atomic下提供的原子操作的类:
这里以AtomicInteger来进行分析:
public class AtomicInteger extends Number implements java.io.Serializable {
// 实例化Unsafe
private static final Unsafe unsafe = Unsafe.getUnsafe();
// valueOffset变量保存的是value中的偏移量,这一点可以在下面的static初始化块中可以看出
private static final long valueOffset;
static {
try {
// 获取value的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
* 通过int值实例化一个AtomicInteger对象
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* 实例化一个AtomicInteger对象,其初始值为0
*/
public AtomicInteger() {
}
// 获取当前值
public final int get() {
return value;
}
// 设置当前值
public final void set(int newValue) {
value = newValue;
}
// 延迟设值
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
// 设置新值获取旧值
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
// CAS操作,把当前值和预期值做比较,相当时设置新值
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final boolean weakCompareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
// 当前值加1,返回旧值
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// 当前值-1,返回旧值
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
// 当前值加上delta,然后返回旧值
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 当前值加1,然后返回
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// 当前值减1,返回返回
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
// 当前值加上delta,然后返回
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
// ...
}
其中的大部分方法都直接或者间接使用了CAS来保证安全。
除了上面的对于基本变量的Atomic类,还有关于普通对象引用的Atmoic类。
AtomicReference 无锁的引用
AtomicReference和AtmoicInteger非常类似,不同之处就是AtmoicInteger是对整数的封装,而AtmoicReference是对普通对象的引用,也就是它可以保证在修改对象引用时线程的安全性。
CAS中有一个很重要的问题ABA。CAS比较的是对象中的值和期望值,但是有可能在你获取到当前对象的数据后,在准备修改为新值之前,对象的值被其他线程连续修改两次,而且经过这两次修改之后,对象有恢复为旧值。这样,前后的结果看似没有被改过,但是其实已经被修改了2次。过程如下图:
一般来说,发生这种事情的可能性很小。而且即使发生了也不会有什么影响,比如,一个数字,被修改一次后,在修改回去,不会对CAS产生什么影响。
但是有时候在一些具体问题中这种情况就有可能发生。所以在JDK中提供了AtomicStampedReference来解决这种问题。
AtomicStampedReference
AtomicStampedReference在内部维护了一个时间戳。当AtmoicStampedReference对应的数字被修改时,除了更新数据本身,还必须更新时间戳。当AtomicStampedReference设置对象值时,对象和时间戳都必须满足期望值,才会写入成功。通过维护时间戳能有效的防止ABA问题。
AtmoicStampedReference的几个API在Atmomic的基础上新增了几个有关时间戳的信息。
// 参数为:期望值,新值,期望时间戳,新的时间戳
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp);
// 获取索引
public V getReference()
// 获取时间戳
public int getStamp()
// 设置当前对象引用和时间戳
public void set(V newReference, int newStamp)