首先我们需要了解为什么有CAS机制的存在?那我们就不得不先知道互斥同步和非阻塞同步。
互斥同步
互斥同步面临的主要问题是进行线程阻塞和唤醒带来的性能开销,因此这种同步也叫阻塞同步。互斥同步属于悲观的并发策略,其总是认为只要不做同步措施(加锁),就肯定会出现问题,无论共享数据是否真的会出现竞争,它都会加锁,将会导致用户态到内核态转换、维护锁计时器和检查是否有被阻塞的线程需要被唤醒等开销。
非阻塞同步
基于冲突检测的乐观并发策略,通俗的说就是不管风险,先进行操作。如果没有线程争用共享资源,那就直接操作成功;如果存在线程争用共享资源,,那就进行补偿措施,最常用的补偿措施是不断的重试,直到出现没有竞争为止。这种方式不需要把线程阻塞,因此也叫无锁编程。
非阻塞同步要求操作和冲突检测这两步具备原子性。如果在使用互斥同步来保证就失去了意义,所以我们只能靠硬件来实现这件事,硬件保证某些语义看起来需要多次操作的行为可以只通过一条处理器指令就能完成。在java里用到的是CAS(Compare-And-Swap)操作,比较并交换。
CAS指令需要三个操作数,分别是内存地址V、旧预期值A、准备设置的新值B。
CAS指令执行更新一个变量的时候,只有当旧预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。这样的一个过程属于原子操作(依赖硬件),执行期间不会被其他线程打断。
java中的“原子操作类”就用到了“CAS机制”。
所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。
在这用AtomicInteger来进行举例说明CAS
有这样一段代码没有加任何同步锁,创建20个线程,每个线程对number进行10000次自增1,最后number的结果总是200000,说明了AtomicInteger是原子操作类
public class Test {
static AtomicInteger number = new AtomicInteger(0);
public static void main(String[] args) {
Thread[] threads = new Thread[20];
for (int i = 0; i < 20; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
//每个线程对number进行10000次自增
for (int i = 0; i < 10000; i++) {
number.incrementAndGet();//自增1
}
}
});
threads[i].start();
}
//等待所有累加线程都结束
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(number);
}
}
这一切都要归功于 incrementAndGet()方法
JDK中 incrementAndGet()源码如下
public final int incrementAndGet(){
for(;;){
int current = get();//旧值A
int next = current+1;//新值B
//比较
if(compareAndSet(current,next))
return next;
}
}
incrementAndGet()方法在一个无线循环中,不断尝试将一个比当前值大一的的新值赋值给自己。如果失败,那说明在执行CAS操作时,旧值已经发生改变,于是再次循环进行下一次操作,直到设置成功。
下面我们用2个线程的操作来进行具体解释这一过程:
1.在变量地址V中存放着20
2.此时线程一要将变量的值修改为21,旧值A=20与地址V中的值进行比较发现相等,说明没有其他线程对变量进行修改,可以直接将地址V中的值改为21。
3.此时线程二要将变量的值修改为21,旧值A=20与地址V中的值进行比较发现不相等,说明有其他线程对变量进行了修改,所以不能地址V中的值改为21。
4.线程二进入下一次循环,旧值=21,新值=22。旧值A=21与地址V中的值进行比较发现相等,说明没有其他线程对变量进行修改,可以直接将地址V中的值改为22。
5.最终结果