1、面试题
2、说明
【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:
- 尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用
RPC
方法。
3、Synchronized 锁
由对象头中的Mark Word
根据锁标志位的不同而被复用及锁升级策略
4、Java5之前的用户态和内核态间的切换
java
的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
在Java
早期版本中,synchronized
属于重量级锁,效率低下,因为监视器锁〈monitor
)是依赖于底层的操作系统的Mutex Lock
(系统互斥量)来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java
线程需要操作系统切换CPU
状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,
这也是为什么早期的synchronized
效率低的原因
Java 6
之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁
4.1、Monitor
M o n i t o r 的 本 质 是 依 赖 于 底 层 操 作 系 统 的 M u t e x L o c k 实 现 , \color{red}Monitor的本质是依赖于底层操作系统的Mutex Lock实现, Monitor的本质是依赖于底层操作系统的MutexLock实现,
操 作 系 统 实 现 线 程 之 间 的 切 换 需 要 从 用 户 态 到 内 核 态 的 转 换 , 成 本 非 常 高 。 \color{red}操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。 操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高。
Monitor
,java对象
以及线程
是如何关联?
1.如果一个java
对象被某个线程锁住,则该java
对象的Mark
Word
字段中LockWord
指向monitor
的起始地址
2.Monitor
的Owner
字段会存放拥有相关联对象锁的线程id
Mutex Lock的切换需要从用户态转换到核心态中r因此状态转换需要耗费很多的处理器时间。
4.2、结合之前sy和对象头说明
5、种类及升级步骤
5.1、多线程访问情况,3种
只有一个线程来访问
有多个线程(2个线程A、B 交替访问)
竞争激烈,更多个线程来访问
5.2、升级流程
synchronized
用的锁是存在Java
对象头里的Mark
Word
中
锁升级功能主要依赖MarkWord
中锁标忑位和释放偏向锁标志位
5.2.2、64位标记图再看
c源码的MarkWord标记
5.2.3、锁指向, 牢 记 ! ! ! \color{red}牢记!!! 牢记!!!
偏向锁:
- MarkWord存储的是偏向的线程
ID
;
轻量锁:
- MarkWord存储的是指向线程栈中
Lock Record
的指针;
重量锁:
- MarkWord存储的是指向堆中的
monitor
对象的指针;
5.3、无锁
对应64位标记图
public class Sup {
public static void main(String[] args) {
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
没有打印hashcode之前,不会存入内存布局中,没有生成,所以为0
打印了hashcode
,才会内存布局
public class Sup {
public static void main(String[] args) {
Object o = new Object();
System.out.println(o.hashCode());
System.out.println(Integer.toHexString(o.hashCode()));
System.out.println(Integer.toBinaryString(o.hashCode()));
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
5.4、偏向锁
5.4.1、是什么
偏向锁:
- 单线程竞争
当线程A第一次竞争到锁时,通过操作修改Mark Word中的偏向线程ID、偏向模式。
如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。
一段同步代码多次被同一个线程访问到,没有必要来回的加锁,解锁,减少用户态到内核态的切换,提高性能
5.4.2、主要作用
定义:当一段同步代码一直被同一个线程连续访问,由于还是同一个线程那么该线程在访问时直接进入同步代码
注 意 , 偏 向 锁 只 有 遇 到 其 他 线 程 尝 试 竞 争 偏 向 锁 时 , 持 有 偏 向 锁 的 线 程 才 会 释 放 锁 , 线 程 是 不 会 主 动 释 放 偏 向 锁 的 。 \color{blue}注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。 注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
同一个老顾客来访,直接老规矩行方便(一直在一个地方点同样的菜,后面直接老规矩)
看看多线程卖票,同一个线程获得体会一下
5.4.3、小结论
Hotspot 的作者经过研究发现,大多数情况下:
多线程的情况下,锁不仅不存在多线程竞争,还存在锁 由 同 一 个 线 程 多 次 获 得 的 情 况 \color{blue}由同一个线程多次获得的情况 由同一个线程多次获得的情况,
偏向锁就是在这种情况下出现的,它的出现是为了解决 只 有 在 一 个 线 程 执 行 同 步 时 提 高 性 能 \color{red}只有在一个线程执行同步时提高性能 只有在一个线程执行同步时提高性能。
备注:
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,
则持有偏向锁的线程将永远不需要触发同步。
也即偏向锁在资源没有竞争情况下消除了同步语句,懒的连CAS操作都不做了,直接提高程序性能
5.4.4、64位标记图再看
5.4.5、 偏 向 锁 的 持 有 \color{red}偏向锁的持有 偏向锁的持有
5.4.5.1、说明
理论落地:
-
在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说 锁 总 是 被 第 一 个 占 用 他 的 线 程 拥 有 , 这 个 线 程 就 是 锁 的 偏 向 线 程 。 \color{red}锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。 锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
-
那么只需要在锁第一次被拥有(无锁,MarkWord里面的线程ID为null)的时候(CAS替换线程ID,即将无锁变为偏向锁),记录下偏向线程ID。这样偏向线程就一直持有若锁(后续这个线程进入和退出这段加了同步锁的代码块时, 不 需 要 再 次 加 锁 和 释 放 锁 。 \color{red}不需要再次加锁和释放锁。 不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID)。
-
如 果 相 等 \color{red}如果相等 如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。 如 果 自 始 至 终 使 用 锁 的 线 程 只 有 一 个 \color{red}如果自始至终使用锁的线程只有一个 如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
-
如 果 不 等 \color{red}如果不等 如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS(比较并交换)来替换MarkWord里面的线程ID为新线程的ID,
-
竞 争 成 功 \color{red}竞争成功 竞争成功,表示之前的线程不存在了,检查MarkWord里面的线程ID不为空(设置为无锁状态,并且重新偏向),将MarkWord里面的线程ID修改为新线程的ID,锁不会升级,仍然为偏向锁(重新偏向);
-
竞 争 失 败 \color{red}竞争失败 竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。
-
注 意 , 偏 向 锁 只 有 遇 到 其 他 线 程 尝 试 竞 争 偏 向 锁 时 , 持 有 偏 向 锁 的 线 程 才 会 释 放 锁 , 线 程 是 不 会 主 动 释 放 偏 向 锁 的 。 \color{blue}注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。 注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
技术实现:
一个synchronized
方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word
中将偏向锁修改状态位,同时还会有占用前54
位来存储线程指针作为标识。若该线程再次访问同一个synchronized
方法时,该线程只需去对象头的Mark
Word
中去判断一下是否有偏向锁指向本身的ID
,无需再进入 Monitor
去竞争对象了。
5.4.5.2、细化案例Account对象
偏向锁的操作不用直接捅到操作系统, 不 涉 及 用 户 到 内 核 转 换 , 不 必 要 直 接 升 级 为 最 高 级 \color{red}不涉及用户到内核转换,不必要直接升级为最高级 不涉及用户到内核转换,不必要直接升级为最高级,我们以一个account对象的“对象头”为例,
假如有一个线程执行到synchronized
代码块的时候,JVM
使用CAS
操作把线程指针ID
记录到Mark Word
当中,并修改标偏向标示,标示当前线程就获得该锁。锁对象变成偏向锁〈通过CAS
修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。
这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里),
JVM
通过account
对象的Mark Word
判断t
当前线程ID
还在,说明还持有着这个对象的锁,就可以维续进入临界区工作。由于之前没有释放锁,
这
里
也
就
不
需
要
重
新
加
锁
。
如
果
自
始
至
终
使
用
锁
的
线
程
只
有
一
个
\color{red}这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个
这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
结论:
JVM
不用和操作系统协商设置Mutex
(争取内核),它只需要记录下线程ID
就标示自己获得了当前锁,不用操作系统接入。
上 述 就 是 偏 向 锁 \color{green}上述就是偏向锁 上述就是偏向锁:
- 在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行。
5.4.6、偏向锁JVM命令
java -XX:+PrintFlagsInitial | grep BiasedLock*
默偏向锁启动延迟4秒
5.4.6.2、重要参数说明
5.4.7、Code演示
public class SupDemo {
public static void main(String[] args) {
//biased
Object o = new Object();
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
发现是轻量级锁
参数说明;
偏向锁在JDK1.6以上默认开启,开启后程序启动几秒后才会被激活,可以使用JVM参数来关闭延迟
-XX:BiasedLockingStartupDelay=0
如果确定锁通常处于竞争状态则可通过JVM参数 -XX:-UseBiasedLocking
关闭偏向锁,那么默认会进入轻量级锁
5.4.8、Code演示2
修改配置参数
暂停4秒钟以上
public class SupDemo {
public static void main(String[] args) {
try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
//biased
Object o = new Object();
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
第一种情况
可以看到,锁状态为101
可偏向锁状态了,只是由于o
对象未用synchronized
加锁,
所以线程id是空的。其余数据跟上述无锁状态一样
第二种情况
public class SupDemo {
public static void main(String[] args) {
try {TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
System.out.println("============");
new Thread(()->{
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
},"t1").start();
}
}
偏向锁带线程id
情况,(就是加锁),第一行中后面不再是0
了,有了线程id
的值。
5.4.9、开始有第2个线程来抢夺
5.4.10、偏向锁的撤销
当有另外线程逐步来竞争锁的时候,就不能再使用偏向锁了,要升级为轻量级锁
竞争线程尝试CAS更新对象头失败,会等待到全局安全点(此时不会执行任何代码)撤销偏向锁.
偏向锁使用一种等到 竞 争 出 现 才 释 放 锁 的 机 制 \color{red}竞争出现才释放锁的机制 竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。
撤 销 需 要 等 待 全 局 安 全 点 ( 该 时 间 点 上 没 有 字 节 码 正 在 执 行 ) \color{red}撤销需要等待全局安全点(该时间点上没有字节码正在执行) 撤销需要等待全局安全点(该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:
-
第一个线程正在执行synchronized方法( 处 于 同 步 块 \color{red}处于同步块 处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现 锁 升 级 \color{red}锁升级 锁升级。
此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。
-
第一个线程执行完成synchronized方法( 退 出 同 步 块 \color{red}退出同步块 退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。
5.4.11、 小 总 结 \color{red}小总结 小总结
-
在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说 锁 总 是 被 第 一 个 占 用 他 的 线 程 拥 有 , 这 个 线 程 就 是 锁 的 偏 向 线 程 。 \color{red}锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。 锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。
-
那么只需要在锁第一次被拥有(无锁,MarkWord里面的线程ID为null)的时候(CAS替换线程ID,即将无锁变为偏向锁),记录下偏向线程ID。这样偏向线程就一直持有若锁(后续这个线程进入和退出这段加了同步锁的代码块时, 不 需 要 再 次 加 锁 和 释 放 锁 。 \color{red}不需要再次加锁和释放锁。 不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID)。
-
如 果 相 等 \color{red}如果相等 如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。 如 果 自 始 至 终 使 用 锁 的 线 程 只 有 一 个 \color{red}如果自始至终使用锁的线程只有一个 如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
-
如 果 不 等 \color{red}如果不等 如果不等,表示发生了竞争,锁已经不是总是偏向于同一个线程了,这个时候会尝试使用CAS(比较并交换)来替换MarkWord里面的线程ID为新线程的ID,
-
竞 争 成 功 \color{red}竞争成功 竞争成功,表示之前的线程不存在了,检查MarkWord里面的线程ID不为空(设置为无锁状态,并且重新偏向),将MarkWord里面的线程ID修改为新线程的ID,锁不会升级,仍然为偏向锁(重新偏向);
-
竞 争 失 败 \color{red}竞争失败 竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。
-
注 意 , 偏 向 锁 只 有 遇 到 其 他 线 程 尝 试 竞 争 偏 向 锁 时 , 持 有 偏 向 锁 的 线 程 才 会 释 放 锁 , 线 程 是 不 会 主 动 释 放 偏 向 锁 的 。 \color{blue}注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。 注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
5.4.12、题外话
J a v a 15 逐 步 废 弃 偏 向 锁 \color{red}Java15逐步废弃偏向锁 Java15逐步废弃偏向锁
5.5、轻锁
5.5.1、是什么
轻量级锁:
- 多 线 程 竞 争 , 但 是 任 意 时 刻 最 多 只 有 一 个 线 程 竞 争 ( 交 替 ) \color{blue}多线程竞争,但是任意时刻最多只有一个线程竞争(交替) 多线程竞争,但是任意时刻最多只有一个线程竞争(交替),即不存在锁竞争太过激烈的情况,也就没有线程阻塞。
5.5.2、主要作用
有线程来参与锁的竞争,但是获取锁的冲突时间极短
本 质 就 是 自 旋 锁 C A S \color{red}本质就是自旋锁CAS 本质就是自旋锁CAS
5.5.3、64位标记图再看
5.5.4、轻量锁的获取
轻量级锁是为了在线程 近 乎 交 替 \color{red}近乎交替 近乎交替执行同步块时提高性能。
主要目的:
- 在没有多线程竞争的前提下, 通 过 C A S \color{red}通过CAS 通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗, 说 白 了 先 自 旋 , 不 行 才 升 级 阻 塞 \color{red}说白了先自旋,不行才升级阻塞 说白了先自旋,不行才升级阻塞。
升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。
此
时
线
程
B
操
作
中
有
两
种
情
况
:
\color{red}此时线程B操作中有两种情况:
此时线程B操作中有两种情况:
如
果
锁
获
取
成
功
\color{blue}如果锁获取成功
如果锁获取成功,直接替换Mak Word中的线程ID为B自己的ID(A→ B)重新偏向于其他线程
(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;
如 果 锁 获 取 失 败 \color{blue}如果锁获取失败 如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。
5.5.4.2、 补 充 \color{red}补充 补充
轻
量
级
锁
的
加
锁
\color{blue}轻量级锁的加锁
轻量级锁的加锁
JVM
会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方成为Displaced Mark Word
。
若一个线程发现是轻量级锁,会把锁的Mark Word
复制到自己的Displaced Mark Word
里面。
然后线程尝试用CAS
将锁的Mark Word
替换为指向锁记录(Lock Record
)的指针。
-
如果成功,当前线程获得锁,
-
如果失败,表示
Mark Word
已经被替换成了其他线程的锁记录,- 说明在与其它线程竞争锁,当前线程就尝试使用
自旋来获取锁
。 - 自旋达到一定的次数依然没有成功时,升级为重量锁
- 说明在与其它线程竞争锁,当前线程就尝试使用
自旋CAS
:不断尝试去获取锁,能不升级就不往上捅,尽量不要阻塞
轻 量 级 锁 的 释 放 \color{blue}轻量级锁的释放 轻量级锁的释放
在释放锁时,当前线程会使用CAS
操作将Displaced Mark Word
的内容复制回锁的Mark Word
里面。
-
如果没有发生竞争,那么这个复制的操作会成功。
-
如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,
- 那么
CAS
操作会失败,此时会释放锁并唤醒被阻塞的线程。
- 那么
5.5.5、Code演示
如果关闭偏向锁,就可以直接进入轻量级锁
-XX:-UseBiasedLocking
public class SupDemo {
public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
},"t1").start();
}
}
5.5.6、自旋达到一定次数和程度
Java6之前
默认启用,默认情况下自旋的次数是10
次
-
-XX:PreBlockSpin=10 来修改
或者自旋线程数超过cpu
核数一半
上述了解即可,别用了。
java6之后
自适应自旋锁的大致原理
线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM
认为既然上次成功了,那么这一次也很大概率会成功。
反之
如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU
空转。
自适应意味着自旋的次数不是固定不变的
而是根据:
-
同一个锁上一次自旋的时间。
-
拥有锁线程的状态来决定。
5.5.7、轻量锁和偏向锁的区别
争夺轻量级锁失败时,自旋尝试抢占锁
-
轻量级锁每次退出同步块都需要释放锁,
-
而偏向锁是在竞争发生时才释放锁
5.6、重锁
.5.6.1、大量线程参与锁竞争
5.6.2、锁标志位
原 理 \color{blue}原理 原理
Java
中synchronized
的重量级锁,是基于进入和退出Monitor
对象实现的。在编译时会将同步块的开始位置插入monitor enter
指令,在结束位置插入monitor exit
指令。
当线程执行到monitor enter
指令时,会尝试获取对象所对应的Monitor
所有权,
如果获取到了,即获取到了锁,会在Monitor
的owner
中存放当前线程的id
,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor
。
5.7、小总结
5.7.1、锁升级后,hashcode去哪拉
说明
锁升级为轻量级或重量级锁后,Mark Word中保存的分别是 线 程 栈 帧 里 的 锁 记 录 指 针 \color{red}线程栈帧里的锁记录指针 线程栈帧里的锁记录指针和 重 量 级 锁 指 针 \color{red}重量级锁指针 重量级锁指针,已经没有位置再保存哈希码,GC年龄了, 那 么 这 些 信 息 被 移 动 到 哪 里 去 了 呢 ? \color{red}那么这些信息被移动到哪里去了呢? 那么这些信息被移动到哪里去了呢?
在
无
锁
状
态
\color{blue}在无锁状态
在无锁状态下,Mark Word
中可以存储对象的identity hash code
值。当对象的hashCode()
方法第一次被调用时,JVM
会生成对应的identity hash code
值并将该值存储到Mark Word
中。
对 于 偏 向 锁 \color{blue}对于偏向锁 对于偏向锁,
-
在线程获取偏向锁时,会用
Thread ID
和epoch
值覆盖identity hash code
所在的位置。如果一个对象的hashCode()
方法已经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的化,那
Mark Word
中的identity hash code
必然会被偏向线程ld
给覆盖,这就会造成同一个对象前后两次调用hashCode()
方法得到的结果不一致(相当于要重新计算一次)。 -
如果一个对象正处于偏向锁状态,又收到需要计算一致性哈希请求时,其偏向锁的状态会立即撤销,并且锁会膨胀为重量级锁
升 级 为 轻 量 级 锁 时 \color{blue}升级为轻量级锁时 升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝(Displaced Mark Word),该拷贝中可以包含identity hash code, 所 以 轻 量 级 锁 可 以 和 i d e n t i t y h a s h c o d e 共 存 \color{red}所以轻量级锁可以和identity hash code共存 所以轻量级锁可以和identityhashcode共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。
升 级 为 重 量 级 锁 后 \color{blue}升级为重量级锁后 升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头。
5.7.1.1、code01(刚开始)
当一个对象计算过identity hash code,它就无法进入偏向锁状态,跳过偏向锁,直接升级轻量级锁
public class SupDemo {
public static void main(String[] args) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object o = new Object();
System.out.println("本应是偏向锁");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
o.hashCode();//没有重写,一致性哈希,重写后失效当一个对象已经计算过identity hash code,它就无法进入偏向锁状态
synchronized (o) {
System.out.println("本应是偏向锁,但是由于计算过一致性哈希,会直接升级为轻量级锁");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
}
5.7.1.2、code02(以处于偏向锁状态)
偏向锁过程中遇到一致性哈希计算请求,
立马撤销偏向模式,膨胀为重量级锁
public class SupDemo {
public static void main(String[] args) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
Object o = new Object();
synchronized (o) {
o.hashCode();
System.out.println("偏向锁过程中遇到一致性哈希计算请求,立马撤销偏向模式,膨胀为重量级锁");
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
5.7.2、各种锁优缺点、synchronized锁升级与实现原理
锁的优缺点对比
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 若线程之间存在锁竞争,会带来额外的锁撤销的消耗 | 使用于 只 有 一 个 \color{red}只有一个 只有一个线程访问同步块场景(已知一个用户访问) |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快追求吞吐量 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 同步块执行速度较长 |
synchronized锁升级过程总结:
- 一 句 话 , 就 是 先 自 旋 , 不 行 再 阻 塞 \color{red}一句话,就是先自旋,不行再阻塞 一句话,就是先自旋,不行再阻塞。
- 实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式
synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的Mark Word来实现的。
JDK1.6之前synchronized使用的是重量级锁, J D K 1.6 之 后 进 行 了 优 化 , 拥 有 了 无 锁 > 偏 向 锁 > 轻 量 级 锁 − → 重 量 级 锁 的 升 级 过 程 \color{red}JDK1.6之后进行了优化,拥有了无锁>偏向锁>轻量级锁-→重量级锁的升级过程 JDK1.6之后进行了优化,拥有了无锁>偏向锁>轻量级锁−→重量级锁的升级过程,而不是无论什么情况都使用重量级锁。
偏 向 锁 \color{blue}偏向锁 偏向锁:
- 适用于单线程适用的情况(同一个用户一直访问),在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
轻 量 级 锁 \color{blue}轻量级锁 轻量级锁:
- 适用于竞争较不激烈的情况(这和乐观锁的使用范围类似),存在竞争时升级为轻量级锁,
- 轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源
- 但是相对比使用重量级锁还是更高效。
重 量 级 锁 \color{blue}重量级锁 重量级锁:
- 适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。
6、JIT编译器对锁的优化
6.1、JIT
Just ln Time Compiler,
一般翻译为即时编译器
6.2、锁消除
从JIT角度看相当于无视它,synchronized (o)不存在了,
这个锁对象并没有被共用扩散到其它线程使用,
极端来说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
就是每次新建对象,在对它加锁,JIT会无视它
public class LockClearUpDemo {
static Object objectLock = new Object();
public void m1() {
/*synchronized (objectLock){
System.out.println("=====hello LockClearUpDemo");
}*/
//锁消除问题,JIT编译器会无视它,synchronized(o),每次new出来的,不存在了,非正常的
Object o = new Object();
synchronized (o) {
System.out.println("=====hello LockClearUpDemo" + "\t"
+ o.hashCode() + "\t" + objectLock.hashCode());
}
}
public static void main(String[] args) {
LockClearUpDemo lockClearUpDemo = new LockClearUpDemo();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
lockClearUpDemo.m1();
}, String.valueOf(i)).start();
}
}
}
6.3、锁粗化
假如方法中首尾相接,前后相邻的都是同一个锁对象,
那JIT编译器就会把这几个synchronized块合并成一个大块,
加粗加大范围,一次申请锁使用即可,避免次次的申请和释放锁,提升了性能
public class LockBigDemo {
static Object objectLock = new Object();
public static void main(String[] args) {
LockClearUpDemo lockClearUpDemo = new LockClearUpDemo();
new Thread(() -> {
synchronized (objectLock){
System.out.println("11111");
}
synchronized (objectLock){
System.out.println("22222");
}
synchronized (objectLock){
System.out.println("33333");
}
synchronized (objectLock){
System.out.println("44444");
}
System.out.println("=======,相当于下面");
synchronized (objectLock){
System.out.println("11111");
System.out.println("22222");
System.out.println("33333");
System.out.println("44444");
}
}, "t1").start();
}
}
7、小总结
没有锁:自由自在
偏向锁:唯我独尊
轻量锁:楚汉争霸
重量锁:群雄逐鹿