0


乐观锁(CAS)和悲观锁(synchronized)的详细介绍

1. 锁的定义

在代码中多个线程需要同时操作共享变量,这时需要给变量上把锁,保证变量值是线程安全的。
锁的种类非常多,比如:互斥锁、自旋锁、重入锁、读写锁、行锁、表锁等这些概念,总结下来就两种类型,乐观锁和悲观锁。

2.乐观锁

乐观锁就是持比较乐观态度的锁。在操作数据时非常乐观,认为别的线程不会同时修改数据,只有到数据提交的时候才通过一种机制来验证数据是否存在冲突。一般使用CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。

3.悲观锁

比较悲观的锁,总是想着最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。在Java中,synchronized从偏向锁、轻量级锁到重量级锁,全是悲观锁。JDK提供的Lock实现类全是悲观锁。一般用于多写的场景。

4.CAS的介绍

CAS 即 Compare and Swap,它体现的一种乐观锁的思想,比如:多个线程要对一个共享的整型变量执行 +1 操作:

  1. // 需要不断尝试
  2. while (true) {
  3. int 旧值 = 共享变量; // 比如拿到了当前值 0
  4. int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1
  5. /*这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候compareAndSwap 返回 false,重新尝试,
  6. 直到:compareAndSwap 返回 true,表示本线程做修改的同时,别的线程没有干扰*/
  7. if( compareAndSwap ( 旧值, 结果 )) {
  8. // 成功,退出循环
  9. }
  10. }

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。结合 CAS 和 volatile 可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令。

java.util.concurrent中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、 AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的。

java的内存模型(可见性,原子性,有序性)详细介绍_傻鱼爱编程的博客-CSDN博客这个里面原子性问题,用CAS技术解决方案如下:

  1. // 创建原子整数对象
  2. private static AtomicInteger i = new AtomicInteger(0);
  3. public static void main(String[] args) throws InterruptedException {
  4. Thread t1 = new Thread(() -> {
  5. for (int j = 0; j < 1000; j++) {
  6. i.getAndIncrement(); // 获取并且自增 i++
  7. }
  8. });
  9. Thread t2 = new Thread(() -> {
  10. for (int j = 0; j < 1000; j++) {
  11. i.getAndDecrement(); // 获取并且自减 i--
  12. }
  13. });
  14. t1.start();
  15. t2.start();
  16. t1.join();
  17. t2.join();
  18. System.out.println(i);
  19. }

5. synchronized的介绍

Java HotSpot 虚拟机中,每个对象都有对象头(包括 class 指针和 Mark Word)。Mark Word 平时存储这个对象的哈希码 、分代年龄,当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指针 、重量级锁指针 、线程ID 等内容。

JDK5引入了CAS原子操作,从JDK6开始对synchronized的实现机制进行了各种优化,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁(默认开启偏向锁)这些优化策略。由于此关键字的优化使得性能极大提高,同时语义清晰、操作简单、无需手动关闭,推荐在允许的情况下尽量使用此关键字。

JDK6以后锁主要存在四种状态,依次是:**无锁状态(对象头中存储01)、偏向锁状态(对象头中存储线程id)、轻量级锁状态(对象头中存储00)、重量级锁状态(对象头中存储10)**,锁的升级是单向的。

5.1 偏向锁

Java6中引入了偏向锁来做优化:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。

偏向锁的缺点:

  1. 撤销偏向需要将持锁线程升级为轻量级锁,这个过程中所有线程需要暂停(STW)
  2. 访问对象的 hashCode 也会撤销偏向锁

3.撤销偏向和重偏向都是批量进行的,以类为单位,如果撤销偏向到达某个阈值,整个类的所有对象都会变为不可偏向的

开启偏向锁(默认开启):-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
关闭偏向锁:-XX:-UseBiasedLocking

举个例子分析一下:

假设有两个方法同步块,利用同一个对象加锁

  1. Object ob = new Object();
  2. public void method1() {
  3. synchronized(ob) {
  4. // 同步块 A
  5. method2();
  6. }
  7. }
  8. public void method2() {
  9. synchronized(ob) {
  10. // 同步块 B
  11. }
  12. }

解释过程如下:

5.2 轻量级锁

若偏向锁失败,它会尝试使用轻量级锁的优化手段,此时Mark Word 的结构也变为轻量级锁的结构。如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(也就是没有竞争),如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

轻量级锁竞争的时候,用自旋进行了优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。Java 7 之后不能控制是否开启自旋功能。

举个例子分析一下:

假设有两个方法同步块,利用同一个对象加锁

  1. Object ob = new Object();
  2. public void method1() {
  3. synchronized(ob) {
  4. // 同步块 A
  5. method2();
  6. }
  7. }
  8. public void method2() {
  9. synchronized(ob) {
  10. // 同步块 B
  11. }
  12. }

解释过程如下: (每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的 Mark Word)

5.3 重量锁

如果在尝试加轻量级锁的过程中,CAS 操作无法成功(经过自旋),这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

标签: java 开发语言 jvm

本文转载自: https://blog.csdn.net/m0_57640408/article/details/127417615
版权归原作者 傻鱼爱编程 所有, 如有侵权,请联系我们删除。

“乐观锁(CAS)和悲观锁(synchronized)的详细介绍”的评论:

还没有评论