synchronized底层是怎么实现的?
前言
面试的时候有被问到,synchronized底层是怎么实现的,回答的比较浅,面试官也不是太满意,所以觉得要好好总结一下,啃啃这个硬骨头。
synchronized使用场景
我们在使用synchronized的时候都知道它是可以使用在方法上的也可以使用在代码块上的,那么使用在这两个地方有什么区别呢?
synchronized用在方法上
使用在静态方法上,synchronized锁住的是类对象。
public class SynchronizedTest {
/**
* synchronized 使用在静态方法上
*/
public static synchronized void test1(){
System.out.println("I am test1 method");
}
}
使用在实例方法上,synchronized锁住的是实例对象。
public class SynchronizedTest {
/**
* synchronized 使用在实例方法上
* @return
*/
public synchronized String syncOnMethod(){
return "a developer name Jimoer";
}
}
synchronized用在代码块上
synchronized的同步代码块用在类实例的对象上,锁住的是当前的类的实例。
即执行buildName的时候,整个对象都会被锁住,直到执行完成buildName后释放锁。
public class SynchronizedTest {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
/**
* 带姓氏的名称
* @param firstName 姓氏
*/
public void buildName(String firstName){
synchronized(this){
this.setName(firstName+this.getName());
}
}
}
synchronized的同步代码块用在类对象上,锁住的是该类的类对象。
public class SynchronizedTest {
private static String myName = "Jimoer";
/**
* 带姓氏的名称
* @param firstName 姓氏
*/
public static void buildName(String firstName){
synchronized(SynchronizedTest.class){
System.out.println(firstName+myName);
}
}
}
synchronized的同步代码块用在任意实例对象上,锁住的就是配置的实例对象。
public class SynchronizedTest {
private String lastName;
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
/**
* 带姓氏的名称
* @param firstName 姓氏
*/
public void buildName(String firstName){
synchronized(lastName){
System.out.println(firstName+lastName);
}
}
}
synchronized的使用就介绍到这里,正常情况下会用了就可以了,能在实际场景中使用的时候知道锁住的范围就可以了,但是面试的时候可是要问原理的,而且在程序出现问题的时候,知道原理也是能快速定位问题的基础。
synchronized的原理
我们来看一下synchronized底层是怎么实现的吧。
例如:
下面一段代码,包含一个synchronized代码块和一个synchronized的同步方法。
public class SynchronizedTest {
private static String myName = "Jimoer";
public static void main(String[] args) {
synchronized (myName){
System.out.println(myName);
}
}
/**
* synchronized 使用在静态方法上
*/
public static synchronized void test1(){
System.out.println("I am test1 method");
}
}
在编译完成后生成了class文件,我将class文件反编译出来,看看生成的class文件的内容。
javap -p -v -c SynchronizedTest.class
反编译出来的字节码文件内容有点多,我只截取了关键部分来分析。

注意上面我用红框标出来的地方,synchronized关键字在经过Javac编译之后,会在同步块的前后形成monitorenter和monitorexit两个字节码指令。
根据《Java虚拟机规范》的要求
- 在执行
monitorenter指令的时候,首先要去尝试获取对象的锁(获取对象锁的过程,其实是获取monitor对象的所有权的过程)。 - 如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一。
- 而在执行
monitorexit指令时会将锁计数器减一。一旦计数器的值为零,锁随即就被释放了。 - 如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。
同步方法
同步方法test1的反编译后的字节码文件部分如下:

注意我用红框圈起来的部分,这个ACC_SYNCHRONIZED标志。代表的是当线程执行到方法后会检查是否有这个标志,如果有的话就会隐式的去调用monitorenter和monitorexit两个命令来将方法锁住。
monitor对象
我在上面说了,获取对象锁的过程,其实是获取monitor对象的所有权的过程。哪个线程持有了monitor对象,那么哪个线程就获得了锁,获得了锁的对象可以重复的来获取monitor对象,但是同一个线程每获取一次monitor对象所有权锁计数就加一,在解锁的时候也是需要将锁计数减成0才算真的释放了锁。
monitor对象,我们其实在Java的反编译文件中并没有看到。这个对象是存放在对象头中的。
对象头
这里要介绍一下对象头,首先要说一下对象的内存布局,在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
- 实例数据里面存储的是对象的真正有效数据,里面包含各种类型的字段内容,无论是自身的还是从父类继承来的。
- 对齐填充这部分并不是必然存在的,只是为了占位。虚拟机自动管理内存系统要求对象的大小必须是8字节的整数倍,当整个对象的大小不是8字节的整数倍时,用来对齐填充补全。
- 对象头部分包含两类信息。
1、第一类是自身运行时数据,如何哈希码(hashcode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等,这部分数据官方称它为“Mark Word”。
2、第二类是类型指针,即对象指向它的类型元数据的指针,虚拟机通过它来确定对象是哪个类型的实例。
接着回到我们的monitor对象,monitor对象的源码是C++写的,在虚拟机的ObjectMonitor.hpp文件中。
数据结构长这个样子。
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; // 线程重入次数
_object = NULL; // 存储Monitor对象
_owner = NULL; // 持有当前线程的owner
_WaitSet = NULL; // wait状态的线程列表
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 单向列表
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁状态block状态的线程列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
有想对这个monitor对象更深入了解的可以去Java虚拟机的源码里看看。
重量级锁
在主流的Java虚拟机实现中,Java的线程是映射到操作系统的原生内核线程之上的,如果要阻塞或唤醒一条线程,则需要操作系统来帮忙完成,这就不可避免地陷入用户态到核心态的转换中,这种状态的转换要耗费很多的处理时间。
所以在ObjectMonitor文件中的调用过程和复杂的操作系统运行机制导致线程的阻塞或唤醒时是很耗费资源的。
这样在JDK1.6之前都称synchronized为重量级锁。
重量级锁的减重
高效并发是从JDK5升级到JDK6的一项重要的改进项,在JDK6版本上虚拟机开发团队花费了大量的资源去实现各种锁优化技术,来为重量级锁减重。
synchronized在升级后的整个加锁过程,大致如下图。

这里要说明一下,锁升级的过程是不可逆的。
偏向锁
上面在介绍对象头的时候,说到了对象头中包含的内容了,其中有一个就是偏向锁的线程ID,它代表的意思就是说,如果当一个线程获取到了锁之后,锁的标志计数器就会+1,并且把这个线程的id存储在锁住的这个对象的对象头上面。
这个过程是通过CAS来实现的,每次线程进入都是无锁的,当执行CAS成功后,直接将锁的标志计数+1(持有偏向锁的线程以后每次进入锁时不做任何操作,标志计数直接+1),这个时候其他线程再进来时,执行CAS就会失败,也就是获取锁失败。

偏向锁在JDK1.6是默认开启的,通过参数进行关闭xx:-UseBiasedLocking=false。
偏向锁可以提高带有同步但无竞争的程序性能,但如果大多数的锁都总是被多个不同的线程访问,那偏向锁就是多余的。
轻量级锁
轻量级锁还是和对象头的第一部分(Mark Word)相关。
- 在代码即将进入同步块的时候,如果此同步对象没有被锁定,虚拟机首先将当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用户存储锁对象目前的Mark Word的拷贝。
- 然后JVM将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,说明线程获取锁成功,并执行后面的同步操作。
- 如果这个更新动作失败了,说明锁对象已经被其他线程抢占了,那轻量级锁不在有效,必须膨胀为重量级锁。此时被锁住的对象的标志变为重量级锁的标志。

自旋锁
当轻量级锁获取失败后,就会升级为重量级锁,但是重量级锁之前也介绍了是很耗资源的,JVM开发团队注意到许多程序上,共享数据的二锁定状态只会持续很短一段时间,为了这段时间去挂起和恢复线程并不值得。
所以想到了一个策略,那就是当线程请求一个已经被锁住的对象时,可以让未获取锁的线程“稍等一会”,但不放弃处理器执行时间,只需要让线程执行一个忙循环(自旋),这就是所谓的自旋锁。
自旋锁在JDK1.4.2中引入,默认关闭,可以通过-XX:UserSpinning参数来开启,默认自旋次数是10次,用户可以自定义次数,配置参数是-XX:PreBockSpin。
无论是用户指定还是默认值的自旋次数,对JVM重所有的锁来说都是相同的。在JDK6中引入了自适应自旋,根据前一次在同一锁上的自旋时间及拥有者的状态来决定。如果上一次同一个对象自旋锁获得成功了,那么再次进行自旋时就会认为成功几率很大,那么自旋次数就会自动增加。反之如果自旋很少成功获得锁,那么以后这个自旋过程都有可能被省略掉。
这样在轻量级失败后,就会升级为自旋锁,如果自旋锁也失败了,那就只能是升级到重量级锁了。

参考资料:《深入理解Java虚拟机》、死磕synchronized底层实现
synchronized底层是怎么实现的?的更多相关文章
- synchronized底层实现学习
上文我们总结了 synchronized 关键字的基本用法以及作用,并未涉及 synchronized 底层是如何实现的,所谓刨根问底,本文我们就开始 synchronized 原理的探索之旅吧(*& ...
- Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
- 并发-Synchronized底层优化(偏向锁、轻量级锁)
Synchronized底层优化(偏向锁.轻量级锁) 参考: http://www.cnblogs.com/paddix/p/5405678.html 一.重量级锁 上篇文章中向大家介绍了Synchr ...
- 面试官都叫好的Synchronized底层实现,这工资开多少一个月?
本文为死磕Synchronized底层实现第三篇文章,内容为重量级锁实现. 本系列文章将对HotSpot的synchronized锁实现进行全面分析,内容包括偏向锁.轻量级锁.重量级锁的加锁.解锁.锁 ...
- 一文让你读懂Synchronized底层实现,秒杀面试官
本文为死磕Synchronized底层实现第三篇文章,内容为轻量级锁实现. 轻量级锁并不复杂,其中很多内容在偏向锁一文中已提及过,与本文内容会有部分重叠. 另外轻量级锁的背景和基本流程在概论中已有讲解 ...
- 说一下 synchronized 底层实现原理?(未完成)
说一下 synchronized 底层实现原理?(未完成)
- Java多线程和并发(八),synchronized底层原理
目录 1.对象头(Mark Word) 2.对象自带的锁(Monitor) 3.自旋锁和自适应自旋锁 4.偏向锁 5.轻量级锁 6.偏向锁,轻量级锁,重量级锁联系 八.synchronized底层原理 ...
- 死磕synchronized底层实现
点赞再看,养成习惯,微信搜索[三太子敖丙]第一时间阅读. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的系列文章. 前言 ...
- synchronized底层原理
synchronized底层语义原理 Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现. 在 Java 语言中,同步用的最多的地方可能是被 syn ...
随机推荐
- do...while循环语句(水仙花)
#define _CRT_SECURE_NO_WARNINGS#include<stdio.h>#include<string.h>#include<stdlib.h&g ...
- 高吞吐量消息系统—kafka
现在基本上大数据的场景中都会有kafka的身影,那么为什么这些场景下要用kafka而不用其他传统的消息队列呢?例如rabbitmq.主要的原因是因为kafka天然的百万级TPS,以及它对接其他大数据组 ...
- Linux学习笔记 一 第三章 Linux常用命令
第三章Linux常用命令 一.文件处理命令 1.命令格式 2.目录处理命令:ls 3.目录处理命令:mkdir 4.文件处理命令: touch
- 关于初次使用Thymeleaf遇到的问题 2020-08-11
关于初次使用Thymeleaf遇到的问题 环境: IDEA :2020.1 Maven:3.5.6 SpringBoot: 2.3.2 原做法: 按照视频教程,导入依赖,并修改报的版本为3.0.9,适 ...
- Python版常见的排序算法
学习笔记 排序算法 目录 学习笔记 排序算法 1.冒泡排序 2.选择排序 3.插入排序 4.希尔排序 5.快速排序 6.归并排序 7.堆排序 排序分为两类,比较类排序和非比较类排序,比较类排序通过比较 ...
- puppeteer去掉同源策略及请求拦截
puppeteer是一个功能强大的工具,在自动化测试和爬虫方面应用广泛,这里谈一下如何在puppeteer中关掉同源策略和进行请求拦截. 同源策略 同源策略为web 安全提供了有力的保障,但是有时候我 ...
- 简单说说mybatis是防止SQL注入的原理
mybatis是如何防止SQL注入的 1.首先看一下下面两个sql语句的区别: <select id="selectByNameAndPassword" parameterT ...
- C++ Templates (1.2 模板实参推断 Template Argument Deduction)
返回完整目录 目录 1.2 模板实参推断 Template Argument Deduction 1.2 模板实参推断 Template Argument Deduction 当调用函数模板(如max ...
- Ceph Luminous手动解决pg分布不均衡问题
原文链接: https://www.jianshu.com/p/afb6277dbfd6 1.设置集群仅支持 Luminous(或者L之后的)客户端 具体命令: ceph osd set-requir ...
- 利用python爬取贝壳网租房信息
最近准备换房子,在网站上寻找各种房源信息,看得眼花缭乱,于是想着能否将基本信息汇总起来便于查找,便用python将基本信息爬下来放到excel,这样一来就容易搜索了. 1. 利用lxml中的xpath ...