对于高并发的环境下,无论使用什么加锁策略,程序的性能始终不能与无锁的程序相媲美,那么是否存在一种不使用加锁也能实现各个线程之间同步的策略呢?答案当然是肯定的。那就是比较交换(CAS)策略。
- CAS
- AtomicInteger、AtomicLong
- Unsafe
- AtomicReference、AtomicStampedReference
CAS
CAS是比较交还的英文缩写,和使用锁相比,使用CAS会使得程序更为复杂,但是它是非阻塞的,所以天生对死锁免疫。除此之外,它使用无锁的方式,完全没有锁竞争带来的系统开销,也没有线程之间频繁调度带来的开销,性能更加出色。
CAS的算法原理是这样的:包含三个参数(Value,EexpectValue,NewValue),V表示需要更新的数值,E表示预期数值,N表示新值。当且仅当,V=E时,才会将V的数值改为N;当V!=E时,说明已经有人提前改动了,那么当前线程就不能在继续改动了并且返回当前的数值。当多个线程同时使用CAS改动一个对象时,只有一个线程会胜出,其余失败的线程不会被挂机,而是再次的尝试(很有毅力啊),当然也允许线程放弃。
简单来说,CAS需要一个你希望值,也就是你认为现在这个对象应该的样子,如果和你预想的不一样,说明有人已经改动过了,你就要重新读取,再次尝试修改。但是这种策略存在一个问题,假设有一个线程提把变量改变了一次,接着又变为原来的值;而后一个线程一看,变量还是原来的值,会误以为没有线程改动过,就继续执行。这在某些情况下,会造成程序的错误执行。
后面的内容会告诉我们如何解决这中问题。
AtomicInteger、AtomicLong
JDK中的并发包中有一个Atomic包,里面实现了使用CAS操作的线程安全的类型。最常用的就是AtomicInteger,他和Integer不同,是可变且线程安全的。
public final int get();
public final void set(int newValue);
//以原子方式设置为给定值,并返回旧值。
public final int getAndSet(int newValue);
//如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值。
public final boolean compareAndSet(int expect,int update);
public final int getAndIncrement();//以原子方式将当前值加 1。
public final int getAndDecrement();//以原子方式将当前值减 1。
public final int getAndAdd(int delta);//给定值与当前值相加,返回旧值
public final int incrementAndGet();//以原子方式将当前值减 1。
public final int addAndGet(int delta);//给定值与当前值相加,返回新值
//更多的请看源码
AtomicInteger使用非常简单:
public class AtomicIntegerDemo {
static AtomicInteger i = new AtomicInteger();
public static class Add implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
AtomicIntegerDemo.i.incrementAndGet();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = new Thread[10];
for (int i = 0; i < 10; i++) {
ts[i] = new Thread(new Add());
}
for (int i = 0; i < 10; i++) {
ts[i].start();
}
for (int i = 0; i < 10; i++) {
ts[i].join();
}
System.out.println(i);
}
}
/*
out:
10000
*/
程序对i进行了10000次的加1,如果线程不安全,输出应该小于10000,从输出看说明程序执行正确。
我们关注一下incrementAndGet()的内部实现:
public final int incrementAndGet() {
for (;;) {
//返回数据
int current = get();
int next = current + 1;
//CAS操作
if (compareAndSet(current, next))
return next;
}
}
对于第二行的for (;;);CAS并不是都是成功的,对于失败的情况,我们需要不停的尝试。如果有线程提前修改了值,那么compareAndSet(current, next)会返回false。
AtomicLong实现原理也大同小异,这里不再累述。
Unsafe
让我们来看看compareAndSet(int expect,int update)是如何实现的。
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
我们可以看到一个特殊的变量unsafe它是sum.misc.Usafe类型,从名字看这是一种不安全的类型。我们知道在C/C++中,指针是不安全的,所以在JAVA中取消了指针的概念,而这里的Usafe类则是封装了一些类似指针的概念。valueOffset是对象内的偏移量,通过这个偏移量可以快速定位到指定的字段,expect表示期望值,如果指定的字段值与希望值相等,则设为update。
AtomicReference
和AtomicInteger类似,AtomicReference是保证对普通对象引用的线程安全类。之前我说过,CAS存在的一个问题:对象被反复修改回原数据以后,使得之后的线程误以为对象没有被修改。打一个比方:有一家理发店,搞活动,为会员卡里少于20元的顾客免费充值20元,且每人只有一次机会,而顾客获得免费的20元后,洗一次头发需要10元。
public class AtomicReferenceDemo {
static AtomicReference<Integer> money = new AtomicReference<>();
public static void main(String[] args) {
money.set(19);
for (int i = 0; i < 3; i++) {
new Thread() {
public void run() {
while (true) {
while (true) {
Integer m = money.get();
if (m < 20) {
if (money.compareAndSet(m, m+20)) {
System.out.println("充值20成功,余额:"
+ money.get()
+ Thread.currentThread().getId());
break;
}
} else {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("不需要充值"+money.get());
break;
}
}
}
}
}.start();
}
new Thread() {
public void run() {
for (int i = 0; i < 10; i++) {
while (true) {
Integer m = money.get();
if (m > 10) {
System.out.println("大于10元");
if (money.compareAndSet(m, m - 10)) {
System.out.println("消费10元,余额"+money.get());
break;
}
} else {
System.out.println("钱不够");
break;
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}.start();
}
}
/*
output:
充值20成功,余额:39
大于10元
消费10元,余额29
大于10元
消费10元,余额19
大于10元
消费10元,余额9
不需要充值9
充值20成功,余额:29
......
*/
我们使用AtomicReference来表示钱,先使得余额只有19,我们用了三台充钱机器,然后对其进行充值,但是当顾客消费了两次以后,余额又回到了19元,其他的充钱器误以为这张卡还没有被充值过,于是又充值了一次。就如输出所看到的。如何解决这个问题呢?答案是:AtomicStampedReference
AtomicStampedReference
AtomicReference出现上面问题的根本原因是对象在修改过程中丢失了状态信息,简单的认为数据值没改变,对象也没有被改变。于是,只要我们能记录下对象在修改中的状态值就能解决上述问题了,AtomicStampedReference就是这样做的。
public class AtomicReferenceDemo {
//static AtomicReference<Integer> money = new AtomicReference<>();
static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(19,0);
public static void main(String[] args) {
//money.set(19);
for (int i = 0; i < 3; i++) {
//获得时间戳
final int stamp = money.getStamp();
new Thread() {
public void run() {
while (true) {
while (true) {
Integer m = money.getReference();
if (m < 20) {
//带有时间戳的CAS
if (money.compareAndSet(m, m+20,stamp,stamp+1)) {
System.out.println("充值20成功,余额:"
+ money.getReference()
+ Thread.currentThread().getId());
break;
}
} else {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("不需要充值");
break;
}
}
}
}
}.start();
}
new Thread() {
public void run() {
for (int i = 0; i < 10; i++) {
while (true) {
//获得时间戳
final int stamp = money.getStamp();
Integer m = money.getReference();
if (m > 10) {
System.out.println("大于10元");
////带有时间戳的CAS
if (money.compareAndSet(m, m - 10,stamp,stamp+1)) {
System.out.println("消费10元,余额"+money.getReference());
break;
}
} else {
System.out.println("钱不够");
break;
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
/*
output:
充值20成功,余额:39
大于10元
消费10元,余额29
大于10元
消费10元,余额19
大于10元
消费10元,余额9
不需要充值
不需要充值
不需要充值
钱不够
*/
使用了AtomicStampedReference代替AtomicReference,从输出可以看出,会员卡只充值一次。