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(); // thread1
new Thread(demo::synObj).start(); // thread2
new Thread(demo::synThis).start(); // thread3
new 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() {};
}
// 两个线程互斥s
new 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,不可释放则升级为轻量级锁。
勘误:图中如判断对象头Mark Word记录非当前线程ID,下一步应当为开始偏向锁撤销而非CAS替换。如有不同意见欢迎留言
轻量级锁
轻量级锁在MarkWord
标志位中由00表示,轻量级锁首先在当前线程栈帧当中建立一个锁记录Lock Record
,用于存储MarkWord
的拷贝;然后虚拟机使用CAS
操作将Lock Record
的地址记录到MarkWord
当中,并将标志位改为00,表示对象处于轻量级锁定状态。如果更新失败,则会进入自旋并在自旋达到一定次数后升级为重量级锁。自旋的同时如果有第三个线程尝试获取锁,也会直接升级到重量级锁。
同步代码执行完毕后,轻量级锁同样使用CAS
操作将栈帧中的MarkWord
拷贝回到对象中,如果操作成功,则释放锁;如果替换失败,则说明有其他线程在竞争锁(意味着升级为重量级锁),则当前膨胀为重量锁转换为重量锁的释放。
重量级锁
重量级锁即上文Synchronized重量级锁原理
所述内容,综上synchronized
的加锁过程为偏向锁 -> 轻量级锁 -> 重量级锁,这个过程也称为锁膨胀。
图源自网络
总结
最后总结对比一下几种锁实现。
锁类型 | 运行空间 | 实现机制 | 适用范围 |
---|---|---|---|
偏向锁 | 用户态 | 初次CAS加锁,后续如无竞争可直接进入 | 单线程执行 |
轻量级锁 | 用户态 | CAS+自旋加锁 | 锁竞争不激烈 |
重量级锁 | 内核态 | mutex 内核态操作 | 锁激烈竞争 |
参考
Java并发之Synchronized机制详解的更多相关文章
- 【转】java的动态代理机制详解
java的动态代理机制详解 在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们 ...
- java的动态代理机制详解-----https://www.cnblogs.com/xiaoluo501395377/p/3383130.html
java的动态代理机制详解-----https://www.cnblogs.com/xiaoluo501395377/p/3383130.html
- java 深拷贝与浅拷贝机制详解
概要: 在Java中,拷贝分为深拷贝和浅拷贝两种.java在公共超类Object中实现了一种叫做clone的方法,这种方法clone出来的新对象为浅拷贝,而通过自己定义的clone方法为深拷贝. (一 ...
- JAVA中的GC机制详解
优秀Java程序员必须了解的GC工作原理 一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只 ...
- java的动态代理机制详解
在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的 ...
- Java的动态代理机制详解(转)
在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一个就是AOP,对于IoC,依赖注入就不用多说了,而对于Spring的核心AOP来说,我们不但要知道怎么通过AOP来满足的 ...
- (转)java的动态代理机制详解
原文出自:http://www.cnblogs.com/xiaoluo501395377/p/3383130.html 在学习Spring的时候,我们知道Spring主要有两大思想,一个是IoC,另一 ...
- [转载] java的动态代理机制详解
转载自http://www.cnblogs.com/xiaoluo501395377/p/3383130.html 代理模式 代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代 ...
- Java虚拟机:类加载机制详解
版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! 大家知道,我们的Java程序被编译器编译成class文件,在class文件中描述的各种信息,最终都需要加载到虚拟机内存才能运行和使用,那么 ...
随机推荐
- 声明提前(hoist)
程序执行前,都会先找到var声明的变量和function声明的函数. 一.var声明的变量 程序 结果 console.log(a); var a=10; console.log(a); //unde ...
- ES6新特征
1.块级作用域 { } 就是块级作用域,还包括if.else.for.while...下都属于块级作用域. let 声明的变量不存在变量的提升,不允许let反复声明同一个变量:块级作用域下let ...
- 通过比较rgb破解滑动验证码
目标url:信用中国(广东中山)行政处罚 /** * base64转BufferedImage * * @param base64 * @return */ public static Buffere ...
- 【NLP学习其四】如何构建自己用于训练的数据集?什么是词性标注?
数据集与词性标注 数据集是NLP中的重要一环. 但是提到数据集,很多人的第一个想法可能是:"这玩意从网上下载就好了,不用管". 真的不用管?最开始我也是这么认为的 于是我直奔CoN ...
- Mol Cell | 张令强/贺福初/魏文毅/刘翠华揭示线性泛素化调控血管生成新机制
景杰学术 | 报道 泛素化修饰作为主要的蛋白质翻译后修饰之一,与细胞周期.应激反应.信号传导和DNA损伤修复等几乎所有的生命活动密切相关[1].泛素分子通常含有7个赖氨酸残基,通过这些残基可以和其他泛 ...
- 大写字母与小写字母 相差 一个 空格 A = 65 a = 97 空格 = 32
1 //大写字母与小写字母 相差 一个 空格 A = 65 a = 97 空格 = 32 2 3 #include <stdio.h> 4 int main() 5 { 6 7 char ...
- 本以为精通Android事件分发机制,没想到被面试官问懵了
文章中出现的源码均基于8.0 前言 事件分发机制不仅仅是核心知识点更是难点,并且还是View的一大难题滑动冲突解决方法的理论基础,因此掌握好View的事件分发机制是十分重要的. 一.基本认识 1. 事 ...
- SpringCloud升级之路2020.0.x版-11.Log4j2 监控相关
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford Log4j2 异步 ...
- Java集合框架和数组的排序(转载)
Java集合框架(*Collection)*和数组的排序 根据约定,在使用java编程的时候应尽可能的使用现有的类库,当然你也可以自己编写一个排序的方法,或者框架,但是有几个人能写得比JDK里的还 ...
- Arduino连接L298n驱动板驱动小车的电机
1.L298N介绍 先来讲讲电机驱动,驱动一般使用L298N,L298N 是一种双H桥电机驱动芯片,其中每个H桥可以提供2A的电流,功率部分的供电电压范围是2.5-48v,逻辑部分5v供电,接受5vT ...