【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 之后不能控制是否开启自旋功能。

相关推荐
·云扬·18 分钟前
Lambda 表达式详解
java·开发语言·笔记·学习·1024程序员节
星叔1 小时前
ARXML汽车可扩展标记性语言规范讲解
java·前端·汽车
2401_857600951 小时前
SpringBoot框架:共享汽车管理的创新工具
java·spring boot·汽车
代码小鑫2 小时前
A15基于Spring Boot的宠物爱心组织管理系统的设计与实现
java·开发语言·spring boot·后端·毕业设计·宠物
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
mapper.xml 使用大于号、小于号示例
xml·java·数据库
一直学习永不止步2 小时前
LeetCode题练习与总结:迷你语法分析器--385
java·数据结构·算法·leetcode·字符串··深度优先搜索
Tech Synapse2 小时前
Java将Boolean转为Json对象的方法
java·开发语言·json
小冉在学习2 小时前
day55 图论章节刷题Part07([53.寻宝]prim算法、kruskal算法)
java·算法·图论
伴野星辰2 小时前
网站视频过大,加载缓慢解决方法【分段加载视频】
java·服务器·音视频
让生命变得有价值3 小时前
k8s 启用 ValidatingAdmissionPolicy 特性
java·容器·kubernetes·kubelet