Skip to main content

乐观锁

David LiuAbout 3 min

乐观锁

代表:AtomicInteger,使用 cas(Unsafe)来保证原子性

  • 核心是:无需加锁,每次只有一个线程能够成功修改共享变量,其他失败的线程不需要停止,不断重复直至成功
  • 由于线程一直运行,不需要阻塞,因此不涉及线程上下文切换
  • 它需要多核 cpu 支持,且核心数不应超过 cpu 核数

缺点:

  • 只能保证一个变量的原子操作
  • 解决:AtomicReference

1、乐观锁:假定没有冲突,在更新数据时比较发现不一致时,则读取新值修改后重试更新。(自旋锁就是一种乐观锁)

3、自旋锁:循环使用 cup 时间,尝试 cas 操作直至成功返回 true,不然一直循环。(比较内存值与线程旧值是否一致,一致则更新,不然则循环)

CAS 算法

问题:

  1. ABA 问题

    解决:时间戳/版本号,如 AtomicStampedReference

    如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。

    ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。JDK 1.5 以后的 AtomicStampedReference类就是用来解决 ABA 问题的,其中的 compareAndSet() 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

  2. 循环时间长开销大

    CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。

    解决:CLH 降低粒度,或 AQS 改为阻塞

  3. 只能保证一个共享变量的原子操作

CAS 涉及到三个操作数:

  • V :要更新的变量值(Var)
  • E :预期值(Expected)
  • N :拟写入的新值(New)

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。

当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

Atomic