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

相关推荐
羊锦磊42 分钟前
[ Mybatis 多表关联查询 ] resultMap
java·开发语言·数据库·mysql·mybatis
ZeroToOneDev3 小时前
Java(泛型和JUnit)
java·开发语言·笔记
迪尔~5 小时前
Apache POI中通过WorkBook写入图片后出现导出PDF文件时在不同页重复写入该图片问题,如何在通过sheet获取绘图对象清除该图片
java·pdf·excel
现在,此刻5 小时前
leetcode 11. 盛最多水的容器 -java
java·算法·leetcode
DKPT6 小时前
Java设计模式之开闭原则介绍与说明
java·设计模式·开闭原则
hyy27952276846 小时前
企业级WEB应用服务器TOMCAT
java·前端·tomcat
布朗克1686 小时前
Spring Boot项目通过Feign调用三方接口的详细教程
java·spring boot·feign
Arva .6 小时前
Spring基于XML的自动装配
xml·java·spring
帅得不敢出门8 小时前
Android Framework定制长按电源键关机的窗口
android·java·framework
fatfishccc8 小时前
循序渐进学 Spring (上):从 IoC/DI 核心原理到 XML 配置实战
xml·java·数据库·spring·intellij-idea·ioc·di