一、引子
如果我问你在Java语言环境下何时使用CAS机制,你可能会说:出现线程不安全可能性的时候就是我们应当使用CAS机制的时候。但是这个说话虽然是正确的,但是太笼统以至于说了好像没说一样。如果你学过synchronized
关键字,你一定知道同步机制带来的内存上的损耗是很大的,比如频繁的上下文切换就是我们在使用synchronized
关键字时急需避免的。但是如果你了解CAS机制的话,你就会知道此机制有可能会导致线程占据CPU资源,如果在线程安全的条件下仍然使用CAS机制,那么就会带来不必要的CPU资源损耗。
二、何时使用CAS机制
首先给出使用CAS机制的原则:
- 线程之间抢占资源不是特别激烈使用CAS机制,这保证了大部分线程不会是在干等资源的释放
- 等待资源释放时的CPU占用反而小于上下文切换所消耗的资源,使用CAS机制
- 线程可能出现不安全情况的条件下才使用CAS机制
解释:
- CAS机制由于往往和自锁(
for(;;)
)机制相结合使用,所以在自旋机制下,线程竞争越激烈,越多的线程在循环中等待资源释放,而这个过程是占据CPU资源的 - 第二点的内涵是:我们需要确保
synchronized
关键字性能比CAS机制差 - 第三点的解释看似平常,但是却是我们平常不关注的地方,以下我们JDK源代码做解释:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//得到访问锁对象的当前线程对象
int c = getState();//得到当前锁对象的状态
if (c == 0) { //状态为0,意味着没有任何线程占据着当前锁对象
if (compareAndSetState(0, acquires)) {//使用CAS机制将当前锁状态更新,只有一个线程会成功,返回true
setExclusiveOwnerThread(current);//将当前线程置为锁的独占线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//如果当前线程卡位占据锁对象的线程
int nextc = c + acquires;//得到当前线程重入锁后的状态
if (nextc < 0) // overflow//这是锁状态的非法值,如若此值,则抛出异常
throw new Error("Maximum lock count exceeded");
setState(nextc);//调用set方法,更新状态值。
return true;
}
return false;
}
以上代码是java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire
中所定义的当前线程尝试获取资源的方法,可能你还没有学过AQS
机制,Lock
接口,但是通过我上述对代码的注释,相信你应该对这个代码块可以有一个大致的认识。
不知道你有没有注意到一点,上述代码有两处用不同的方法进行锁状态的更新:
if (compareAndSetState(0, acquires))
以及setState(nextc);
但是为何目的都是锁对象状态更新,实现方式却是一个CAS机制,一个普通的set
方法。
原因是上述原则中的第三点:CAS机制使用处可能出现线程不安全情况,而后者却是一定处于线程安全情况。下面来说说具体的判断原因:
- 首先说明上述代码块的锁特性:上述锁结构是一个独占锁,只允许一个线程占据锁资源,但是允许一个线程多次占据锁资源(重入);
- 当锁资源没有被任何线程占据,那么可能出现多个线程同时去抢占锁资源的情况,此时线程显然是不安全的,所以需要使用CAS机制来进行线程安全性的保证,并且多个抢占资源的线程中只有一个线程会抢占到所资源,所以将其放置于
if逻辑判断语句
中,只有成功的线程才会被设置为当前锁对象的独占线程; - 而后者调用普通的set方法原因是:允许重入锁的条件是占据锁资源的线程恰好为当前访问锁对象的线程,这样的线程有且只有一个,那么进行状态更新时,就相当于我们尚未学习多线程知识前单线程的set方法,无须考虑线程不安全性,那么就无须使用CAS机制。
三、小结
从CAS机制使用原则上我们还是可以看出一点,如果能笃定地根据代码逻辑判断出当前代码块是被单线程访问或者执行的,那么我们应当坚决拥护最简单的单线程中的写方法。不是说在学习好多线程知识之后我们在何时何处都应当使用多线程的写方法来保护线程安全性。如果多线程带来线程安全性保障是不必要的,那么多线程导致的额外损耗就是多余。