【JavaEE初阶】常见的锁策略及synchronized实现原理

目录

🌳 常见的锁策略

🚩 乐观锁 vs 悲观锁

🚩 重量级锁 vs 轻量级锁

🚩 自旋锁 vs 挂起等待锁

🚩 可重入锁 vs 不可重入锁

🚩 公平锁 vs 非公平锁

🚩 互斥锁 vs 读写锁

🎄 相关面试题

🍀 synchronized 实现原理

🚩 锁升级

🙂 无锁

🙂 偏向锁

🙂 轻量级锁

🙂 重量级锁

🚩 锁消除

🚩 锁粗化

面试题


🌳 常见的锁策略

接下来讲解的锁策略不仅仅是局限于 Java . 任何和 “锁” 相关的话题, 都可能会涉及到以下内容. 这些特性主要是给锁的实现者来参考的.

普通的程序猿也需要了解一些, 对于合理的使用锁也是有很大帮助的

所说的策略,是这把锁,在加锁/解锁/遇到锁冲突的时候,都会怎么做,"策略"可以理解为"做法"。

🚩 乐观锁 vs 悲观锁

举个栗子: 同学 A 和 同学 B 想请教老师一个问题.

同学 A 认为 “老师是比较忙的, 我来问问题, 老师不一定有空解答”. 因此同学 A 会先给老师发消息: “老师你忙嘛? 我下午两点能来找你问个问题嘛?” (相当于加锁操作) 得到肯定的答复之后, 才会真的来问问题.如果得到了否定的答复, 那就等一段时间, 下次再来和老师确定时间. 这个是悲观锁.

同学 B 认为 “老师是比较闲的, 我来问问题, 老师大概率是有空解答的”. 因此同学 B 直接就来找老师.(没加锁, 直接访问资源) 如果老师确实比较闲, 那么直接问题就解决了. 如果老师这会确实很忙, 那么同学 B也不会打扰老师, 就下次再来(虽然没加锁, 但是能识别出数据访问冲突). 这个是乐观锁

注意:

这两种思路不能说谁优谁劣, 而是看当前的场景是否合适.

如果当前老师确实比较忙, 那么使用悲观锁的策略更合适, 使用乐观锁会导致 “白跑很多趟”, 耗费额外的资源.

如果当前老师确实比较闲, 那么使用乐观锁的策略更合适, 使用悲观锁会让效率比较低

在Java中,Synchronized 关键字初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略

🚩 重量级锁 vs 轻量级锁

🚩 自旋锁 vs 挂起等待锁

自旋锁是轻量级锁的一种典型实现

伪代码:

挂起等待锁是重量级锁一种典型的实现

借助系统中的线程调度机制,当尝试加锁,并且锁被占用了,出现锁冲突,就会让当前这个尝试加锁的线程被挂起(阻塞状态),此时这个线程就不参与调度了,知道这个锁被释放,然后系统才能去唤醒这个线程,去尝试重新获取锁。(这个过程消耗的时间更长)

那么 synchronized 的轻量级部分(基于 CAS 机制来实现的),基于自旋锁实现的,重量级部分(调用系统 API 通过内核完成),基于挂起等待锁实现的。

例子:

🚩 可重入锁 vs 不可重入锁

这个呢之前已经讲过了:

🚩 公平锁 vs 非公平锁

假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后C 也尝试获取锁, C也获取失败, 也阻塞等待.当线程 A 释放锁的时候, 会发生啥呢?

公平锁: 遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.

非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁

这就好比一群男生追同一个女神.

当女神和前任分手之后, 先来追女神的男生上位, 这就是公平锁;

如果是女神不按先后顺序挑一个自己看的顺眼的, 就是非公平锁

总结:

  • 严格按照先来后到的顺序来获取锁,哪个线程等待时间长,哪个线程就获取到锁,这就是公平锁
  • 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁.

  • 如果要想实现公平锁, 就需要依赖额外的数据结构(队列), 来记录线程们的先后顺序.

  • 公平锁和非公平锁没有好坏之分, 关键还是看适用场景

我们的 synchronized 是非公平锁,多个线程尝试获取到锁,此时是按照概率均等的方式来进行获取,系统本身线程调度的顺序就是随机的。

🚩 互斥锁 vs 读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。

所以读写锁因此而产生。读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥

一个线程对于数据的访问, 主要存在两种操作: 读数据 和 写数据.

总结:

  • 两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.

  • 两个线程都要写一个数据, 有线程安全问题.

  • 一个线程读另外一个线程写, 也有线程安全问题

读写锁就是把读操作和写操作区分对待. Java 标准库提供了ReentrantReadWriteLock 类, 实现了读写锁

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁

读写锁的应用场景:

读写锁特别适合于 “频繁读, 不频繁写” 的场景中. (这样的场景其实也是非常广泛存在的)

比如学校的教务系统.
每节课老师都要使用教务系统点名, 点名就需要查看班级的同学列表(读操作). 这个操作可能要每天就要执行好几次.
而什么时候修改同学列表呢(写操作)? 就新同学加入的时候. 可能一个月都不必改一次.
再比如, 同学们使用教务系统查看自己课表的时候(读操作), 一个班级的同学很多, 读操作一天就要进行几十次,一学期可能就几百次几千次.但是这一学期的课表, 学校可能只用发布一次(写操作)

Synchronized 不是读写锁

🎄 相关面试题

1.你是怎么理解乐观锁和悲观锁的,具体怎么实现呢?

  • 悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.
  • 乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
  • 悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
  • 乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突. 

2.介绍下读写锁?

  • 读写锁就是把读操作和写操作分别进行加锁.
  • 读锁和读锁之间不互斥.
  • 写锁和写锁之间互斥.
  • 写锁和读锁之间互斥.
  • 读写锁最主要用在 “频繁读, 不频繁写” 的场景中

3.什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

  • 如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.相比于挂起等待锁,
  • 优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
  • 缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源

4.synchronized 是可重入锁么?

  • 是可重入锁.
  • 可重入锁指的就是连续两次加锁不会导致死锁.
  • 实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增

🍀 synchronized 实现原理

🚩 锁升级

Synchronized的加锁过程:

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

🙂 无锁

无锁大家都能理解,大致意思是如果这个锁无人竞争,或者只有一个线程的时候,这时候不存在线程安全问题,Synchronized不会对其进行加锁

🙂 偏向锁

第一个尝试加锁的线程, 优先进入偏向锁状态

  • 偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程.
  • 如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)
  • 如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别,当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.
  • 偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.
  • 但是该做的标记还是得做的, 否则无法区分何时需要真正加锁

🙂 轻量级锁

随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).

此处的轻量级锁就是通过 CAS 来实现

  • 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)

  • 如果更新成功, 则认为加锁成功

  • 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).

值得注意的是:

自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.
因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.
也就是所谓的 "自适应"

🙂 重量级锁

如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁

  • 执行加锁操作, 先进入内核态.
  • 在内核态判定当前锁是否已经被占用
  • 如果该锁没有占用, 则加锁成功, 并切换回用户态.
  • 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
  • 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁

图示与例子:

除上述的加锁过程中做的优化,Synchronized还有一些其他的优化措施

分别为:锁消除 和 锁粗化

🚩 锁消除

🚩 锁粗化

面试题:

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值