【Java EE 初阶】锁策略以及CAS问题

本文详细介绍了各种锁策略,包括乐观锁和悲观锁的概念,读写锁的使用场景,以及重量级锁与轻量级锁的区别。讨论了Synchronized的特性,它既是乐观锁也是悲观锁,能实现轻量级和重量级锁,并且是可重入的。此外,文章还提到了CAS操作及其在解决ABA问题上的应用,以及锁消除和锁粗化的优化技术。
摘要由CSDN通过智能技术生成

目录

1.常见的锁策略

1.乐观锁 vs 悲观锁

2.读写锁

3.重量级锁 vs 轻量级锁

4.自旋锁(Spin Lock)

5.公平锁 vs 非公平锁

6.可重入锁 vs 不可重入锁

7.Synchronized实现了哪些锁策略?

1.是乐观锁也是悲观锁

2.既是轻量级锁也是重量级锁

3.是普通互斥锁

4.是非公平锁

5.是可重入锁

6.既是自旋锁也是挂起等待锁

8.CAS(Compare And Swap)

9.基于CAS的应用,原子类

 10.CAS实现自旋锁

11.CAS的ABA问题

1.ABA问题

2.解决ABA问题

12.加锁工作过程

13.锁消除

14.锁粗化


1.常见的锁策略

1.乐观锁 vs 悲观锁

  • 悲观锁: 一开始就会上锁
  • 乐观锁: 假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做

2.读写锁

  • 多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需 要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
  • 读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。

读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock , 实现了读写锁.
  • ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁.
其中,
  • 读加锁和读加锁之间, 不互斥.
  • 写加锁和写加锁之间, 互斥.
  • 读加锁和写加锁之间, 互斥.
注意, 只要是涉及到 "互斥", 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多
久了. 因此尽可能减少 "互斥" 的机会, 就是提高效率的重要途径
读写锁特别适合于 "频繁读, 不频繁写" 的场景

3.重量级锁 vs 轻量级锁

锁的核心特性 "原子性", 这样的机制追根溯源是 CPU 这样的硬件设备提供的.
  • CPU 提供了 "原子操作指令".
  • 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
  • JVM 基于操作系统提供的互斥锁, 实现了 synchronized ReentrantLock 等关键字和类.
重量级锁: 加锁机制重度依赖了 OS 提供了 mutex
  • 大量的内核态用户态切换
  • 很容易引发线程的调度

重量级锁过程

  • 执行加锁操作, 先进入内核态.
  • 在内核态判定当前锁是否已经被占用
  • 如果该锁没有占用, 则加锁成功, 并切换回用户态.
  • 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
  • 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒 这个线程, 尝试重新获取锁.
这两个操作, 成本比较高. 一旦涉及到用户态和内核态的切换, 就意味着 "沧海桑田".
轻量级锁: 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex. .
  • 少量的内核态用户态切换.
  • 不太容易引发线程调度
此处的轻量级锁就是通过 CAS 来实现.
  • 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
  • 如果更新成功, 则认为加锁成功
  • 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).
自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.
因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.
也就是所谓的 "自适应"

4.自旋锁(Spin Lock

  • 如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.
  • 自旋锁是一种典型的 轻量级锁 的实现方式.
  • 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
  • 缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是不消耗 CPU ).

5.公平锁 vs 非公平锁

  • 公平锁: 遵守 "先来后到". B C 先来的. A 释放锁的之后, B 就能先于 C 获取到锁.
  • 非公平锁: 不遵守 "先来后到". B C 都有可能获取到锁.
  • 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要 想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序.
  • 公平锁和非公平锁没有好坏之分, 关键还是看适用场景.

6.可重入锁 vs 不可重入锁

可重入锁的字面意思是可以重新进入的锁,即允许同一个线程多次获取同一把锁
比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入 锁(因为这个原因可重入锁也叫做递归锁
  • Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括
  • synchronized关键字锁都是可重入的。
  • Linux 系统提供的 mutex 是不可重入锁.

7.Synchronized实现了哪些锁策略?

1.是乐观锁也是悲观锁

开始没有线程竞争的时候无锁,当有第一个线程来使用的时候加偏向锁,偏向锁并不是真正的加锁,只是给对象头做一个偏向锁的标记,记录这个锁属于哪个线程,如果后续没有其他线程来争抢锁,那就不用真正的加锁,(避免了加锁解锁的系统开销)并记录当前线程的版本号,当出现两个以上的线程竞争时变为轻量级锁,当越来越多的线程参与到竞争中,自旋不能快速获取到锁状态,此时就会变为重量级锁

2.既是轻量级锁也是重量级锁

轻量级锁是基于自旋锁实现的,重量级锁是基于挂起等待锁实现的

3.是普通互斥锁

4.是非公平锁

5.是可重入锁

6.既是自旋锁也是挂起等待锁

8.CAS(Compare And Swap)

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号

9.基于CAS的应用,原子类

 两个线程通过CAS同时对一个共享变量做自增,通过不停的自旋检查预期值来保证了线程安全

while循环是在应用层执行的,也就是用户态锁比内核态的锁效率要高很多

Compare And Swap 是CPU中的一条指令,可以完成CAS的整个操作(比较并交换),简而言之,是因为硬件予以了支持,软件层面才能做到

 10.CAS实现自旋锁

11.CAS的ABA问题

1.ABA问题

在上述过程中,两个A校验的时候都可以通过,但两个A却并非同一个A,

如果CAS中出现ABA问题,在真实的业务中可能会造成比较大的影响

2.解决ABA问题

给预期值加一个版本号,在做CAS操作的同时更新预期值的版本号,版本号只增不减

CAS 操作在读取旧值的同时, 也要读取版本号真正修改的时候

  • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
  • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)

12.加锁工作过程

JVM synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级

13.锁消除

14.锁粗化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值