线程安全策略--CAS
CAS与锁
CAS 全称 Compare and Swap ,中文名比较交换空间。JDK5.0 以后通过 CAS 操作实现无锁,现代主流处理器都支持CAS指令操作。 CAS 由三个参数(V,A,B)。V表示要变更的变量,A代表期望值,B代表要写入的新值。当且仅当 V == A 时, 执行 V = B,否者什么都不做(因为其它线程可能已经做了更新)。
线程安全策略除了 CAS 还有一种就是比较普遍的 加锁 方式来实现。使用锁的方式来实现线程安全是一种悲观的策略。锁的存在是因为当多个线程访问临界区的资源时,避免多个线程同时对同一资源进行操作而导致的数据异常。锁的使用是一种牺牲系统性能换取线程安全,因为线程在对锁进行申请锁和等待锁的过程中,会有线程的挂起和恢复,引起较多的上下文调度和调度延时。同时多锁的使用不当还会造成死锁。
Unsafe类
Java不能直接访问操作系统,而是通过本地方法来访问。 Unsafe 提供了类似硬件级别的原子操作,是不安全的操作,是 sun.misc 包中的一个类。众所周知,指针是不安全的,如果指针指错了位置或者计算指针偏移量出错,会引起灾难性问题。这也是Java去指针化的原因之一。而 Unsafe 类就封装了一些类指针操作。
Unsafe 类的一些操作如下(以int操作为例):
//给定对象var1,var2为对象内偏移量(用于定位一个字段到对象头部的偏移量,可以快速定位到指定字段),
//如果字段的值等于期望var4,则更新字段为var5
public final native boolean compareAndSwapInt(Object var1, long var2,
int var4, int var5);
public native int getInt(Object var1, long var2); //获取给定对象上的偏移量
public native void putInt(Object var1, long var2, int var4);//设置给定字段在对象中的偏移量
public native long objectFieldOffset(Field var1); //获取字段在对象中的偏移量
public native void putIntVolatile(Object var1, long var2, int var4);//使用volatile语义设置对象的int值
public native int getIntVolatile(Object var1, long var2);//使用volatile语义获取对象的int值
//与putIntVolatile()方法一样,但是要求被操作字段必须是volatile类型
public native void putOrderedInt(Object var1, long var2, int var4);
前面说到 Java 抛弃了指针操作,但是在关键时候还是需要提供一些类似指针的操作, Unsafe就是一个例子,但是 JDK 的开发人员并不想各位直接操作 Unsafe 类,笔者尝试使用试用 IDEA 和 Eclipse 进行 Unsafe 类的相关操作均保存。
Unsafe 的实例方法是通过调用工厂方法 getUnsafe()。它的实现如下:
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
上述代码块中的 VM.isSystemDomainLoader(var0.getClassLoader()) 判断是否是 JDK 内部专属的类,是则可以使用,否者抛出异常。
原子变量
Java 中的原子变量是实现了 CAS 操作。 JDK 的 java.util.concurrent.atomic 包就是提供一些原子变量类。常见的原子变量类有 AtomicInteger 、 AtomicLong 、 AtomicBoolean 、 AtomicReference 等。这些类分别对应操作 int、long、boolean 和 Object。原子变量没有定义 hashCode() 和 equals() 方法。例如: AtomicInteger 可以是一个整数,但是和 Integer 不同,它是可变的,并且是线程安全的,对它的所有操作都是基于 CAS 指令进行的。
AtomicReference 类
以上列举的几个原子操作的方法都很类似,这里以 AtomicReference 进行进一步的剖析讲解。
AtomicReference 类中有两个比较重要的变量。
private volatile V value; //代表当前的实际取值
private static final long valueOffset; //变量value的偏移量
AtomicReference 提供的方法如下:
public final V get() //获取当前值
public final void set(V newValue) //设置当前值
public final V getAndSet(V newValue) //设置新值,并返回旧值
public final void lazySet(V newValue) //最终设置为给定值
public final boolean compareAndSet(V expect, V update) //如果当前期望为expect,则设置为update
我们关注下 getAndSet 方法的内部实现:
public final V getAndSet(V newValue) {
while (true) {
V x = get();
if (compareAndSet(x, newValue))
return x;
}
}
先获取当前的值,然后执行 compareAndSet 方法,这个方法本质就是执行了 Unsafe 类中的 compareAndSwapObject() 方法。如果当前值是期望值 x ,则设置新值 newValue 。如果 不满足,则说明其它线程修改了当前值,重新设置新值,直到满足条件后返回。这体现了 CAS 操作的思想。
这里以商品库存为例,一个线程代表一次用户购买改商品,库存减少一个。
public class Commodity {
private String name;
private int stock;
public Commodity(String name, int stock) {
this.name = name;
this.stock = stock;
}
//省略get和set方法
//...
}
public class Shopping {
public static AtomicReference ar = new AtomicReference();
public static class Task implements Runnable {
@Override
public void run() {
while(true) {
Commodity c1 = ar.get();
Commodity c2 = new Commodity(c1.getName(), c1.getStock()-1);
if(ar.compareAndSet(c1, c2))
break;
}
}
}
public static void main(String[] args) throws InterruptedException {
Commodity commodity = new Commodity("X手机", 100);
ar.set(commodity);
Thread[] threads = new Thread[50];
for(int i = 0; i < 50; i++) {
threads[i] = new Thread(new Task());
}
for(int i = 0; i < 50; ++i) { threads[i].start(); }
for(int i = 0; i < 50; ++i) { threads[i].join(); }
System.out.println("原来库存:"+commodity.getStock()+
"\n现在库存:"+ar.get().getStock());
}
}
上面例子的执行的执行结果如下:
原来库存:100
现在库存:50
可以看出,我们开了50个线程,在线程安全的情况下的剩余库存应该是50个,实际我们也是得到了50个线程。
AtomicStampedReference 类
从以上的例子,我们可以看出,线程判断是否可以写入是基于对象的期望值和当前值是否一致。在这种情况下,我们获取对象后,有另外一个线程修改了当前的对象值,而在我们要将新值写入的时候,又有别的线程将当前值改回成了我们期望的值,这时候我们的更新操作的可以成功的。所以我们可以看出 AtomicReference 虽然可以保证数据安全,当时无法判断当前值是否被修改过。
一般情况下,上述情况是不会发生的,即使发生了,也不是什么很大的问题。因为只要我们关注的是结果正确就行了。但是,现实生活中我们不仅要关注结果,还关注这个变化过程。这时候就会出现问题。
这时候,我们就需要借助 AtomicStampedReference 的帮助。 AtomicStampedReference 内部不仅维护了对象值,还维护了对象的状态值。当对象被修改时就会更新状态值,只有对象值和状态值都满足期望时,更新才会成功。一般,我们选取时间戳来当状态值。
以下列出 AtomicStampedReference 的几个重要的方法:
//比较满足后设置新值。参数分别是 期望值,新值,期望状态值,新状态值
public boolean compareAndSet(V expectedReference,V newReference, int expectedStamp, int newStamp)
public V getReference() //获取当前对象的引用
public int getStamp() //获取当前对象的状态值
public void set(V newReference, int newStamp) //设置当前对象的引用和状态值
public V get(int[] stampHolder) //返回对象的引用和状态值
例:一家商店搞活动,当贵宾卡的余额小于20元时,免费为用户充值20元,但是每个用户只能被赠送一次。
public class AtomicReferenceDemo {
public static AtomicStampedReference money = new AtomicStampedReference(19, 0);
public static void main(String[] args) {
//模拟后台多个线程为用户更新充值
for(int i = 0; i <= 2; ++i) {
final int nTimeStamp = money.getStamp();
new Thread() {
@Override
public void run() {
while(true) {
while(true) {
Integer m = money.getReference();
if(m < 20) {
if(money.compareAndSet(m, m+20, nTimeStamp, nTimeStamp+1 )) {
System.out.println("金额小于20.充值20元,余额: "+money.getReference() + " 元");
break;
}
} else {
break;
}
}
}
}
}.start();
}
//用户线程,模拟用户消费
new Thread() {
@Override
public void run() {
for(int i = 0; i <= 100; ++i) {
while(true) {
int nTimeStamp = money.getStamp();
Integer m = money.getReference();
if(m > 10) {
System.out.println("大于10元");
if(money.compareAndSet(m, m-10, nTimeStamp, nTimeStamp+1)) {
System.out.println("消费10元,余额: "+ money.getReference()+ " 元");
break;
}
} else {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}.start();
}
}
我们使用时间戳作为状态值,如果修改成功则更新时间戳,使得系统不会发生二次赠送。运行结果如下:
金额小于20.充值20元,余额: 39 元
大于10元
消费10元,余额: 29 元
大于10元
消费10元,余额: 19 元
大于10元
消费10元,余额: 9 元
无锁数组
JDK 除了为我们准备了基本类型的原子变量外,还提供了数组等复合变量。当前原子数组有:AtomicIntegerArray , AtomicLongArray 和 AtomicReferenceArray 。这里以 AtomicIntegerArray 为例进行讲解。
AtomicIntegerArray 是本质上是对 int[] 类型的封装。使用 Unsafe 类通过 CAS 方式 控制 int[] 在多线程下的安全。常用的方法如下:
public final int get(int i) //获取下标为i的数组元素
public final int length() //获取数组长度
public final int getAntSet(int i, int newValue) //将第i个数组的下标设置为newValue
public final boolean compareAndSet(int i, int expect, int update) //第i个的当前值等于期望值expect时,更新为新值update
public final int getAndIncrement(int i) //第i个元素+1
public final int getAndDecrement(int i) //第i个元素-1
public final int getAndAdd(int i, int delta) //第i个元素+delta(delta可以为负数)
下面是一个实例:
public class AtomicIntegerArrayDemo {
static AtomicIntegerArray array = new AtomicIntegerArray(10);
public static class AddThread implements Runnable {
@Override
public void run() {
for(int k = 0; k < 10000; ++k) {
array.getAndIncrement(k%array.length());
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[10];
for(int k = 0; k < 10; ++k) {
ts[k] = new Thread(new AddThread());
}
for(int k = 0; k < 10; ++k) { ts[k].start(); }
for(int k = 0; k < 10; ++k) { ts[k].join(); } //表示主线程原意等待子线程执行完毕
System.out.println(array);
}
}
程序的执行结果如下:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
可以看到输出的结果都是10000,在线程不安全的情况下应该会有值小于10000,所以 AtomicIntegerArray 是可以保证数组的线程安全的。
总结
CAS 提供一种无锁的操作来保证线程安全,拥有比锁更高的性能,具有非拥塞性,而且天生免疫死锁。但是,CAS 的实现操作比较复杂。
本文在编写中难免会有所错误,欢迎批评指正。