偏向锁/轻量级锁/重量级锁
重量级锁会造成 CPU 在用户态和核心态之间频繁切换,所以代价高、效率低。JDK1.6 版本为了减少获得锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”实现。所以,在 JDK1.6 版本里内置锁一共有四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这些状态随着竞争情况逐渐升级。内置锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种能升级却不能降级的策略,其目的是为了提高获得锁和释放锁的效率。
1. 无锁状态
2. 偏向锁状态
3. 轻量级锁状态
4. 重量级锁状态
轻量级锁的很总要一个实现基础就是CAS操作(自旋):
CAS(Compare and swap),即比较并交换,也是实现我们平时所说的自旋锁或乐观锁的核心操作。
- 执行函数:
CAS(V,E,N)
其包含3
个参数
- V 表示要更新的变量
- E 表示预期值
- N 表示新值
如何要更新的变量等于预期值,就把新值赋值给变量,如何要更新的变量不等于预期值,就CAS
再重新试一下,再试的时候,会重新读取要更新的变量作为预期值
比方说:
当前的这个线程想改这个值,我期望你是0
,你就不能是1
;如果是1
,那就说明我这个值不对,然后想把你变成1
。大概就是:原来这个值是变为3
了,我这个线程想修改这个值的时候我一定期望你现在是3
,是3
我才改,如果在我修改的过程你变4
了,说明就有另外一个线程修改过该值,那我cas
就再重新试一下,再试的时候,我希望你的这个值是4
,在修改的时候期望值是4
,没有其它线程修改该值,那好我给你改成5
,这样就是cas
操作。
CAS 实现自旋锁
既然用锁或 synchronized 关键字可以实现原子操作,那么为什么还要用 CAS 呢,因为加锁或使用 synchronized 关键字带来的性能损耗较大,而用 CAS 可以实现乐观锁,它实际上是直接利用了 CPU 层面的指令,所以性能很高。
上面也说了,CAS 是实现自旋锁的基础,cas
是cpu
的原语支持,也就是说cas
是cpu
指令级别上的支持,中间不能被打断,不会造成所谓的数据不一致问题,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转,翻译成人话就是循环,一般是用一个无限循环实现。这样一来,一个无限循环中,执行一个 CAS 操作,当操作成功,返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。
其实 JDK 中有好多地方用到了 CAS ,尤其是 java.util.concurrent
包下,比如 CountDownLatch、Semaphore、ReentrantLock 中,再比如 java.util.concurrent.atomic
包下,相信大家都用到过 Atomic* ,比如 AtomicBoolean、AtomicInteger 等。
这里拿 AtomicBoolean 来举个例子,因为它足够简单。
public class AtomicBoolean implements java.io.Serializable {
private static final long serialVersionUID = 4654671469794556979L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicBoolean.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final boolean get() {
return value != 0;
}
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u); //这里cas,会触发cpu指令
}
}
ABA问题
CAS 存在一个问题,就是一个值从 A 变为 B ,又从 B 变回了 A,这种情况下,CAS 会认为值没有发生过变化,但实际上是有变化的。对此,并发包下倒是有 AtomicStampedReference 提供了根据版本号判断的实现,可以解决一部分问题。