【JavaEE】synchronized原理详解

本文使用的是JDK1.8

目录

引言

Java对象在JVM的结构

对象头

[Mark Word](#Mark Word)

Monitor

Owner

EntryList

WaitSet

加锁过程

锁消除

偏向锁

偏向锁使用

重偏向

撤销偏向

轻量级锁

重量级锁

自旋优化


引言

对于synchronized原理讲解之前,我们需要知道Java对象在JVM中的结构和Monitor是什么。

参考文章:Java对象头详解 - 简书 (jianshu.com)

http://t.csdnimg.cn/uX4RP

黑马程序员深入学习Java并发编程,JUC并发编程全套教程


Java对象在JVM的结构

普通对象

数组对象

其中对象头和加锁关系非常大。所以这里只介绍对象头。

对象头

以32位的JVM为例,如果是64位的,那就×2.

普通对象

ruby 复制代码
|--------------------------------------------------------------|
|                     Object Header (64 bits)                  |
|------------------------------------|-------------------------|
|        Mark Word (32 bits)         |    Klass Word (32 bits) |
|------------------------------------|-------------------------|

数组对象

ruby 复制代码
|---------------------------------------------------------------------------------|
|                                 Object Header (96 bits)                         |
|--------------------------------|-----------------------|------------------------|
|        Mark Word(32bits)       |    Klass Word(32bits) |  array length(32bits)  |
|--------------------------------|-----------------------|------------------------|
Mark Word
ruby 复制代码
|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|

64位的如下:

偏向锁位 + 锁标志位

identity_hashcode :hashcode。只有调用该方法时才会生成。如果调用某个对象未被重写的HashCode方法,此时对它进行上锁,它将直接进入轻量级锁;如果它在偏向锁的基础上,在调用HashCode方法,此时它就变成重量级锁了。

epoch:偏向时间戳。


Monitor

上面提到了重量级锁,就是和这里的Monitor对象有关。每个Java对象都可以关联一个Monitor对象,使用synchronized加锁的对象如果升级成重量级锁,就要和Monitor对象关联了。

Monitor对象主要由三部分构成

Owner

开始时,Owner为空。如果对象A被Thread-1线程加成了重量级锁,并且它右调用了Object中的wait方法,则它就进入WaitSet中等待,此时其他线程可以对对象A上锁了。Thread-1同理。现在就是Thread-2对对象A上锁了,并且没释放,也没有wait。

EntryList

Thread-3和Thread-4线程也想对对象A上锁,但是此时Thread-2线程持有锁,它俩只能进入EntryList进行等待。如果后续Thread-2线程释放了锁,就会通知EntryList中所有的线程来竞争锁。

WaitSet

原本持有锁,但是使用wait方法后放弃了锁,就进入WatiSet中进行等待。后续只能用对象A的notify()或者notifyAll()方法来唤醒它们。


加锁过程

synchronized的加锁过程是逐步提高的,并不是一上来就要加重量级锁。

锁消除

对于一些对象,如果它不可能被其他线程贡献,并且对于该对象使用synchronized加锁了,那么JIT(即时编译器)会自动的不给这个对象加锁。因为不可能发生锁冲突的情况。如下代码:

java 复制代码
public class MyTest {
	static int x = 0;
	public void a() throws Exception {
		x++;
	}
	// 这里的 o 对象是不可能被其他线程使用到的
	public void b() throws Exception {
		Object o = new Object();
		synchronized (o) {
			x++;
		}
	}
}

锁消除是默认开启的。-XX:-EliminateLocks 使用它关闭。


偏向锁

  • 偏向锁是默认开启的。所以当对象创建后,它的锁标志位后三位为 101,且偏向时间戳为0,也没有线程指针。
  • 偏向锁默认是延迟的。也就是不会在程序启动时立刻生效,如果不想有延迟,可以添加VM参数 -XX:BiasedLockingStartupDelay=0 来禁用。

偏向锁使用

在没有锁竞争的时候,每次重入都需要进行CAS操作(把线程ID记录到对象中),但这个操作在第一次执行完之后如果重入的时候在使用CAS操作没必要。所以在JDK 6 之后就使用偏向锁来优化。

线程第一次使用对象后,就把线程ID记录的对象头中的Mark Word中。后续如果要加锁,先看看线程ID是不是自己,表示重入,就没发生竞争。

重偏向

如果有多个线程 访问同一个对象 ,但是没有发生锁竞争。比如线程1先对对象A加了偏向锁(线程A已经结束),后续线程2又使用了对象A,当访问次数超过20 次后,后续如果线程2还要使用对象A,那么此时对象A的Mark Word中的线程ID就变成了线程2的。

撤销偏向

  • 当上述的重偏向次数超过 40 次后,那么这个类所创建的对象都会变成不可偏向的,新建的对象也都是不可偏向的。
  • 当调用对象的hashCode方法时,偏向锁也会被撤销。如果是轻量级锁,那么hashCode会保存在锁记录中;如果是重量级锁,hashCode会保存在Monitor中。
  • 线程1使用对象时,线程2也来使用相同的对象了,此时也会撤销偏向锁,升级成轻量级锁。
  • 当调用了wait方法后,在notify后,此时就从偏向锁升级成重量级锁了。

轻量级锁

之前谈到,当多个线程对同一对象操作时,锁状态会从偏向锁升级到轻量级锁。轻量级锁是对使用者透明的。

轻量级锁加锁过程如下:

  • 每个线程的栈帧都要创建一个**锁记录(Lock Record)**的结构,其内部存储锁定对象的Mark Word
  • Object reference指向锁对象;并用CAS尝试把对象头中的Mark Word中的跟偏向锁相关的内容存到lock record中,把原来的Mark Word中偏向锁的记录改成存 lock record的地址。如果操作成功,就如下图所示:
  • 如果CAS尝试失败:
    • 可能是其他线程拥有了该对象的轻量级锁。此时就会自选优化,最后升级到重量级锁
    • 也有可能是自己这个线程执行的所重入,那么就会继续增加一条锁记录,不过新加的锁记录的指向地址就是空,后续取到为空的锁记录时,重入记录减一。如下图
  • 轻量级锁解锁时,如果最后一条锁记录为 偏向锁的相关信息,则使用CAS把Mark Word中的恢复
    • 恢复成功,就是解锁成功
    • 恢复失败,说明轻量级锁升级为重量级锁了,要用重量级锁的方式来解锁。

重量级锁

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

下图中的Thread-0线程已经加了轻量级锁,当Thread-1线程想来加锁时,那就不成功,Object对象就进入锁升级,升级到重量级锁,也就要和刚开始提到的Monitor关联。

Thread-1线程要为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址然后自己进入 Monitor 的 EntryList 进行 BLOCKED。

自旋优化

上面Thread-1线程进入EntryList中不是立刻的。它还对Thread-0线程抱有一丝希望,觉得它能马上执行完成,然后释放锁。所以Thread-1在此期间就重试加锁,过程如下:

重试成功:

重试失败:

自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

Java 7 之后不能控制是否开启自旋功能。

相关推荐
李慕婉学姐13 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆14 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin15 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model200515 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉15 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国15 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_9418824815 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈16 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_9916 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹16 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理