synchronized的实现原理和锁升级 面试重点

1.synchronized的实现原理

synchronized是Java 中的一个很重要的关键字,主要用来加锁,synchronized所添加的锁有以下几个特点。synchronized的使用方法比较简单,主要可以用来修饰方法和代码块。根据其锁定的对象不同,可以用来定义同步方法和同步代码块。

方法级的同步是隐式的(同步方法)。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

同步代码块使用monitorenter和monitorexit两个指令实现。 可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行 monitorenter)后,该计数器自增变为 1,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

2.synchronized特性

synchronized是Java中的一个很重要的关键字,主要用来加锁,synchronized具有以下特性:
互斥性

同一时间点,只有一个线程可以获得锁,获得锁的线程才可以处理被 synchronized 修饰的代码片段。
阻塞性

只有获得锁的线程才可以执行被synchronized 修饰的代码片段,未获得锁的线程只能阻塞,等待锁释放。
可重入性

如果一个线程已经获得锁,在锁未释放之前,再次请求锁的时候,是必然可以获得锁的

3.锁的具体表现:

对于普通同步方法,锁是当前 实 例 对 象。
对于静 态 同步方法, 锁 是当前 类 的 Class 对 象。
对于同步方法 块 , 锁 是 Synchonized 括号里配置的 对 象。

4.monitor(监视锁)

为了解决线程安全的问题,Java提供了同步机制、互斥锁机制,这个机制保证了在同一时刻只一个线程能访问共享资源。
这个机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。当我们尝试获得对象的锁的时候,其实是对该对象拥有的Monitor 进行操作。

5.synchronized锁的是什么?

无论是给静态方法或者非静态方法,加上synchronized标识,都是锁的对象,synchronized的普通方法,其实锁的是具体调用这个方法的实例对象,而synchronized的静态方法,其实锁的是这个方法锁属于的类对象。

6.synchronized的锁升级过程是怎样的?

6.1为什么要锁升级?

在JDK1.6及之前的版本中,synchronized锁是通过对象内部的一个叫做监视器锁(也称对象锁)来实现的。当一个线程请求对象锁时,如果该对象没有被锁住,线程就会获取锁并继续执行。如果该对象已经被锁住,线程就会进入阻塞状态,直到锁被释放。这种锁的实现方式称为"重量级锁",因为获取锁和释放锁都需要在操作系统层面上进行线程的阻塞和唤醒,而这些操作会带来很大的开销。
在JDK 1.6之后,synchronized锁的实现发生了一些变化,引入了"偏向锁"、"轻量级锁"和"重量级锁"三种不同的状态,用来适应不同场景下的锁竞争情况。
所以,在Java中,锁的状态分为四种,分别是无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

6.2 不同锁状态下的对象头中 mark word结构

在Java中,mark word的低两位用于表示锁的状态,分别为"01"(无锁状态)、"01"(偏向锁状态)、"00"(轻量级锁状态)和"10"(重量级锁状态)。但是由于无锁状态和偏向锁都是"01",
所以在低三位引入偏向锁标记位,用"0"表示无锁,"1"表示偏向。


6.3 偏向锁

大多数情况下,锁 不 仅 不存在多 线 程 竞 争,而且 总 是由同一线 程多次 获 得, 为 了 让线 程 获 得 锁 的代价更低而引入了偏向 锁 。当一个 线 程 访问 同步 块 并获取 锁时 ,会在 对 象 头 和 栈帧 中的 锁记录 里存 储锁 偏向的 线 程 ID ,以后 该线 程在 进 入和退出同步块时 不需要 进 行 CAS 操作来加 锁 和解 锁 ,只需 简单 地 测试 一下 对 象 头 的 Mark Word 里是否存储 着指向当前 线 程的偏向 锁 。如果 测试 成功,表示 线 程已 经获 得了 锁 。如果 测试 失 败 , 则 需要再测试 一下 Mark Word 中偏向 锁 的 标识 是否 设 置成 1 (表示当前是偏向 锁 ):如果没有 设 置, 则使用CAS 竞 争 锁 ;如果 设 置了, 则尝试 使用 CAS 将 对 象 头 的偏向 锁 指向当前 线 程。
触发条件:首次进入synchronized块是自动开启。

6.3.1偏向锁的撤销

偏向锁 使用了一种等到 竞 争出 现 才 释 放 锁 的机制,所以当其他 线 程 尝试竞 争偏向 锁时, 持有偏向锁 的 线 程才会 释 放 锁 。偏向 锁 的撤 销 ,需要等待全局安全点(在 这 个 时间 点上没有正在执 行的字 节码 )。它会首先 暂 停 拥 有偏向 锁 的 线 程,然后 检查 持有偏向 锁 的 线 程是否活着,如果线 程不 处 于活 动 状 态 , 则 将 对 象 头设 置成无 锁 状 态 ;如果 线 程仍然活着, 拥 有偏向 锁 的 栈会被执 行,遍 历 偏向 对 象的 锁记录 , 栈 中的 锁记录 和 对 象 头 的 Mark Word 要么重新偏向于其他线程,要么恢复到无 锁 或者 标记对 象不适合作 为 偏向 锁 ,最后 唤 醒 暂 停的 线 程。

6.4 轻量级锁
(1)轻量级加锁

线程在 执 行同步 块 之前, JVM 会先在当前 线 程的 栈桢 中 创 建用于存 储锁记录 的空 间 ,并将对 象 头 中的 Mark Word 复制到 锁记录 中,官方称 为 Displaced Mark Word 。然后 线 程 尝试 使用CAS将 对 象 头 中的 Mark Word 替 换为 指向 锁记录 的指 针 。如果成功,当前 线 程 获 得 锁 ,如果失败,表示其他 线 程 竞 争 锁 ,当前 线 程便 尝试 使用自旋来 获 取 锁 。

(2)轻量级锁解锁

轻量 级 解 锁时 ,会使用原子的 CAS 操作将 Displaced Mark Word 替 换 回到 对 象 头 ,如果成
功, 则 表示没有 竞 争 发 生。如果失 败 ,表示当前 锁 存在 竞 争, 锁 就会膨 胀 成重量 级锁 。

6.5 重量级锁

当轻量级锁的CAS操作失败,即出现了实际的竞争,锁会进一步升级为重量级锁。当锁状态升级到重量级锁状态时,JVM会将该对象的锁变成一个重量级锁,并在对象头中记录指向等待队列的指针。
此时,如果一个线程想要获取该对象的锁,则需要先进入等待队列,等待该锁被释放。当锁被释放时,JVM 会从等待队列中选择一个线程唤醒,并将该线程的状态设置为"就绪"状态,然后等待该线程重新获取该对象的锁。
触发条件:当轻量级锁的CAS操作失败,轻量级锁升级为重量级锁。
因为 自旋会消耗 CPU , 为 了避免无用的自旋(比如 获 得 锁 的 线 程被阻塞住了),一旦 锁 升 级 成重量级锁 ,就不会再恢复到 轻 量 级锁 状 态 。当 锁处 于 这 个状 态 下,其他 线 程 试图获 取 锁时 ,都会被阻塞住,当持有锁 的 线 程 释 放 锁 之后会 唤 醒 这 些 线 程,被 唤 醒的 线 程就会 进 行新一 轮的夺锁 之争。

6.6 锁的优缺点对比
相关推荐
营赢盈英2 分钟前
using showdown js with openAi streaming response
开发语言·前端·javascript·stream·openai api
箬敏伊儿3 分钟前
Spring Boot 启动时循环依赖的详细排查和解决步骤
java·网络·spring boot·后端·spring
wjs20244 分钟前
PostgreSQL LIMIT 子句的使用与优化
开发语言
Trouvaille ~12 分钟前
【Python篇】Python 函数综合指南——从基础到高阶
开发语言·python·生成器·异步函数·高阶函数·匿名函数·闭包
旺小仔.13 分钟前
【初阶C++篇】~ C++入门
java·c语言·开发语言·c++·算法
Mr. zhihao16 分钟前
为什么 mysql-connector-java 只需要在 runtime 作用范围中配置
java·数据库·mysql
zhangbin_23723 分钟前
【Python机器学习】NLP词频背后的含义——距离和相似度
开发语言·人工智能·python·机器学习·自然语言处理
番茄灭世神26 分钟前
C语言补习课
c语言·开发语言
1.01^100028 分钟前
JVM介绍
jvm
xunznux29 分钟前
Java 面试题:HTTP缓存:强制缓存和协商缓存--xunznux
java·经验分享·笔记·后端·网络协议·http·缓存