无锁是一种乐观的策略,它会假设资源的访问时没有冲突的,没有冲突则不需要等待,所有的线程都可以在不停顿的状态下执行,如果遇到冲突,使用比较交换的技术(CAS)重试当前的操作直到没有冲突为止。
CAS算法过程:它包含三个参数CAS(v,e,n),v表示需要更新的变量,e表示预期的值,n表示新值,仅当v等于e时,才会将v的值设置为n。如果v值和e值不同,则说明已有其他线程做了更新,当前线程什么都不做,最后CAS返回v的真实值。多个线程使用CAS操作一个变量时,只会有一个胜出,也就是更新成功,其他的全部失败,失败的线程不会被挂起,而是再次尝试,当然也允许失败的线程放弃操作。
它是基于处理器原子化的CAS指令
JDK提供了并发包中有一个atomic包,里面 实现了一些直接使用CAS操作的线程安全的类型。
其中AtomicInteger就是一种,它与Integer不同,它是可变的,线程安全的,对其进行修改等操作都是用CAS指令进行的。
内部有一个核心字段private volatile int value;
它表示AtomicInteger对象的实际值,还有一个private static final long valueOffset;
它保存了AtomicInteger对象中的偏移量。
一个小例子:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIn {
static AtomicInteger at = new AtomicInteger();
public static class addThread implements Runnable{
public void run(){
for(int i=0;i<1000;i++){
at.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 addThread());
}
for(int k=0;k<10;k++){
ts[k].start();
}
for(int k=0;k<10;k++){
ts[k].join();
}
System.out.println(at);
}
}
程序中incrementAndGet()方法使用CAS操作自增1,同事也会返回当前值,所以程序的输出为10000。并且经过试验,此方法比synchronized具有更好的性能。那么我们来看一按下你内部的实现,此为JDK1.7的实现,JDK1.8有所不同。
public final int incrementAndGet(){
for(;;){
int current = get();
int next = current+1;
if(compareAndSet(current,next)){
retrun next;
}
}
}
CAS操作未必成功,因此对于不成功的情况就进行不断的尝试。当预期值和预期值相同时直接返回next,否则继续循环重试。
在上面所说的compareAndSet方法为什么有那样的功能呢?请看他的Unsafe类的实现:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这里有一个特殊的变量为unsafe,它是sum.miss.Unsafe
类型,compareAndSwapInt()
是一个navite方法,他有几个参数分别是要操作的变量,offset偏移量(其实就是一个字段到头部的偏移量通过这个偏移量可以快速定位字段),expected期望值以及要设置的值x。而在compareAndSwapInt()方法内部,使用CAS原子命令来完成。
具体请看:Unsafe详解
CAS看起来很爽,但是会导致“ABA问题”。
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。如果链表的头在变化了两次后恢复了原值,但是不代表链表就没有变化。
各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。
举个例子:
public class ABATest {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); //true
}
});
Thread refT1 = new Thread(new Runnable() {
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
}
});
Thread refT2 = new Thread(new Runnable() {
public void run() {
int stamp = atomicStampedRef.getStamp();
System.out.println("before sleep : stamp = " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
System.out.println(c3); //false
}
});
intT1.start();
intT2.start();
intT2.join();
refT1.start();
refT2.start();
}
}
AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败