Skip to content

CAS和synchronized介绍,两者存在什么不同

CAS(Compare-and-Swap)的实现思路

CAS是一种非阻塞(non-blocking)的乐观并发策略。它基于硬件层面的原子指令,旨在在不阻塞线程的情况下实现数据更新的原子性。

  1. 核心思想:CAS操作包含三个基本参数:

    • 内存位置(V):要操作的变量在内存中的地址。
    • 预期原值(A):认为V当前应该具有的值。
    • 新值(B):如果V等于A,则要更新为的新值。 CAS的逻辑是:如果内存位置V的值与预期原值A相等,那么就将V的值更新为新值B;否则,不进行任何操作。这个“比较并交换”的整个过程是由硬件保证原子性的。
  2. 硬件支持:Java中的CAS操作依赖于现代处理器提供的底层硬件指令。例如,在Intel x86架构上,这对应于CMPXCHG(Compare Exchange)指令;在ARM架构上,则是LDREX/STREX(Load-Exclusive / Store-Exclusive)指令对。这些指令能够确保在多处理器环境下,比较和交换操作的原子性,即不会被其他线程或中断打断。

  3. Java中的实现

    • sun.misc.Unsafe:Java通过内部的sun.misc.Unsafe类来直接访问和操作内存,并调用底层的CAS指令。这个类提供了如compareAndSwapIntcompareAndSwapLong等方法。Unsafe类通常不建议直接在业务代码中使用,因为它可能破坏JVM的安全性。
    • java.util.concurrent.atomic:为了方便开发者,Java在java.util.concurrent.atomic包中提供了AtomicIntegerAtomicLongAtomicReference等原子类。这些类内部封装了Unsafe类的CAS操作,提供了一系列线程安全的原子操作方法(如incrementAndGet()compareAndSet()),使得开发者无需关注底层细节。
    • 自旋(Spin-locking):当CAS操作失败时(即V不等于A),意味着变量已被其他线程修改。CAS并不会阻塞当前线程,而是让线程进行“自旋”,即在一个循环中反复尝试执行CAS操作,直到成功为止。这个过程完全在用户态完成,避免了线程上下文切换的开销。

synchronized 的实现思路

synchronized是Java语言层面的关键字,它提供了一种阻塞(blocking)的悲观并发策略。synchronized通过对象监视器(Object Monitor)来实现线程同步,确保同一时间只有一个线程能够执行特定的代码块或方法。

  1. 对象监视器(Monitor):Java中的每个对象都可以作为锁。当synchronized修饰一个方法或代码块时,它会关联到一个对象的监视器。这个监视器在JVM的C++实现中对应于ObjectMonitor

  2. 锁的获取与释放

    • 当一个线程进入synchronized修饰的代码块或方法时,它会尝试获取关联对象的监视器锁。
    • 如果锁可用,线程就获取锁并进入临界区执行代码。其他试图获取同一锁的线程将被阻塞(暂停执行),并被放入等待队列,直到持有锁的线程释放锁。
    • 锁在synchronized代码块或方法执行完毕(正常退出或抛出异常)后自动释放。
  3. JVM锁优化(Lock Escalation):为了提高synchronized的性能,JVM对其进行了大量的优化,根据竞争程度,锁会经历从低到高的升级过程:

    • 偏向锁(Biased Locking):在几乎没有竞争的情况下,锁会偏向于第一个获取它的线程。该线程再次进入同步块时,无需任何同步操作,只需检查Mark Word中的偏向ID即可,完全在用户态完成。
    • 轻量级锁(Lightweight Locking):当有多个线程交替访问同一把锁,但不存在真正的并发竞争时,偏向锁会升级为轻量级锁。线程会在自己的栈帧中建立锁记录(Lock Record),并通过CAS操作尝试将对象的Mark Word更新为指向该锁记录的指针。如果CAS成功,则获得轻量级锁;如果失败,线程会进行短时间的自旋,尝试再次获取锁。这个阶段也主要在用户态通过CAS和自旋完成。
    • 重量级锁(Heavyweight Locking):当竞争激烈,轻量级锁的自旋多次后仍无法获取锁时,锁会升级为重量级锁。此时,synchronized的实现会依赖于操作系统的互斥量(Mutex)。未获取到锁的线程会被操作系统挂起(阻塞),并从运行状态切换到等待状态。线程的挂起和唤醒涉及到用户态到内核态的切换,这是开销较大的操作。
  4. 内存可见性synchronized除了保证原子性外,还保证了内存可见性。当一个线程释放synchronized锁时,它所做的所有修改都会被刷新到主内存中;当一个线程获取synchronized锁时,它会从主内存中读取共享变量的最新值。这符合Java内存模型的“happens-before”原则。

特性 CAS (Compare-and-Swap) synchronized
并发策略 乐观并发:假设不会发生冲突,尝试更新,失败则重试(非阻塞)。 悲观并发:假设会发生冲突,先获取锁,独占访问(阻塞)。
实现机制 基于硬件原子指令,通过Unsafe类和Atomic类实现。 基于对象监视器(Monitor),JVM层面实现,依赖操作系统互斥量(重量级锁)。
阻塞性质 非阻塞:操作失败时线程自旋重试,不会被挂起。 阻塞:竞争失败的线程会被挂起(重量级锁),进入等待状态。
用户态/内核态 主要在用户态完成,极少涉及内核态切换。 低竞争时(偏向/轻量级锁)在用户态完成;高竞争时(重量级锁)涉及频繁的用户态与内核态切换
锁粒度 通常用于对单个共享变量的原子操作,粒度较细。 可以保护一个代码块、方法或整个对象,粒度较粗。
适用场景 适用于对单个共享变量进行高效原子更新,是实现无锁数据结构的基础。 适用于保护更大范围的临界区,或涉及复杂逻辑的同步。
“死循环”风险 高竞争时可能导致长时间自旋(CPU空转),浪费CPU资源。 竞争失败的线程会被挂起,不会空转,但存在线程上下文切换开销。
公平性 通常是非公平的,自旋的线程可能立即成功。 默认是非公平的,但可以通过ReentrantLock实现公平锁。
可见性 CAS指令通常包含内存屏障,保证内存可见性。 锁的获取和释放隐式地保证内存可见性。

性能上的差别

CAS和synchronized的性能差异是一个关键点,它高度依赖于具体的应用场景、竞争程度以及JVM的优化水平。理解其性能差异,特别是与用户态和内核态的关联,至关重要。

  1. 用户态与内核态的开销

    • 用户态:应用程序运行的环境,权限受限。
    • 内核态:操作系统内核运行的环境,拥有最高权限,可以访问所有硬件资源。
    • 上下文切换(Context Switch):当操作系统将CPU从一个进程/线程切换到另一个进程/线程时,需要保存当前进程/线程的状态,然后加载下一个进程/线程的状态。这个过程是昂贵的,尤其是用户态与内核态之间的切换,因为这涉及到特权指令的执行、寄存器保存/恢复、地址空间切换等。
  2. 低竞争(Low Contention)情况下的性能

    • CAS更优:在低竞争甚至无竞争的场景下,CAS的性能通常优于synchronized。CAS操作仅仅是执行一个简单的硬件原子指令,它完全在用户态完成,不会导致线程阻塞和上下文切换的开销。
    • synchronized的优化效果:JVM对synchronized的优化(偏向锁和轻量级锁)使得其在低竞争时开销也变得非常小。
      • 偏向锁:无需任何CAS,仅检查对象头,开销几乎为零,完全在用户态。
      • 轻量级锁:虽然使用了CAS操作,但同样在用户态进行,并且在成功获取锁后,后续操作也避免了更昂贵的同步原语。线程可能进行短时间自旋,但如果很快获取到锁,则开销很小。
    • 总结:在低竞争时,CAS通常表现出更好的性能,因为它完全避免了传统锁机制的开销。而synchronized经过JVM优化后,在低竞争时性能也相当接近,甚至可以忽略不计差异。两者都在用户态高效完成。
  3. 高竞争(High Contention)情况下的性能

    • CAS的劣势(自旋开销与CPU空转):在高竞争环境下,CAS操作可能会反复失败并进行自旋重试。
      • 每次CAS失败,线程都会再次尝试,这个“忙等”过程会持续消耗CPU资源,但线程却没有实际进展,导致CPU空转
      • 如果临界区代码执行时间较长,或者竞争非常激烈,CAS的自旋开销可能远大于线程阻塞和唤醒的开销,从而导致整体性能下降,甚至比synchronized更差。CPU资源的浪费会拖慢整个系统的吞吐量。
      • 这个自旋过程虽然在用户态完成,避免了上下文切换,但无效的CPU时间消耗反而成了瓶颈。
    • synchronized的优势(线程挂起与资源让渡):在高竞争环境下,synchronized的锁会升级为重量级锁。
      • 此时,未获取到锁的线程会被操作系统挂起(阻塞),不再消耗CPU资源进行自旋。CPU会从用户态切换到内核态,由操作系统负责将线程放入等待队列。
      • 当持有锁的线程释放锁时,操作系统会再次从内核态唤醒等待队列中的一个或多个线程,这同样涉及到用户态和内核态的切换。
      • 虽然用户态到内核态的切换以及线程的挂起和唤醒是昂贵的操作(上下文切换),但它会将CPU时间片让给其他有用的任务,而不是进行无谓的自旋。因此,在高竞争且临界区较长的情况下,synchronized通过阻塞线程来避免CPU空转,可能表现出更好的吞吐量和资源利用率
  4. 其他性能考量

    • 缓存一致性(Cache Coherency):无论是CAS还是synchronized,都需要保证多核CPU缓存之间的数据一致性。频繁的跨核数据共享和修改会导致缓存线(cache line)失效,引发缓存一致性协议(MESI等)的开销,这也会影响性能。
    • 实现复杂度:从工程实践角度,对于复杂的并发逻辑,使用synchronizedjava.util.concurrent.locks.Lock等显式锁通常更容易理解和证明其正确性。无锁算法(lock-free algorithms)虽然在理论上性能潜力大,但基于CAS实现起来非常复杂,容易出错,且调试困难。