前言

面试的时候有被问到,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编译之后,会在同步块的前后形成monitorentermonitorexit两个字节码指令。

根据《Java虚拟机规范》的要求

  • 在执行monitorenter指令的时候,首先要去尝试获取对象的锁(获取对象锁的过程,其实是获取monitor对象的所有权的过程)。
  • 如果这个对象没被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值增加一。
  • 而在执行monitorexit指令时会将锁计数器减一。一旦计数器的值为零,锁随即就被释放了。
  • 如果获取对象锁失败,那当前线程就应当被阻塞等待,直到请求锁定的对象被持有它的线程释放为止。

同步方法

同步方法test1的反编译后的字节码文件部分如下:



注意我用红框圈起来的部分,这个ACC_SYNCHRONIZED标志。代表的是当线程执行到方法后会检查是否有这个标志,如果有的话就会隐式的去调用monitorentermonitorexit两个命令来将方法锁住。

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底层是怎么实现的?的更多相关文章

  1. synchronized底层实现学习

    上文我们总结了 synchronized 关键字的基本用法以及作用,并未涉及 synchronized 底层是如何实现的,所谓刨根问底,本文我们就开始 synchronized 原理的探索之旅吧(*& ...

  2. Java并发编程:Synchronized底层优化(偏向锁、轻量级锁)

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  3. 并发-Synchronized底层优化(偏向锁、轻量级锁)

    Synchronized底层优化(偏向锁.轻量级锁) 参考: http://www.cnblogs.com/paddix/p/5405678.html 一.重量级锁 上篇文章中向大家介绍了Synchr ...

  4. 面试官都叫好的Synchronized底层实现,这工资开多少一个月?

    本文为死磕Synchronized底层实现第三篇文章,内容为重量级锁实现. 本系列文章将对HotSpot的synchronized锁实现进行全面分析,内容包括偏向锁.轻量级锁.重量级锁的加锁.解锁.锁 ...

  5. 一文让你读懂Synchronized底层实现,秒杀面试官

    本文为死磕Synchronized底层实现第三篇文章,内容为轻量级锁实现. 轻量级锁并不复杂,其中很多内容在偏向锁一文中已提及过,与本文内容会有部分重叠. 另外轻量级锁的背景和基本流程在概论中已有讲解 ...

  6. 说一下 synchronized 底层实现原理?(未完成)

    说一下 synchronized 底层实现原理?(未完成)

  7. Java多线程和并发(八),synchronized底层原理

    目录 1.对象头(Mark Word) 2.对象自带的锁(Monitor) 3.自旋锁和自适应自旋锁 4.偏向锁 5.轻量级锁 6.偏向锁,轻量级锁,重量级锁联系 八.synchronized底层原理 ...

  8. 死磕synchronized底层实现

    点赞再看,养成习惯,微信搜索[三太子敖丙]第一时间阅读. 本文 GitHub https://github.com/JavaFamily 已收录,有一线大厂面试完整考点.资料以及我的系列文章. 前言 ...

  9. synchronized底层原理

    synchronized底层语义原理 Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现. 在 Java 语言中,同步用的最多的地方可能是被 syn ...

随机推荐

  1. 手动向Maven本地仓库添加ORACLE ojdbc6jar包

    第一步: 把你的oracle中的ojdbc6.jar复制放到D盘首目录 这是我的D:\oracle\product\11.2.0\dbhome_1\jdbc\ D:ojdbc6.jar 但是Maven ...

  2. 如何解决Mybatis 日期查询时后面带.0

    关于如何解决Mybatis 日期查询时后面带.0   MySQL数据库: 直接在MySQL查询语句中解决: SELECT T.ID, T.USERNAME, DATE_FORMAT(t.birthda ...

  3. C++ 不具有继承关系的类之间的显式,隐式转换 2013-07-11 15:41

    好久没有写blog了,今天在学习c#的时候看到某一章节 讲类的隐式与显式转换.特此留笔,以供后续参考之用. 关于显式,隐式转换有些争论,说什么不建议隐式转换.但是个人认为非必要,如果有良好的基础书写基 ...

  4. JDBC | 第一章: 快速开始使用JDBC连接Mysql数据库?

    开始使用基于java的JDBC技术来连接mysql进行msyql数据库简单的CRUD操作 下载对应mysql驱动包 这里我创建maven项目基于maven下载 <!--mysql 驱动--> ...

  5. MyISAM 和InnoDB的区别

    InnoDB和MyISAM是许多人在使用MySQL时最常用的两个表类型,这两个表类型各有优劣,视具体应用而定.基本的差别为:MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持.MyISA ...

  6. ImportError: No module named git

    问题:ImportError: No module named git 解决:yum install GitPython

  7. 用python实现实时监控网卡流量

    很多时候,我们是需要查看服务器的网卡当前跑了多大流量,但对于网卡流量的查询,在linux下似乎没有像top那样的原生命令.虽然top功能很强大,可以实时查看cpu.内存.进程的动态,但是却没有对网卡流 ...

  8. Java 类初始化和实例初始化过程

    1.类初始化过程 2.实例初始化过程 3.方法的重写

  9. Spring Cloud Feign 自定义配置(重试、拦截与错误码处理) 实践

    Spring Cloud Feign 自定义配置(重试.拦截与错误码处理) 实践 目录 Spring Cloud Feign 自定义配置(重试.拦截与错误码处理) 实践 引子 FeignClient的 ...

  10. Spring boot程序的部署及运行

    将 spring boot 应用程序打包成 jar 包 我们使用 spring boot 的 maven 插件来构建管理整个应用程序,使用 mvn package 将应用程序打包成一个 jar 包 将 ...