在多线程并发环境中,多个线程可能同时读取和修改同一个共享变量,这可能会导致数据不一致或竞态条件等问题。由此我们需要用一个“锁”来确保线程不会同时读取或修改同一个变量——例如一个票不能买两次
1、乐观锁与悲观锁
悲观锁
悲观锁认为对于同一个数据的并发操作一定是会发生修改的,采取加锁的形式,悲观地认为,不加锁的并发操作一定会出问题。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中 Synchronized 和 ReentrantLock 等独占锁就是悲观锁思想实现的。
乐观锁
乐观锁正好和悲观锁相反,它获取数据的时候,并不担心数据被修改,每次获取数据的时候也不会加锁,只是在更新数据的时候,通过判断现有的数据是否和原数据一致来判断数据是否被其他线程操作,如果没被其他线程修改则进行数据更新,如果被其他线程修改则不进行数据更新。Lock 是乐观锁的典型实现案例,本文的 CAS 就属于此类。
2、CAS
全称: Compare-And-Swap。它是并发编程中的一种原子操作,通常用于多线程环境下实现同步和线程安全。CAS 操作通过比较内存中的值与期望值是否相等来确定是否执行交换操作。如果相等,则执行交换操作,否则不执行。
原理如图
3、CAS 带来的问题
3.1 ABA 问题
ABA 问题指的是,线程拿到了最初的值 A,然而在将要进行 CAS 的时候,被其他线程抢占了执行权,把此值从 A 变成了 B,然后其他线程又把此值从 B 变成 A,然而此时的 A 值已经并非原来的 A 值了,但最初的线程并不知道这个情况,在它进行 CAS 的时候,就会误认为它从来没有被修改过,只对比了预期原值为 A 就进行了修改,这就造成了 ABA 的问题。
- ABA 的解决
JDK 在 1.5 时提供了 AtomicStampedReference 类也可以解决 ABA 的问题,此类维护了一个“版本号”Stamp,每次在比较时不止比较当前值还比较版本号,这样就解决了 ABA 的问题。
3.2、自旋次数过多
CAS 操作在不成功时会重新读取内存值并自旋重试。当系统的并发量非常高时,导致 CAS 操作失败并不断自旋重试。此时使用 CAS 并不能提高效率,还会造成大量运算资源占用,拖慢运行速率
3.3、只能保护单个共享变量
CAS 只能针对单个共享变量进行原子操作,无法保护多个共享变量之间的一致性。如果多个共享变量之间存在依赖关系,使用 CAS 可能无法满足一致性的要求。
- 解决
将多个变量封入一个对象中进行处理