【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)

目录

1、锁的策略

1.1、乐观锁和悲观锁 

1.2、轻量级锁和重量级锁

1.3、自旋锁和挂起等待锁

1.4、普通互斥锁和读写锁

1.5、公平锁和非公平锁

1.6、可重入锁和不可重入锁 

2、synchronized 内部的升级与优化过程

2.1、锁的升级/膨胀

2.1.1、偏向锁阶段

2.1.2、轻量级锁阶段

2.1.3、重量级锁阶段

2.2、锁消除

2.3、锁粗化

3、CAS(Compare and swap)

3.1、CAS 的应用

3.1.1、实现 Atomic 原子类

3.1.2、实现自旋锁

3.1.3、CAS 的 ABA 问题



1、锁的策略

加锁过程中,处理冲突的过程中,涉及到的一些不同的处理方式,就叫锁的策略。

1.1、乐观锁和悲观锁 

乐观锁
在加锁之前,预估当前出现锁冲突的概率不大,因此在进行加锁的时候就不会做太多的工作。

由于加锁过程中做的事情比较少,加锁的速度可能就更快,但是更容易引入一些其他的问题(消耗更多cpu资源)。


悲观锁
在加锁之前,预估当前出现锁冲突的概率比较大,因此在进行加锁的时候就会做更多的工作。

由于加锁过程中做的事情更多,加速的速度可能更慢,但是整个过程中不容易出现其他问题

1.2、轻量级锁和重量级锁

轻量级锁
加锁的开销小,加锁的速度更快, 轻量级锁,一般就是乐观锁。


重量级锁
加锁的开销更大,加锁的速度更慢,重量级锁,一般就是悲观锁。

需要注意的是: 

悲观乐观,是在加锁之前,对未发生的事情进行的预估。
轻量重量,是在加锁之后,对结果的评价。
但是整体来说,这两种角度,描述的都是同一个事情。 

1.3、自旋锁和挂起等待锁

自旋锁
就是轻量级锁的一种典型实现,同时也是一种乐观锁。
进行加锁的时候,会搭配一个while循环,如果加锁成功,自然循环结束。
如果加锁不成功,不是阻塞放弃cpu,而是进行下一个循环,再次尝试获取到锁。
上述操作的反复快速执行的过程,就称为“自旋”。一旦其他线程释放了锁,就能第一时间拿到锁。


挂起等待锁
就是重量级锁的一种典型实现,同时也是一种悲观锁(适用于锁冲突激烈的情况)。

1.4、普通互斥锁和读写锁

普通互斥锁
类似于 synchronized 操作涉及的 加锁 和 解锁


读写锁
把加锁分为两种情况: 1)加读锁 ReadLock 2)加写锁 WriteLock
结论:读锁与读锁不会出现锁冲突(不会阻塞),写锁与写锁、读锁与写锁都会出现锁冲突(会阻塞)
这里的差异理解起来也很简单,因为两个线程进行读操作,本身就是线程安全的,所以不需要进行互斥(而且读操作在实际开发中有非常频繁,所以能够大大提升了性能), synchronized 不是读写锁,对于读写锁 JVM 同样封装了 api 给程序员使用。

1.5、公平锁和非公平锁

公平锁
先来后到叫公平,实现公平锁需要引入额外的数据结构(队列,记录先后顺序)。


非公平锁
Java中默认的锁就是非公平锁,正是因为非公平锁导致了“线程饿死”

所谓“线程饿死”,可以理解为:

        当线程A获取锁时,发现此时不能完成实质性的逻辑(取钱时发现atm没钱了),此时只能释放锁并退出,重新与其他线程竞争该锁(刚刚释放锁的线程也会参与到锁竞争中)。有一个极端情况,就是每次释放锁之后又是线程A获取到了锁,并且此时依然无法完成实质性的逻辑,又只能释放锁。就出现【线程A忙了半天不干实事,其他线程都只能干等了】的情况,这就称之为“线程饿死”。
        而这种极端情况出现的概率还挺高的,因为线程A获取锁后处在RUNNABLE状态,而其他线程处在BLOCKED状态,处于BLOCKED状态的线程需要系统唤醒之后才能参与锁竞争,而线程A不需要,因此线程A再次获取到锁的概率比其他的线程高。

1.6、可重入锁和不可重入锁 

针对一个线程一把锁,连续加锁两次不会产生死锁,就是可重入锁,反之就是不可重入锁。系统自带的锁是不可重入锁,Java 的 synchronized 是可重入锁。

2、synchronized 内部的升级与优化过程

Java 中的 synchronized 具有自适应能力。内部会自动评估当前锁冲突的剧烈程。

如果当前锁冲突的剧烈程度不大,就处在 乐观锁/轻量级锁/自旋锁。

如果当前锁冲突的剧烈程度很大,就处在 悲观锁/重量级锁/挂起等待锁。

2.1、锁的升级/膨胀

2.1.1、偏向锁阶段

类似于“懒汉模式”,能不加锁就不加锁,能晚加锁就晚加锁。在遇到竞争的情况下,偏向锁没有提高效率;但是如果在没有竞争的情况下, 偏向锁就大幅度的提高了效率。

.

【只是做了一个标记,没有真加锁(也就不存在互斥),一旦有其他线程想要来竞争锁,就在另一个线程之前先把锁获取到,进而从偏向锁升级到轻量级锁(存在互斥)。】

2.1.2、轻量级锁阶段

通过自旋锁的方式来实现优势:其他线程释放锁后能够第一时间拿到锁;劣势:消耗cpu;

.

此外 synchronized 内部也会统计当前这个锁对象上,有多少个线程产于竞争(都是自旋式竞争),如果发现参与竞争的线程比较多,就会进一步升级到重量级锁。

2.1.3、重量级锁阶段

此时拿不到锁的线程就不会继续自旋,而是进入“阻塞等待”,让出cpu资源。锁被释放后回归随机唤醒机制。

2.2、锁消除

简单来说就是自动干掉不必要的锁。

2.3、锁粗化

简单来说就是把多个细粒度的锁合并成一个粗粒度的锁,减少锁竞争的开销。

以上说的所有机制,都是在内部自动执行的,不需要程序员在编写代码的时候真正的手动执行。

3、CAS(Compare and swap)

CAS(即比较并交换)本身是特殊的单个 cpu 指令,一个 CAS 涉及以下操作:

假设内存中的原数据V,旧的预期值A,需要修改的新值B

1、比较A与V是否相等(比较)

2、如果比较相等,将B写入V(交换)

3、返回操作是否成功

由于 CAS是单个 cpu 指令,使用 CAS 可以“原子”的完成很多复杂操作,不涉及到加锁,也不会阻塞,合理的使用也可以保证线程安全,使用 CAS 的编程方式称为“无锁化编程”,是多线程编程中的特殊技巧。

操作系统对 CAS 指令进行了封装成了 api ,JVM 有对操作系统提供的 api 又封装一层。

3.1、CAS 的应用

3.1.1、实现 Atomic 原子类

Java 标准库中的 Atomic 原子类就是基于 CAS 来实现的。

伪代码描述 CAS 以及 模拟 AtomicInteger 自增过程

3.1.2、实现自旋锁

伪代码描述自旋锁的执行过程

3.1.3、CAS 的 ABA 问题

CAS 使用的时候,关键要点是判定当前内存的值和寄存器中的值是否一样,判定是否一样的操作本质上是在判定这个代码执行的过程中是否有其他线程穿插进来了。

但是这样的判定可能存在这样的可能,本来数值是0,执行 CAS 之前,另一个线程把值从 0 → 100 ,又从 100 → 0 ,虽然改了,但是又改回去了,这种情况就称为 ABA 问题。CAS 的 ABA 问题一般情况下不会有问题,不会产生啥 bug ,但是就怕一些极端情况。

解决方案:
1、约定数据变化是单向的(只能添加或者只能减少),不能是双向的。
2、对于本身就必须双向变化的数据,可以引入一个版本号,版本号是单向增加的。

【博主推荐】 

【Java多线程】对线程池的理解并模拟实现线程池-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136160003?spm=1001.2014.3001.5501【Java多线程】分析线程加锁导致的死锁问题以及解决方案-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136150237?spm=1001.2014.3001.5501【Java多线程】线程安全问题与解决方案-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136133785?spm=1001.2014.3001.5501

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

  • 39
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 23
    评论
### 回答1: 非常感谢您的问题。synchronized锁升级过程Java优化技术之一,它可以减少的竞争,提高程序的执行效率。在Java中,每个对象都有一个(也称为内置或监视器),线程在执行同步代码块时会获取该对象的,并在代码块执行完成后释放该。当多个线程同时访问同一个对象的同步代码块时,就会出现竞争的情况,影响程序的执行效率。 在Java6之前,synchronized的实现存在较大的性能问题,当多个线程竞争同一个时,会导致线程频繁地进入和退出阻塞状态,增加了线程切换的开销,降低了程序的执行效率。为了解决这个问题,Java6引入了偏向、轻量级和重量级优化技术,并采用锁升级的方式在不同之间进行转换,从而提高程序的执行效率。 具体来说,当一个线程第一次进入同步代码块时,会先尝试获取对象的偏向,如果对象没有被其他线程访问过,那么当前线程会获取到该对象的偏向,并将当前线程ID记录在对象头中,以后该线程每次进入同步代码块时都无需重新获取,直接使用偏向即可。如果有其他线程访问了该对象,那么当前线程会尝试将偏向锁升级为轻量级。轻量级是一种自旋,它采用CASCompare And Swap)指令来保证线程的原子性,避免了线程阻塞和唤醒的开销。如果当前线程获取轻量级成功,那么就可以在自旋过程中执行同步代码,避免了线程阻塞和唤醒的开销。如果轻量级获取失败,那么当前线程就会尝试将轻量级锁升级为重量级,此时当前线程会阻塞等待其他线程释放该对象的,直到获取到重量级才能执行同步代码。 总之,synchronized的升级过程Java优化技术之一,它可以提高程序的执行效率,减少的竞争。 ### 回答2: synchronizedJava中用于保证多个线程访问同一个共享资源时的线程安全的关键字。当一个线程想要执行synchronized修饰的同步代码块时,需要先获取对象的,如果被其他线程占用,则当前线程会被阻塞,直到获取到为止。 synchronized锁升级过程分为偏向、轻量级和重量级三个阶段。 首先,在没有竞争的情况下,将被优化为偏向。偏向的作用是,当一个线程获取到时,会在对象的头部标记记录该线程的ID。在下次该线程再次申请时,无需再次进行同步操作,直接获取即可。 然后,如果有多个线程竞争同一个,偏向将转为轻量级。这时每个线程都会在自己的线程栈帧中保存的副本。线程会通过CAS(比较并交换)来进行的获取和释放,而不再阻塞线程。 最后,如果多个线程仍然竞争同一个,轻量级将升级为重量级。重量级的实现是利用操作系统提供的互斥量机制,当一个线程获取后,其他线程将被阻塞,直到持有的线程释放的升级过程多线程环境下进行,根据的状态切换来提高并发效率。通过合理地选择的类型以及的级别,可以更好地平衡性能与安全性之间的关系。 ### 回答3: synchronized锁升级过程是指在Java中保证多线程访问同步代码时的一种优化机制。其主要目的是提高多线程并发访问共享资源时的性能和效率。 当一个线程尝试进入同步代码块时,会先尝试获取对象的无状态。如果成功获取无状态,则可以直接执行同步代码,并将对象标记为偏向。这是的第一级别,也是最轻量级的。如果在此时另一个线程也想要进入同步代码,就会造成竞争。 如果存在竞争,偏向就会升级为轻量级。轻量级是通过在对象头中的标识字段中记录指向线程栈中记录的指针来实现的。如果线程竞争太激烈,轻量级就会升级为重量级。 重量级是指同步代码块被多个线程访问时,会将线程阻塞并等待释放。重量级采用操作系统的互斥量实现,所以比较耗时和耗资源。 在锁升级过程中,的状态会从无状态到偏向,再到轻量级,最后到重量级。在逐步升级的过程中,的开销也会逐渐增加。 需要注意的是,在JDK 6之后,引入了消除和膨胀机制。消除指的是JVM在编译器优化时发现某些代码分支中不存在线程竞争时,会去除相应的操作;膨胀指的是JVM会根据竞争情况,将轻量级锁升级为重量级。 综上所述,synchronized锁升级过程是为了提高多线程并发访问同步代码时的性能和效率。通过从无状态到偏向,再到轻量级,最后到重量级的升级过程JVM可以根据竞争情况选择最适合的状态,以实现最佳的性能和资源利用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hacynn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值