1. 悲观锁和乐观锁的基本概念
悲观锁:
总是认为当前想要获取的资源存在竞争(很悲观的想法), 因此获取资源后会立刻加锁, 于是其他线程想要获取该资源的时候就会一直阻塞直到能够获取到锁;
在传统的关系型数据库中, 例如行锁, 表锁, 读锁, 写锁等, 都用到了悲观锁. 还有 java 中的同步关键字 Synchronized 也是一种悲观锁;
乐观锁:
总是认为当前想要获取的资源不存在竞争(很乐观的想法), 因此在获取资源后, 并不会加锁;
但是在执行更新操作时, 会判断在这期间是否有其他人更新过这个数据, 可使用版本号等机制实现;
适用于多读的应用程序, 可提高吞吐量;
像数据库提供的类似于 write_condition 机制, 其实都是提供的乐观锁. 在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的.
2. 乐观锁的一种实现方式: CAS
因为乐观锁的思想是: 在通常情况下都认为不会产生并发冲突, 因此在对数据进行提交更新的时候, 会对将要提交更新的数据进行并发冲突检测, 如果冲突存在, 则会返回错误信息给用户, 让用户决定处理方式.
基于乐观锁的思想, 我们可以知道乐观锁实现的步骤包含两个部分: 冲突检测和数据更新, 而 CAS 就是其中一个典型的实现方式.
CAS:Compare And Swap(比较并交换)
CAS 是一种乐观锁技术. 当多个线程使用 CAS 尝试更新同一个变量时, 只有一个线程能够成功更新, 其他线程都会失败, 但是失败的线程并不会挂起, 而是被告知在此次竞争中失败并可再次尝试.
CAS 包含三个操作数:
在 JDK1.5 中新增的 java.util.concurrent 包中的内容就是建立早 CAS 基础之上的, 相对于 Synchronized 的阻塞式算法, CAS 其实是一种非阻塞算法的实现, 因此 java.util.concurrent 包中组件的性能大大提升.
下面以 java.util.concurrent 中的 AtomicInteger 的 getAndIncrement(该操作相当于变量自加) 为例, 看一下在不加锁的情况下, 如何保证线程安全:
- public class AtomicInteger extends Number implements java.io.Serializable {
- private volatile int value;
- public final int get() {
- return value;
- }
- public final int getAndIncrement() {
- // 自旋方式采用 CAS 来修改当前值, 直到成功为止
- for (;;) {
- int current = get();
- int next = current + 1;
- if (compareAndSet(current, next))
- return current;
- }
- }
- public final boolean compareAndSet(int expect, int update) {
- return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
- }
- }
如果 compareAndSet(current, next)方法成功执行, 则直接返回; 如果线程竞争激烈, 导致 compareAndSet(current, next)方法一直不能成功执行, 则会一直循环等待.
3.CAS 存在的问题
1.ABA 问题. 因为 CAS 需要在操作值的时候检查下值有没有发生变化, 如果没有发生变化则更新, 但是如果一个值原来是 A, 变成了 B, 又变成了 A, 那么使用 CAS 进行检查时会发现它的值没有发生变化, 但是实际上却变化了. ABA 问题的解决思路就是使用版本号. 在变量前面追加上版本号, 每次变量更新的时候把版本号加一, 那么 A-B-A 就会变成 1A-2B-3A.
2. 循环时间开销大: 因为 CAS 中存在自旋, 当自旋长时间不成功时, 会给 CPU 带来极大开销, 如果 CPU 执行支持 pause 指令, 效率能够得到提升.
pause 指令作用 1: 延迟流水线执行指令,(de-pipeline), 使 CPU 不会消耗过多的执行资源, 延迟的时间取决于具体实现的版本, 在一些处理器上延迟时间是零.
pause 指令作用 2: 可以避免在退出循环的时候因内存顺序冲突 (memory order violation) 而引起 CPU 流水线被清空(CPU pipeline flush), 从而提高 CPU 的执行效率.
3. 只能保证一个共享变量的原子操作. 当对一个共享变量执行操作时, 我们可以使用循环 CAS 的方式来保证原子操作, 但是对多个共享变量操作时, 循环 CAS 就无法保证操作的原子性, 这个时候就可以用锁, 或者有一个取巧的办法, 就是把多个共享变量合并成一个共享变量来操作. 比如有两个共享变量 i=2,j=a, 合并一下 ij=2a, 然后用 CAS 来操作 ij. 从 Java1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性, 你可以把多个变量放在一个对象里来进行 CAS 操作.
4.CAS 与 Synchronized 的选择
1, 线程冲突严重时, 使用 CAS 等乐观锁, 自旋几率较大, 会因为自旋浪费更多的 CPU 资源; 此时使用 Synchronized 等悲观锁性能较好.
2, 线程冲突较轻时, 使用 synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 CPU 资源, 而自旋概率较小, 使用 CAS 性能高于同步锁.
来源: https://www.cnblogs.com/LearnAndGet/p/9768795.html