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. [Wireshark]_001_入门

    Wireshark(前称Ethereal)是一个网络封包分析软件.网络封包分析软件的功能是撷取网络封包,并尽可能显示出最为详细的网络封包资料.Wireshark使用WinPCAP作为接口,直接与网卡进 ...

  2. 09 . Python3之常用模块

    模块的定义与分类 模块是什么? 一个函数封装一个功能,你使用的软件可能就是由n多个函数组成的(先备考虑面向对象).比如抖音这个软件,不可能将所有程序都写入一个文件,所以咱们应该将文件划分,这样其组织结 ...

  3. PAT 1036 Boys vs Girls (25分) 比大小而已

    题目 This time you are asked to tell the difference between the lowest grade of all the male students ...

  4. Parsing techniques: a practical guide下载

    轮子哥隆重推荐的书,一行代码.一句公式都没有,但是却什么都讲明白了的:<Parsing Techniques>.第一版官网免费下载,第二版多出来的东西你们用不上不用看了.全书只讲parsi ...

  5. 可以Postman,也可以cURL.进来领略下cURL的独门绝技

    文章已经收录在 Github.com/niumoo/JavaNotes ,更有 Java 程序员所需要掌握的核心知识,欢迎Star和指教. 欢迎关注我的公众号,文章每周更新. cURL 是一个开源免费 ...

  6. Java实现 LeetCode 162 寻找峰值

    162. 寻找峰值 峰值元素是指其值大于左右相邻值的元素. 给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引. 数组可能包含多个峰值,在这种情况下,返 ...

  7. Java实现 LeetCode 89 格雷编码

    89. 格雷编码 格雷编码是一个二进制数字系统,在该系统中,两个连续的数值仅有一个位数的差异. 给定一个代表编码总位数的非负整数 n,打印其格雷编码序列.格雷编码序列必须以 0 开头. 示例 1: 输 ...

  8. Java实现 LeetCode 63 不同路径 II(二)

    63. 不同路径 II 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为"Start" ). 机器人每次只能向下或者向右移动一步.机器人试图达到网格的右下角(在 ...

  9. java中Runtime类详细介绍

    Runtime类描述了虚拟机一些信息.该类采用了单例设计模式,可以通过静态方法 getRuntime()获取Runtime类实例.下面演示了获取虚拟机的内存信息: package Main; publ ...

  10. java实现算年龄

    英国数学家德摩根出生于19世纪初叶(即18xx年). 他年少时便很有才华.一次有人问他的年龄,他回答说: "到了x的平方那年,我刚好是x岁". 请你计算一下,德摩根到底出生在哪一年 ...