我们知道锁的实现可以分为乐观锁和悲观锁,具体可以参照我的这篇文章数据库的锁机制及原理。java中也有对应的乐观锁和悲观锁的实现,在之前的文章中我们讨论了ReentrantLock和synchronized,它们都是悲观锁的具体实现,都是先确保拿了锁才会去操作。java中同样也有乐观锁的实现,这就是CAS(compareAndSwap)机制。
锁的劣势
- 如果锁已经被占用,那么其他线程必须被挂起。当线程恢复执行的时候,必须等待其他线程执行完它们的时间片,才能被调度执行。在挂起和恢复线程等过程中存在着很大的开销。
- 当一个线程正在等待锁时,它不能做任何其他事情。如果一个线程在持有锁的情况下被延迟执行(比如发生缺页错误、调度延迟),那么所有需要这个锁的线程都将无法执行下去。而且会发生优先级反转,即优先级高的线程仍然需要等待,导致它的优先级降低。
CAS实现
CAS是一种乐观锁的实现,需要接触冲突检查机制来判断在更新过程中是否存在其他线程的干扰。如果存在,那么这个操作将失败,并且可以重试。具体来讲,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
int A;
do {
A = value.get();
}
while (V != value.compareAndSwap(A,B));
CAS的问题
-
ABA问题。
ABA是一种异常现象,出现异常的点在于,每次去比较内存值V和旧的预期值A是否相等,如果相等我们就判定了在此之前V的值没有改变,但事实并不是这样,很有可能V的值经历了A-B-A的变化而我们没有觉察到。
对于ABA问题有一个相对简单的解决方案:不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值由A变为B,然后又变为A,版本号也是不同的。java中
AtomicStampedReference
和AtomicMarkableReference
支持在两个变量上执行原子的条件更新,实现版本号控制。 -
活锁问题。循环等待时间开销大
所谓活锁,就是线程一直处于running的状态然而却在做无用功,比如while循环在尝试更新,然而每次重试都是失败的,导致线程的其他任务得不到执行。
对于活锁问题的解决可以在重试机制中引入一定的随机量。
原子变量
java中AtomicInteger等等原子类型就是利用了CAS实现。看源码。
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
再看unsafe.getAndAddInt
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
果然实现的代码和我们之前的模版一致。
我们再来看原子变量和一般锁的性能比较。在中低程度的竞争下,原子变量能够提供更高的可伸缩性,而在高强度的竞争下,锁能够有效的避免竞争。(摘自《java并发编程实战》Page-269)
可伸缩性是指,当增加计算资源时(例如CPU,内存、存储容量或IO带宽),程序的吞吐量或者处理能力能相应的增加。
这里有一个特别需要注意的点:原子的标量类并没有扩展基本的包装类,例如Integer。基本类型的包装类是不可修改的,而原子变量是可修改的。在原子变量中同样没有重新定义hashcode
和equals
方法,每个实例都是不同的,所以它们也不能用来做基于散列的容器中的键值。