JAVA并发排队 Java并发之Synchronized机制详解
带着问题阅读
1、Synchronized如何使用,加锁的粒度分别是什么
2、Synchronized的实现机制是什么
3、Synchronized是公平锁吗
4、Java对Synchronized做了哪些优化
Synchronized介绍基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案 。这意味着在给定时刻只允许一个任务访问共享资源 。通常这是通过在代码前面加上一条锁语句来实现的,这就使得在一段时间内只有一个任务可以运行这段代码 。因为锁语句产生了一种互相排斥的效果,所以这种机制常常称为互斥量(mutex) 。
为防止资源冲突,Java提供了Synchronized用于解决互斥访问的问题 。当任务执行被Synchronized修饰的代码时,将先检查锁是否可用,然后获取锁、执行代码,最后释放锁 。考虑屋里有一个卫生间,多个人都需要单独使用,为了使用卫生间,每个人都先敲门,看看能否使用,如果没人使用他就进入卫生间并锁上门,当其它人来的时候就会被挡在门外 。
Synchronized使用方式
- 对象锁
synchronized可以用于修饰具体对象,如示例中分别对synObj和this对象加锁,即synObj和this分别作为共享资源被用于互斥访问,其中thread1和thread2同时访问synObj互斥,thread3和thread4同时访问this(demo对象)互斥 。public class Demo {private Object synObj = new Object();public void synObj() {// 对synObj对象加锁synchronized(synObj) {// 同步代码}}public void synThis() {// 对当前对象加锁synchronized(this) {// 同步代码}}}Demo demo = new Demo();// 假设以下四个线程同时运行new Thread(demo::synObj).start();// thread1new Thread(demo::synObj).start();// thread2new Thread(demo::synThis).start();// thread3new Thread(demo::synThis).start();// thread4- 普通方法锁
synchronized也可用于修饰方法,修饰方法时锁的对象即this,因此如果类的多个方法上都添加了synchronized,那么这几个方法在同步执行时也是互斥的 。public class Demo {public synchronized void test1() {};public synchronized void test2() {};}Demo demo = new Demo();// 以下两个线程同步执行时是互斥的,都需要获取demo对象的锁new Thread(demo::test1).start();new Thread(demo::test2).start();- 静态方法锁
Demo demo1 = new Demo()和Demo demo2 = new Demo()两个对象并分别调用,就不会产生互斥 。如要在多实例之间也达成互斥,则可以通过修饰静态方法来达成 。public class StaticDemo {private static Object obj = new Object();public void test() {synchronized(obj) {// 同步代码}}public static synchronized void testStatic() {};}// 两个线程互斥snew Thread(StaticDemo::testStatic).start();new Thread(StaticDemo::testStatic).start();- 类锁
synchronized修饰在静态方法时,也等价于修饰当前类对象 。public class StaticDemo {public void test() {synchronized(StaticDemo.class) {// 同步代码}}}Synchronized原理分析不论synchronized用于修饰哪里,本质还是会修饰到具体的对象(实例对象或类对象)上,synchronized的实现机制也是对对象的加锁 。Java中每个对象都隐含关联一个监视器ObjectMonitor,监视器通过cpp实现内置在JVM中,监视器地址记录在对象的MarkWord上,synchronized通过ObjectMonitor实现对象的锁操作 。对象头MarkWord简介JVM在内存中将对象划为三部分:对象头、实例数据和填充数据 。对象头分为
MarkWord和类型指针两部分,这里只针对锁相关做进一步介绍 。MarkWord用于存储对象自身的运行数据,如哈希值、GC分代年龄等,这部分在32位和64位虚拟机中会分别占用32位和64位空间,以下是32位的空间布局(64位布局相同,分的bit数不同),MarkWord会根据对象状态复用存储空间,例如对象未锁定状态下,采用25bit哈希 + 4bitGC年龄 + 1bit固定0 + 2bit标志存储 。当标志位为10表示对象处于重量级锁定时,剩余空间就用于存储ObjectMonitor对象的地址 。
文章插图
ObjectMonitor简介
ObjectMonitor() {..._count = 0; // 记录个数_owner = NULL;// 记录持有线程_cxq = NULL;// 记录锁阻塞线程,与EntryList配合_WaitSet = NULL;// 记录处于wait状态的线程_EntryList = NULL;// 记录处于锁阻塞状态的线程...}ObjectMonitor整体内容略去,核心关注以上字段 。_owner用于记录持有线程,_count用于记录重入次数,_cxq和_EntryList配合用于记录获取锁失败阻塞后的线程 。线程获取锁失败后会首先被挂载到
_cxq队列上并调用park阻塞 。当锁被释放时,如_EntryList不为空,则尝试唤醒_EntryList队首元素;如_EntryList为空,默认从_cxq摘取队首元素放入_EntryList并试图获取锁 。由于monitor锁机制为非公平锁,因此可能唤醒失败,两个队列都会保存阻塞元素 。详细解析可见参考第二篇文章

文章插图
Synchronized重量级锁原理
public class Demo {private Object obj = new Object();public void test() {synchronized(obj) {System.out.println("lock");}}}编译以上代码,javap -v查看字节码 。...public void test();Code:...monitorenter// 加锁...monitorexit// 释放锁...return...其余内容略去,关键在于monitorenter和monitorexit两个指令 。当执行
monitorenter时,将会尝试获取该对象monitor的所有权 。- 如果
monitor持有数为0即无线程持有,则直接获取monitor并将进入数+1; - 如果
monitor已被线程占有,检查是否为当前线程,如是当前线程,则将计数器+1;否则阻塞当前线程 。
monitorexit时,将monitor计数器-1,如减后为0,则线程释放monitor 。如
synchronized修饰在方法上,则会在方法上增加ACC_SYNCHRONIZED的标记,原理与上述相同 。JVM对Synchronized的优化
monitorenter和monitorexit依赖底层操作系统的mutex lock实现,该指令对线程的挂起和唤醒涉及到用户态到内核态的切换,如果同步代码频繁调用,会带来昂贵的切换开销 。自jdk1.6起对锁的实现引入了大量优化,下面来介绍一下都做了哪些优化 。锁消除锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除 。锁消除的主要判定一句来源于逃逸分析的数据支持,如果判断一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为是线程私有的,同步加锁无须进行 。
public String copyString(String s) {StringBuffer sb = new StringBuffer();sb.append(s);return sb.toString();}如示例代码,StringBuffer.append是通过sychronized修饰的线程安全操作,但在该代码块中,sb对象是局部变量,仅会被当前线程访问,不存在线程竞争,因此锁经过编译器检测后可以消除 。锁粗化原则上编写同步代码时,推荐将同步块的作用范围限制的尽量小,一方面减少同步代码块的执行时间,一方面减少同步竞争次数,以便存在竞争时,等待锁的线程可以尽快获得锁 。但是如果一系列连续操作都在对同一对象反复加锁和释放锁,那即使没有线程竞争也会产生很多没必要的开销 。
private Object obj = new Object();public void lock() {synchronized(obj) {...}// 再次加锁synchronized(obj) {...}}如上代码,连续两次对同一对象进行同步,即可将锁粗化合并为一个锁 。锁消除和锁粗化都是依赖JIT即时编译实现,因此通过
javac查看编译后的字节码,仍然保留着原始的锁指令 。自旋锁和自适应自旋前文中我们提到,互斥同步涉及的挂起/唤醒线程都涉及内核态转换,如果频繁产生竞争会带来很大的压力 。虚拟机开发团队注意到很多应用对锁的持有只会持续很短的时间,如果可以让竞争锁的线程稍等一下,不放弃处理器,就可以在持有锁的线程执行完毕后获取锁,避免产生空间切换,这就是自旋 。
自旋锁在jdk1.4.2中就引入,需要
-XX: +UseSpining开启,在jdk6以后就默认开启了 。自旋虽然避免了空间切换问题,但如果某个锁竞争很激烈或者锁的持有时间很长,那自旋只能白白占用处理器资源,因此在jdk1.6中引入了自适应自旋 。自适应意味着自旋的时间不再固定,如果对一个锁对象自旋等待刚刚成功过,则允许后续自旋等待较长时间;如果自旋很少成功,那就在后续获得锁的过程中直接跳过自旋 。偏向锁偏向锁也是jdk1.6引入的优化,目的是消除数据在无竞争情况下的同步原语 。锁被第一个线程获取后,在接下来的执行过程中,如果一直没有被其他线程获取,则持有偏向锁的线程不在需要同步 。
偏向锁加锁流程如下:
- 检查当前是否为偏向状态 。
- 如果是,检查当前线程ID与
Mark Word记录的线程ID是否一致,如一致则进入同步代码,不一致则释放偏向锁 - 如不是偏向锁,则使用
CAS尝试修改线程ID,如修改成功则进入同步代码,失败则释放偏向锁
偏向锁不会主动释放,只有当其他线程尝试获取锁时,才会检查持有线程是否可以释放锁 。如可以释放则替换为新线程ID,不可释放则升级为轻量级锁 。
【JAVA并发排队 Java并发之Synchronized机制详解】

文章插图
勘误:图中如判断对象头Mark Word记录非当前线程ID,下一步应当为开始偏向锁撤销而非CAS替换 。如有不同意见欢迎留言
轻量级锁轻量级锁在
MarkWord标志位中由00表示,轻量级锁首先在当前线程栈帧当中建立一个锁记录Lock Record,用于存储MarkWord的拷贝;然后虚拟机使用CAS操作将Lock Record的地址记录到MarkWord当中,并将标志位改为00,表示对象处于轻量级锁定状态 。如果更新失败,则会进入自旋并在自旋达到一定次数后升级为重量级锁 。自旋的同时如果有第三个线程尝试获取锁,也会直接升级到重量级锁 。同步代码执行完毕后,轻量级锁同样使用
CAS操作将栈帧中的MarkWord拷贝回到对象中,如果操作成功,则释放锁;如果替换失败,则说明有其他线程在竞争锁(意味着升级为重量级锁),则当前膨胀为重量锁转换为重量锁的释放 。
文章插图
重量级锁重量级锁即上文
Synchronized重量级锁原理所述内容,综上synchronized的加锁过程为偏向锁 -> 轻量级锁 -> 重量级锁,这个过程也称为锁膨胀 。
文章插图
图源自网络
总结最后总结对比一下几种锁实现 。
锁类型运行空间实现机制适用范围偏向锁用户态初次CAS加锁,后续如无竞争可直接进入单线程执行轻量级锁用户态CAS+自旋加锁锁竞争不激烈重量级锁内核态mutex 内核态操作锁激烈竞争参考
- 《深入理解JAVA虚拟机》
- synchronized 实现原理
- synchronized详解
- Java synchronized原理总结
- java偏向锁,轻量级锁与重量级锁为什么会相互膨胀?
- 消息称台积电主要客户在排队等待3nm制程工艺产能
- 当年卖25万提车还排队,如今标配2.0T+8AT,剑指迈腾仅售15万多
- 分娩期并发症有哪些你要知道
- 孕期胖得快的并发症排查事项
- 冬季幼儿易呕吐 小心这些呕吐并发症
- 老年人糖尿病容易出现哪些并发症
- java编程模拟器,java模拟器使用教程
- java获取计算机信息,js获取电脑硬件信息
- java 编写接口,java如何编写接口
- java鎺ユ敹纭欢鏁版嵁,java鑾峰彇linux纭欢淇℃伅
