深入解析Synchronized锁底层原理

一、概述

java Synchronized锁是Java提供的,底层依赖JVM实现的单机锁。可用于对实例方法静态方法以及代码块进行加锁,用于保护多线程环境下对共享资源访问的安全性。

二、底层原理分析

java Synchronized锁依赖于Java对象的内存布局,因此,需要了解Java对象的内存布局。

详情可见这篇文章:深入解析Java对象创建逻辑、内存布局以及访问机制

2.1 Monitor对象

每个对象自从创建起,都会关联一个锁对象,即Monitor对象【管程\监视器】。每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。该对象在Java虚拟机中,由C++实现的,具体表现为:

java 复制代码
ObjectMonitor,具体的数据结构为:
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;     
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  // _owner指向持有ObjectMonitor对象的线程
    _WaitSet      = NULL;  // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
具体字段解释:
- _owner: 指向拥有ObjectMonitor对象的线程
- _WaitSet: 存放处于wait状态的的线程队列
- _EntryList: 存放处于等待锁block状态的线程队列
- _recursions:锁的重入次数
- _count:用来记录该线程获取锁的次数

工作原理:

  1. 当多个线程同时访问一段同步代码时。首先假设线程A会进入EntryList队列中,会判断owner是否为空,主要通过CAS操作(比较和交换,比较新值和旧值的不同)。如果owner为null,直接把其赋值,指向自己owner=self,同时把可重入次数recursions=1,count+1获取锁成功。如果self=cur,说明是当前线程,锁重入了,recursions++即可。线程A进入owner区域,然后执行同步方法块;
  2. 若线程B来获取锁。首先会放入EntryList队列中。然后去判断锁是否被占用,此时线程A正在使用该锁,那么会一直放在EntryList队列中,直到线程A释放锁,所有EntryList队列中竞争锁是非公平的;
  3. 若持有monitor的线程调用wait()方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入WaitSet集合中等待被唤醒;
  4. 线程从WaitSet集合中被唤醒notifyall后,会放入到EntryList队列中,参与锁的竞争

2.2 锁分类以及升级流程

2.2.1 偏向锁

背景:在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。轻量级锁需要多次CAS操作,偏向锁来减少CAS的操作次数。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了降低获取所得代价,引入偏向锁。
大致流程:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁。
获取锁、释放锁以及锁升级的流程:

  1. 获取锁:当线程1访问代码块并获取锁对象后,会在对象头和栈帧中记录偏向锁的threadID。以后,线程1再次获取锁时,直接比较当前threadID和对象头中的threadID是否一致;如果一致,不需要CAS加锁、解锁;
  2. 释放锁:采用只有线程竞争时才会释放锁的机制【没有竞争时,解锁后线程ID仍会存在对象头中】。偏向锁的撤销需要等待全局安全点(在这个时间点上没有正在执行的字节码)。首先暂停拥有偏向锁的线程,(1)判断拥有偏向锁的线程是否存活;若没有存活,锁对象状态被重置为无锁状态;若存活,(2)查找线程1的栈帧信息,是否需要继续持有这个锁。若需要,等待全局安全点,在安全点暂停持有偏向锁的线程1,撤销偏向锁,升级为轻量级锁(锁的升级);若不需要,将锁对象的状态设为无锁状态,重现偏向新的线程。

2.2.2 轻量级锁

背景:在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生性能消耗
使用场景:如果一个对象虽然有多线程要加锁,但加锁时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
使用轻量级锁的时机:当关闭偏向锁或者多个线程竞争偏向锁导致锁升级为轻量级锁,则会尝试获取轻量级锁,获取锁的流程如下:

  1. JVM在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中:
  2. 让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录。
  3. 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁:

  4. 如果 cas 失败,有两种情况:
    ● 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
    ● 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。

    解锁流程】如下所示:
  5. 当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一;
  6. 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用CAS将 Mark Word 的值恢复给对象头:
    ● 成功,则解锁成功
    ● 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
    锁膨胀流程 】:
    如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
  7. 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁;
  8. 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    ● 为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
    ● 然后自己进入 Monitor 的 EntryList BLOCKED。
  9. 当 Thread-0 退出同步块解锁时,使用cas将Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程

2.2.3 重量级锁

缺点:

  1. ObjectMonitor源码时,会发现一些内核函数,对应的线程为park()和upark(),该操作涉及到用户态和内核态,从用户态切换到内核态是非常消耗资源的
  2. 用户态:程序的运行空间进入到用户运行状态;
  3. 内核态:涉及到IO操作
2.2.3.1 自旋优化-自旋锁

出现原因:线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力
解决方案:让线程等待一段时间,不会立即被挂起,看持有锁的线程是否会很快释放锁
优缺点:

  1. **优点:**可以避免线程切换的开销;但占用了处理器CPU的时间,如果持有锁的线程很快就释放了锁,自旋效率是很高的;否则,会消耗掉大量的资源
  2. **缺点:**对自旋次数进行了限制,默认为10次。如果次数到限制,刚刚退出,锁被释放(多等一两次就可以获得锁)是非常遗憾的
2.2.3.2 自旋优化-自适应自旋锁

即自旋的次数不在固定,取决于前一次在同一锁上的自旋时间以及锁的拥有者的状态来决定。如果自旋成功了,下次自旋的次数就会增加,即jvm会认为自旋获得锁的成功率会很高;反之,对于某个锁,很少有能自旋成功的,在以后自旋时,会相应减少自选的次数。
锁消除:

  1. 如果不存在竞争,为什么还需要加锁?可以将锁消除。
  2. 锁消除依据:逃逸分析的数据支持,如果变量没有逃逸出方法,又因为栈是线程私有的,所以不会存在竞争情况,可以放心清除锁,节省毫无意义的请求锁的时间
    锁粗化:
  3. 出现背景:如果发生同一对象进行一系列的加锁解锁操作,会导致不必要的性能消耗
  4. 解决方法:锁粗化,即将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,降低加锁解锁的次数。

2.2.4 各种锁的对比

2.2.5 synchronized 特性

  1. 可重入性:
    ● Synchronized锁对应的时候有一个计数器,记录下线程获取锁的次数,每次获取锁,计数器加1,每次释放锁,计数器减1,直到计数器为0,锁全部释放。
    ● 好处:可以避免一些死锁的情况
  2. 不可中断性:
    ● 一个线程获取锁之后,另一个线程处于阻塞或者等待状态,前一个线程不释放锁,后一个线程会一直阻塞或者等待,不可以被中断

2.2.6 Synchronized针对同步代码块和同步方法的底层原理

  1. 同步代码块:
    ● 对象头会关联到一个monitor对象。进入一个方法的时候执行monitorEnter(同步代码块的开始位置),获取当前对象的一个所有权owner,monitor数值加1,当前这个线程就是monitor的owner,退出的时候对应monitorexit(插入到方法结束处和异常处)。monitorenter和monitorexit是一对,缺一不可。
    ● 如果已经拥有owner,再次获得锁(可重入),计数器加1,执行monitorexit时,计数器减1;
    ● 互斥性体现在:是否能够获得monitor的所有权
  2. 同步方法
    ● 与同步代码块类似,多了一个标识位ACC_SYNCHRONIZED,一旦执行方法的时候,就会先判断是否存在 标志位,然后ACC_SYNCHRONIZED会隐式的调用monitorenter和monitorexit。归根到底,还是monitor的争夺。
相关推荐
Yan.love30 分钟前
开发场景中Java 集合的最佳选择
java·数据结构·链表
椰椰椰耶33 分钟前
【文档搜索引擎】搜索模块的完整实现
java·搜索引擎
大G哥34 分钟前
java提高正则处理效率
java·开发语言
小_太_阳1 小时前
Scala_【1】概述
开发语言·后端·scala·intellij-idea
智慧老师1 小时前
Spring基础分析13-Spring Security框架
java·后端·spring
lxyzcm1 小时前
C++23新特性解析:[[assume]]属性
java·c++·spring boot·c++23
V+zmm101342 小时前
基于微信小程序的乡村政务服务系统springboot+论文源码调试讲解
java·微信小程序·小程序·毕业设计·ssm
Oneforlove_twoforjob2 小时前
【Java基础面试题025】什么是Java的Integer缓存池?
java·开发语言·缓存
xmh-sxh-13142 小时前
常用的缓存技术都有哪些
java
搬码后生仔2 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net