1 为什么需要synchronized?

当一个共享资源有可能被多个线程同时访问并修改的时候,需要用锁来保证数据的正确性。请看下图:



线程A和线程B分别往同一个银行账户里面添加货币,A线程从内存中读取(read)当前账户金额($=0)到线程A的本地栈,进行+100的操作后,这时B线程也从内存中读取当前金额($=0)到线程B的本地栈,并且进行+200的操作后写回主存,线程B前脚刚写回之后,后脚线程A又把$=100写会到本地内存中。我们顺便来复习一下JMM内存模型的8个原子操作

  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入):作用于工作内存的变量,它把read操作从主内存得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值赋值给主内存的变量。

我们知道,volatile关键字只能保证变量的有序性可见性,但是不能保证原子性。在这个例子中,即使给$变量加上volatile关键字也是不顶用的,原因可见volatile为什么不能保证原子性以及知乎提问:volatile为什么不能保证原子性

这时候就轮到我们的synchronized关键字出场了,它可以在访问竞态资源时加锁,从而保证修改的时候不会出错。它有三种作用范围:

  • 在静态方法上加锁,锁住的是.class对象
  • 在非静态方法上枷锁,锁住的是当前对象this
  • 在代码块上加锁,锁住的是一个Object对象,比如monitor

2 JDK6之前 synchronized 的实现原理

在JDK6以前,synchronized还属于重量级锁,每次枷锁都依赖操作系统Mutex Lock实现,涉及到操作系统让线程从用户态切换到内核态,切换成本很高。在JDK6以后,研究人员引入了偏向锁和轻量级锁,因为Sun公司的程序员发现大部分程序大多数时间都不会发生多个线程同时访问竞态资源的情况,每次线程都加锁解锁,每次这么搞都要操作系统在用户态和内核态之前来回切,太耗性能了。

首先要了解synchronized的实现原理,需要理解二个预备知识:

  • Java对象头的结构。对象存储在堆中,主要分为三部分内容,对象头、对象实例数据和对齐填充(数组对象多一个区域:记录数组长度)。HotSpot虚拟机的对象头分为两部分,第一部分用来存储对象自身的运行时数据,如哈希码,GC分代年龄。这部分数据的长度在32位和64位的Java虚拟机中分别会占用32个或64个比特,官方称它为"Mark Word"。这部分是实现轻量级锁和偏向锁的关键。另一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用于存储数组的长度。

    考虑到Java虚拟机的空间使用效率,Mark Word被设计成一个非固定的动态数据结构,以便在极小的空间内存储尽量多的信息。它会根据对象的状态复用自己的存储空间。32位HotSpot虚拟机中,对象未被锁定的状态下,Mark Word的32个比特空间里的25个比特用来存储对象哈希码,4个比特用于存储对象分代年龄,2个比特用于存储锁标志位,还有1个比特固定为0(表示未进入偏向模式)。对象除了未被锁定的正常状态外,还有轻量级锁定、重量级锁定、GC标记、可偏向等几种不同的状态Mark Word结构如下图所示:

  • Monitor。每个对象都有一个与之关联的Monitor 对象;Monitor对象属性如下所示(Hospot 1.7 代码) 。
//下图详细介绍重要变量的作用
ObjectMonitor() {
_header = NULL;
_count = 0; // 重入次数
_waiters = 0, // 等待线程数
_recursions = 0;
_object = NULL;
_owner = NULL; // 当前持有锁的线程
_WaitSet = NULL; // 调用了 wait 方法的线程被阻塞 放置在这里
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 等待锁 处于block的线程 有资格成为候选资源的线程
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

对象关联的 ObjectMonitor 对象有一个线程内部竞争锁的机制,如下图所示:

下面我们就来分析一下JDK6之前的synchronized具体的实现逻辑。

  1. 当有二个线程A、线程B都要开始给账户的经济money变量加钱,要进行操作的时候 ,发现方法上加了synchronized锁,这时线程调度到A线程执行,A线程就抢先拿到了锁。拿到锁的步骤为:
  • MonitorObject 中的_owner设置成 A线程
  • Mark Word 设置为 Monitor 对象地址,锁标志位改为10;
  • 将B线程阻塞放到ContentionList队列
  1. JVM 每次从Waiting Queue 的尾部取出一个线程放到OnDeck作为候选者,但是如果并发比较高,Waiting Queue会被大量线程执行CAS操作,为了降低对尾部元素的竞争,将Waiting Queue拆分成ContentionListEntryList 二个队列, JVM将一部分线程移到EntryList 作为准备进OnDeck的预备线程。另外说明几点:
  • 所有请求锁的线程首先被放在ContentionList这个竞争队列中;
  • Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
  • 任意时刻,最多只有一个线程正在竞争锁资源,该线程被称为 OnDeck;
  • 当前已经获取到所资源的线程被称为 Owner;
  • 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的);
  1. 作为Owner的A线程执行过程中,可能调用wait释放锁,这个时候A线程进入 Wait Set , 等待被唤醒。

以上就是synchronized 在 JDK 6之前的实现原理。

另外,synchronized在在线程竞争锁时,首先做的不是直接进ContentionList队列排队,而是尝试自旋获取锁(可能ContentionList 有别的线程在等锁),如果获取不到才进入 ContentionList,这明显对于已经进入队列的线程是不公平的,所以synchronized是非公平锁。另一个不公平的是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。

3 JDK6之后synchronized优化

那么JDK6对synchronized做了哪些优化呢?

3.1 偏向锁

  • 如果当前虚拟机启用了偏向锁(启用参数-XX:+UseBiasedLocking,这是自JDK6起HotSpot虚拟机的默认值),那么当锁对象第一次被线程获取的时候,虚拟机会把对象头中的标志位设为01、把偏向模式设置为1,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时。虚拟机都可以不再进行任何同步操作(例如加锁、解锁以及对Mark Word的更新操作等)。
  • 一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束,根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设为0),撤销后标志位恢复到未锁定(01)或轻量级锁定(00)的状态,后续的同步操作就按照轻量级锁进行。

3.2 轻量级锁

3.2.1 加锁

  • 在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为01状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录的空间,用于存储锁对象目前的Mark Word拷贝(官方为这份拷贝加了个前缀Displaced)。
  • 然后虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为00,表示此对象处于轻量级锁定状态。
  • 如果这个操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧。如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。当前线程会进行自旋(?)。如果出现两条以上的线程争用同一个锁,或者当前线程自旋失败(尝试到一定次数,默认10次)的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为10,此时Mark Word中存储的就是指向重量级锁监视器ObjectMonitor(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态

    下图是轻量级锁CAS操作前后堆栈与对象的状态:

3.2.2 解锁

轻量级锁的解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获取该锁(膨胀为重量级锁,Mark Word指向了互斥量),就要在释放重量级锁的同时,唤醒被挂起的线程

3.2.3 使用条件

轻量级锁能提升程序同步性能的依据是“对于绝大部分锁,在同步周期内都是不存在竞争的”这一经验法则。如果没有竞争,轻量级锁通过CAS操作成功避免了使用互斥量的开销;但如果确实存在竞争,除了互斥量本身的开销之外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。

整个锁升级的过程如下图所示:

参考资料:微信公众号:安琪拉的博客

synchronized与锁升级的更多相关文章

  1. synchronized的锁升级/锁膨胀

    偏向锁 偏向第一个拿到锁的线程. 即第一个拿到锁的线程,锁会在对象头 Mark Word 中通过 CAS 记录该线程 ID,该线程以后每次拿锁时都不需要进行 CAS(指轻量级锁). 如果该线程正在执行 ...

  2. 深入并发锁,解析Synchronized锁升级

    这篇文章分为六个部分,不同特性的锁分类,并发锁的不同设计,Synchronized中的锁升级,ReentrantLock和ReadWriteLock的应用,帮助你梳理 Java 并发锁及相关的操作. ...

  3. synchronized中锁是怎么升级的

    在JDK1.6以前,使用synchronized就只有一种方式即重量级锁,而在JDK1.6以后,引入了偏向锁,轻量级锁,重量级锁,来减少竞争带来的上下文切换. 锁升级主要依赖对象头中的Mark Wor ...

  4. Synchronized锁升级原理与过程深入剖析

    Synchronized锁升级原理与过程深入剖析 前言 在上篇文章深入学习Synchronized各种使用方法当中我们仔细介绍了在各种情况下该如何使用synchronized关键字.因为在我们写的程序 ...

  5. Synchronized锁性能优化偏向锁轻量级锁升级 多线程中篇(五)

    不止一次的提到过,synchronized是Java内置的机制,是JVM层面的,而Lock则是接口,是JDK层面的 尽管最初synchronized的性能效率比较差,但是随着版本的升级,synchro ...

  6. Synchronized锁升级

    Synchronized锁升级 锁的4中状态:无锁状态.偏向锁状态.轻量级锁状态.重量级锁状态(级别从低到高) 为什么要引入偏向锁? 因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞 ...

  7. 关于Synchronized的偏向锁,轻量级锁,重量级锁,锁升级过程,自旋优化,你该了解这些

    前言 相信大部分开发人员,或多或少都看过或写过并发编程的代码.并发关键字除了Synchronized(如有不懂请移至传送门,关于Synchronized的偏向锁,轻量级锁,重量级锁,锁升级过程,自旋优 ...

  8. 再谈synchronized锁升级

    在图文详解Java对象内存布局这篇文章中,在研究对象头时我们了解了synchronized锁升级的过程,由于篇幅有限,对锁升级的过程介绍的比较简略,本文在上一篇的基础上,来详细研究一下锁升级的过程以及 ...

  9. 详细了解 synchronized 锁升级过程

    前言 首先,synchronized 是什么?我们需要明确的给个定义--同步锁,没错,它就是把锁. 可以用来干嘛?锁,当然当然是用于线程间的同步,以及保护临界区内的资源.我们知道,锁是个非常笼统的概念 ...

随机推荐

  1. C#基础之构造函数(构造器)

    在每个类里面默认都有一个构造方法,正式因为有了这些方法,你未赋值的变量才会有初始值,当然,我们也可以手动自己创建构造函数,可以创建多个构造函数,自己给出默认值或者!!!规定调用此类的程序对象必须要赋值 ...

  2. 01Java核心-冷门知识001-包

    1)导入静态方法和静态域 import 可以加上static关键字,导入静态的方法和静态域. 例如: package com.gail.test; import static java.lang.Sy ...

  3. 小谢第2问:后端返回为数组list时候,怎么实现转为tree

    要求后端返回给我的list时候,在数组中定义有id , parentid, 可以用双重循环的方法,得到tree需要的数据结构,这样得到的数据就可以直接复制给树组件的data啦const oldData ...

  4. jchdl - GSL实例 - LogicalLeft

    https://mp.weixin.qq.com/s/WNm4bLWzZ0oWHWa7HQ6Y6w   逻辑左移,继承自Shifter类.只需要实现shift方法即可.   参考链接 https:// ...

  5. 【Mybatis plus 3.2】怎么操作?看看我!(update、limit、between)

    必须是springboot工程 在pom.xml中添加 <dependency> <groupId>com.baomidou</groupId> <artif ...

  6. Java实现 蓝桥杯VIP 算法提高 师座操作系统

    算法提高 师座操作系统 时间限制:1.0s 内存限制:256.0MB 问题描述 师座这天在程序设计课上学了指针和结构体以后,觉得自己可以轻松的写出操作系统,为了打败大微软帝国,他给这个系统起了个响亮的 ...

  7. Java实现 蓝桥杯VIP 算法提高 打水问题

    算法提高 打水问题 时间限制:1.0s 内存限制:512.0MB 问题描述 N个人要打水,有M个水龙头,第i个人打水所需时间为Ti,请安排一个合理的方案使得所有人的等待时间之和尽量小. 输入格式 第一 ...

  8. Java实现 蓝桥杯VIP 算法提高 插入排序

    算法提高 插入排序 时间限制:1.0s 内存限制:256.0MB  插入排序 问题描述 排序,顾名思义,是将若干个元素按其大小关系排出一个顺序.形式化描述如下:有n个元素a[1],a[2],-,a[ ...

  9. Java实现 LeetCode 95 不同的二叉搜索树 II(二)

    95. 不同的二叉搜索树 II 给定一个整数 n,生成所有由 1 - n 为节点所组成的二叉搜索树. 示例: 输入: 3 输出: [ [1,null,3,2], [3,2,null,1], [3,1, ...

  10. zabbix 邮箱告警

    脚本内容 #!/bin/env python #coding:utf- import smtplib from email.mime.text import MIMEText from sys imp ...