文章目录
- 一、悲观锁
- 二、乐观锁
- 三、乐观锁 CAS 三大问题
一、悲观锁
假设有
个线程 , 线程 A 和 线程 B ; 线程 A 访问共享资源 , 线程 B 等待 , 一旦线程 A 访问结束 , 线程 B 访问该共享资源 ;
悲观锁 : 只要有 线程 来操作 共享资源 , 就认为肯定 有其它若干线程也要操作该共享资源 , 一定要 对共享资源进行加锁 ; 任何情况下 , 哪怕 只有一个线程访问共享资源 , 也要对该共享资源进行加锁 ; ( 持有悲观的态度 )
对共享资源加锁 , 会对该资源产生负面影响 , 效率会降低 ;
如果只是 单线程访问资源 , 不会产生并发问题 , 没有必要进行加锁 ; 如果加了锁 , 执行效率变低 , 造成了资源浪费 ;
synchronized 就是悲观锁 ;
二、乐观锁
乐观锁 : 持有乐观的态度 , 线程 A 访问共享资源 , 乐观的认为只有
个线程访问该资源 , 不用进行加锁 ;
线程 A 访问主内存变量前 , 记录下值
, 线程 A 访问完毕后 , 会将最终的值同步到主内存中 , 此时会检查下 主内存中变量的值是否还是
,
- 如果是 , 则说明 线程 A 访问期间 没有线程修改该变量值 , 那么将线程 A 计算的 新值更新到主内存中
- 如果不是 , 主内存中的变量值变成了
, 那么说明该值 被其它线程修改了 ; 那么将当前值抛弃 , 重新从主内存获取变量值
, 然后线程 A 继续执行 , 执行完毕后将计算结果同步到主内存变量中 , 再次对比主内存中的变量值是否是
, 如果是可以更新 , 如果不是 , 那么再次重复本操作 ;
乐观锁 , 全程没有加锁 , 没有阻塞 , 只要判定主内存中被访问的共享变量 , 线程计算之前的值与计算之后的值一致 , 就更新到主线程中 ;
三、乐观锁 CAS 三大问题
CAS : Compare and Swap , 比较再交换 , 是乐观锁中的线程访问完共享变量后 , 先进行变量比较 , 然后在同步共享变量值 ;
JDK
之后提供的 java.util.concurrent
包中的类 , 解决了 CAS 相关问题 ;
java.util.concurrent 包简称 J.U.C ;
CAS 解决的
大问题 :
ABA 问题 :
- 问题描述 : 线程 A 访问变量 X = 0 , 访问期间 , 线程 B 访问 X 将其改为 1 , 然后 线程 C 访问 X 将其又改为 0 , 此时线程 A 访问完毕后 , 查询发现变量 X 仍然是 0 , 认为期间没有线程访问该变量 ;
- 解决方案 : 给变量设置一个版本号 , 每次线程访问变量时 , 版本号 1 , 这样每次判断变量的版本号即可 ;
循环时间过长 :
- 问题描述 : 乐观锁中 , 假如连续多次写回数据时 , 发现值改变 , 校验失败 , 导致 重复执行线程代码 , 会给 CPU 带来很大开销 , 这些 CPU 时间片都浪费了 ;
- 解决方案 : 将锁升级 ;
保证多个共享变量原子性问题 :
- 问题描述 : 针对 单个共享变量 访问时 , 使用 atomic 原子类 可以使用 CAS 保证原子操作 , 如果 有多个共享变量 , CAS 无法保证操作的原子性 ;
- 解决方案 : 使用 JDK 提供的 AtomicReference 封装多个变量到一个类对象中 , 保证共享变量的原子性 ;
线程 A 访问共享变量的操作 , 不是原子操作 , 就会导致如下问题 :
线程 A 访问变量 X , 执行完毕后 , 变量 X 值原始值进行比较 , 比较相等 , 将数据更新到主内存 , 如果在 比较相等后 , 在 数据更新到主内存之前 , 有 另外一个线程 B 修改了该变量 X , 这样就出现了问题 ;