Java并发 — 关于CAS,乐观锁和悲观锁
一. 写在前面
上一篇文章主要讲解了锁的抽象概念,Java对于锁的实现原理,以及Java6之后对互斥锁的优化,锁的四种状态和变换条件。那么之前也说到过,在无锁状态下如果需要协调线程对于资源的获取,那么不需要对资源进行锁定,也就减少了内核态和用户态的切换,提升了并发性能。但同时其难度较高,无锁编程可以视为对开发者水平的衡量标准之一。不过好在是前人已经准备了相当的类库供我们站在巨人的肩膀上进行开发,那么接下来我们来了解下无锁编程。
二. 关于悲观锁
在面对多线程对同一资源的使用时,很多人的第一反应都是使用互斥锁”synchronized”,但互斥锁的同步方式是“悲观的”,也就是操作系统会悲观地认为如果不严格地同步线程调用,那么一定产生异常。所以互斥锁会将资源锁定只供一个线程调用而阻塞其他线程。但有些情况下大部分操作都是读操作,或者说同步代码块执行耗时远小于线程切换的耗时,所以在这种情况下我们不需要操作系统这么悲观,因此不应该过于依赖互斥锁。
三. 关于乐观锁和CAS
假设有一个资源存在竞争关系,其标识位为0时代表未被线程使用,标识位为1代表已被占用。这时假如有两个线程A和B来使用这个资源,那么如果线程A优先获得了时间片,发现其标识位为0,则将其标识位改为1后直接进行占用。而后来的线程B发现标识位为1了,这时将无法获取资源,但其不会立即返回,而是会进行自旋等待,一旦发现该标志位再次为0,则立即尝试获取该资源,而等待了一段时间后则会结束自旋返回。
以上便是CAS的基本原理,其简单的实现可以解释为以下代码:
// 图片缺失
不过,仔细分析可以发现,这里展示的CAS存在漏洞,一是没有实现自旋等待的逻辑,二是其操作分为了compare和swap两步操作,本身不具备原子性,在多线程下仍然会出现同步问题。这也就是说在线程A发现标志位是0准备进行占用的一瞬间,该资源有可能刚好已被其他线程占用并修改为1,但A并不知情还是将值修改为1,这样就出现了同步问题。也由此我们可以得知,CAS必须是原子性的操作,那么如何保证其原子性呢,难道还需要锁来保证吗,那岂不是陷入了一个循环?好在是各种CPU都提供了指令级的CAS指令,例如x86架构下的cmpxchg,也就是不需要有操作系统的同步原语mutex即可实现CAS了。但这也并不代表无锁能完全代替有锁,这些通过CAS来实现同步的工具,由于不会锁定资源,而且在线程需要修改资源对象时,总是会“乐观地”认为对象值没有被别的线程修改过,而主动尝试compare状态值,相较于之前提到的“悲观锁”,这种同步机制被称为“乐观锁”,但实际这种称呼存在误区,因为实际并没有用到锁,而是一种同步机制。我们接下来看一下JDK中如何实现无锁编程。
JUC中的AtomicInteger类,由著名并发大师Doug Lea完成。可以看到其中有一个Unsafe类和一个offset,注释也明确写了使用其compareAndSwapInt 来进行CAS操作。接着看一下getAndSet方法:
/** * Atomically sets to the given value and returns the old value. * * @param newValue the new value * @return the previous value */ public final int getAndSet(int newValue) { return unsafe.getAndSetInt(this, valueOffset, newValue); } /** * Atomically exchanges the given value with the current value of * a field or array element within the given object o * at the given offset. * * @param o object/array to update the field/element in * @param offset field/element offset * @param newValue new value * @return the previous value * @since 1.8 */ public final int getAndSetInt(Object o, long offset, int newValue) { int v; do { v = getIntVolatile(o, offset); } while (!compareAndSwapInt(o, offset, v, newValue)); return v; }
其实调用了Unsafe的getAndSetInt方法,可以看到这里有个循环,这就是之前提到的自旋,这里不会出现死循环,自旋的次数可以通过启动参数配置,默认为10。之后该方法会进入native方法的调用,并在x86下使用cmpxchg指令完成CAS调用,这部分需要在openjdk中查看。
到这里,相信对于标题的乐观锁,悲观锁和CAS都有了一定的理解了,同时也浅谈了CAS的具体使用和一小部分JUC的源码,虽然是极小的一部分。