synchronized运行原理以及优化
线程安全问题
- 线程不安全:
- 当多线程并发访问临界资源时(可共享的对象),如果破坏原子操作,可能会造成数据不一致。
- 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可以保证其正确性。
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
同步方式(1)
- 同步代码块:
synchronized(临界资源对象){ //对临界资源加锁
//代码(原子操作)
}
注:
每个对象都有一个互斥锁标记,用来分配给线程的。
只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
线程退出同步代码块时,会释放响应的互斥锁标记。
线程的状态(阻塞)

同步方式(2)
- 同步方法:
synchronized 返回值类型 方法名称(形参列表0){ //对当前对象(this)加锁
//代码(原子操作)
}
注:只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。
线程退出同步方法时,会释放相应的互斥锁标记。
特殊情况1
这种情况下方法由static修饰,因此synchronized锁的对象是类对象.(所谓类对象就是Java虚拟机中的一个模板对象,全局只有一个)
class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public static synchronized void b(){
System.out.Println("2");
}
}
public static void main(String[] args){
Number n1 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n1.b();}).start();
}
特殊情况2
这个时候b方法没有被static修饰,因此synchronized锁的对象是 this,也就是创建的实例对象,因此两个线程是同时执行
class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public synchronized void b(){
System.out.Println("2");
}
}
public static void main(String[] args){
Number n1 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n1.b();}).start();
}
特殊情况3
与上面情况相似,a方法被static修饰,锁的是类对象,b方法锁的是 this 实例对象,这个时候创建两个实例对象,会同时执行 a,b方法
class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public synchronized void b(){
System.out.Println("2");
}
}
public static void main(String[] args){
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n2.b();}).start();
}
特殊情况4
这时候 a,b方法都被static修饰,锁的是类对象,因此即使创建了两个实例对象,方法访问的依然是同一个对象即全局唯一的类对象,这时候会先执行a或先执行b
class Number{
public static synchronized void a(){
sleep(1);
System.out.Println("1");
}
public static synchronized void b(){
System.out.Println("2");
}
}
public static void main(String[] args){
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> {n1.a();}).start();
new Thread(() -> {n2.b();}).start();
}
同步规则
- 注意:
- 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。
- 如果调用不包含同步代码块的方法,或者普通方法时,则不需要锁标记,可直接调用。
- 已知JDK中线程安全的类:
- StringBuffer
- Vector
- Hashtable
- 以上类中的公开方法,均为synchronize修饰的同步方法。
Java对象头
以32位虚拟机为例
普通对象
- Klass Word包含了对象的类元信息

数组对象

其中Mark Word结构为

synchronized运行原理
Monitor
下图的Monitor被翻译为监视器或管程(由操作系统提供)
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针(即上图中的Heavyweight Locked指向monitor)

- 刚开始Monitor中Owner为null
- 当Thread-2执行到synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner
- 在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED
- Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争是非公平的
- 图中WaitSet中的Thread-0,Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程
注意:
- Synchronized必须是进入同一个对象的monitor才会有上述效果
- 不加synchronized的对象不会关联监视器,不遵从上述规则
为什么说重量级锁开销大呢
主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
这就是说为什么重量级线程开销很大的。
synchronized锁优化
轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(无竞争),那么可以使用轻量级锁来优化
轻量级锁对使用者是透明的,即语法仍然是synchronized
例如:
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
public static void method1(){
synchronized(obj){
//同步块B
}
}
- 当执行到method1()时,会创建一个锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

MarkWord的结构:

- 接下来会让锁记录中的Object reference指向锁对象,并尝试用cas替换Object的Mark word, 将 Mark Word的值存入锁记录

- 如果cas替换成功,对象头中存储了
锁记录地址和状态 00,表示由该线程给对象加锁,将来解锁的时候再将Mark word 恢复回去

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

发现object已经加锁了,但是锁的地址是自己,那么再添加一条Lock Record作为重入的计数
- 当退出synchronized代码块(解锁)时,如果有取值为null的锁记录,表示有重入,这时重置锁记录,表示重入计数-1

- 当退出synchronized代码块(解锁)时,锁记录的值不为null,这时使用cas将Mark Word的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
static Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步代码块
}
}
- 当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁

- 这时Thread-1加轻量级锁失败,进入锁膨胀流程
- 即为Object对象申请Monitor锁,让Object指向重量级锁地址
- 然后自己进入Monitor的EntryList BLOCKED

- 当Thread-0退出同步代码块解锁时,使用cas将Mark Word的值恢复给对象头,失败
- 这时会进入重量级解锁流程,即按照Monitor地址找到Minitor对象,设置Owner为null,唤醒EntryList中BLOCKED线程
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经推出了同步块,释放了锁),这时当前线程就可以避免阻塞.
自旋重试成功的情况:

注意:
只有多核cpu下,自旋优化才有意义,因为单核下没有多余cpu可以给其他线程用来自旋了
自旋重试失败的情况:

- 在Java6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋
- Java7之后不能控制是否开启自旋功能
偏向锁
轻量级锁在没有竞争时,每次重入仍然需要执行CAS操作
Java6中引入了偏向锁来进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归线程所有
例如:
static final Object obj = new Object();
public static void method1(){
synchronized(obj){
//同步块A
method2();
}
public static void method1(){
synchronized(obj){
//同步块B
method3();
}
public static void method3(){
synchronized(obj){
//同步块C
}
}


偏向状态

一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,Mark Word值为0x05即最后3位为101,这时它的thread,epoch,age都为0
- 偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加VM参数
-XX:BiasedLockingStartupDelay=0来禁用延迟 - 如果没有开启偏向锁,那么对象创建后,Mark Word值为0x01即最后3位位001,这时它的hashcode,age都为0,第一次用到hashcode时才会赋值
- 同时可以通过添加VM参数:
-XX:-UseBiasedLocking来禁用偏向锁 - 如果说在加锁之前调用了对象的
hashCode()方法,此时会禁用这个对象的偏向锁- 因为哈希码用的时候才产生,默认是0;只有第一次调用对象的hashcode,对象的哈希码才会产生,才在对象头的Mark Word里填充哈希码,只有当Mark Word处于正常状态时才能填充hashCode,因此会禁用掉Biased(偏向锁)
- 当有其他线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程T1的对象仍有机会重新偏内T2,重偏向会重置对象的Thread ID
当撤销偏向锁阈值超过20次后,JVM会认为自己是不是偏向错了,于是会再给这些对象加锁时重新偏向至加锁线程
批量撤销
当撤销偏向锁阈值超过40次之后,JVM就会觉得自己确实偏向错了,根本就不该偏向.于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
锁清除
我们知道Java程序是一边执行一边编译的,在JVM即时编译过程中会去扫描这些代码,做逃逸分析,看看那些对象是线程安全的,那么对于这些对象的访问就不用做同步操作,减少锁带来的性能损失。
逃逸分析具体有对线程的逃逸分析和对方法的逃逸分析
拿方法逃逸举例,JVM检测某个对象在方法中被定义之后,会不会作为参数被外部方法引用
如果不会,表示该对象是线程安全的,那对象不会逃逸到方法之外,那么对该变量的访问就可以不做同步操作。
事实上JVM就是这么做,在程序中对于一些不存在竞争的代码段,减少同步操作可以提升执行速度,用逃逸分析来得出那些不存在竞争的代码段,然后消除一些锁操作。
逃逸分析可以用参数-XX:+EscapeAnalysis来开启,然后参数-XX:+EliminateLocks来开启锁消除
锁粗化
大部分情况下,我们对一段程序加锁需要尽量缩小范围,因为如果范围太大,执行时间也会增加。但是也有特殊情况,我们看看下面这种情况。
public class LockDemo {
public void fun() {
for (int i = 0; i < 10000; ++i) {
synchronized(LockDemo.class) {
System.out.println("test...");
}
}
}
}
上面的for循环整个过程都在频繁加锁和释放锁,非常耗费性能,因此JIT将会把加锁的过程优化到for循环外面,如下面的代码所示,这就是锁粗化的过程。
public class LockDemo {
public void fun() {
synchronized(LockDemo.class) {
for (int i = 0; i < 10000; ++i) {
System.out.println("test...");
}
}
}
}
synchronized运行原理以及优化的更多相关文章
- synchronized实现原理及其优化-(自旋锁,偏向锁,轻量锁,重量锁)
1.synchronized概述: synchronized修饰的方法或代码块相当于并发中的临界区,即在同一时刻jvm只允许一个线程进入执行.synchronized是通过锁机制实现同一时刻只允许一个 ...
- synchronized原理及优化,(自旋锁,锁消除,锁粗化,偏向锁,轻量级锁)
偏向锁:不占用CPU自旋锁:占用CPU.代码执行成本比较低且线程数少时,可以使用 .不经过OS.内核态,效率偏低 理解Java对象头与Monitor 在JVM中,对象在内存中的布局分为三块区域:对象头 ...
- Synchronized用法原理和锁优化升级过程(面试)
简介 多线程一直是面试中的重点和难点,无论你现在处于啥级别段位,对synchronized关键字的学习避免不了,这是我的心得体会.下面咱们以面试的思维来对synchronized做一个系统的描述,如果 ...
- synchronized实现原理
线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据.因此为了解决这个问题,我们可能需要这样一个方案, ...
- jdk源码剖析二: 对象内存布局、synchronized终极原理
很多人一提到锁,自然第一个想到了synchronized,但一直不懂源码实现,现特地追踪到C++层来剥开synchronized的面纱. 网上的很多描述大都不全,让人看了不够爽,看完本章,你将彻底了解 ...
- 深入理解Java并发之synchronized实现原理
深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...
- 深入理解并发编程之----synchronized实现原理
版权声明:本文为博主原创文章,请尊重原创,未经博主允许禁止转载,保留追究权 https://blog.csdn.net/javazejian/article/details/72828483 [版权申 ...
- Java并发(五):synchronized实现原理
一.synchronized用法 Java中的同步块用synchronized标记. 同步块在Java中是同步在某个对象上(监视器对象). 所有同步在一个对象上的同步块在同时只能被一个线程进入并执行操 ...
- synchronized底层原理
synchronized底层语义原理 Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现. 在 Java 语言中,同步用的最多的地方可能是被 syn ...
随机推荐
- Android Studio配置colors.xml
•colors.xml <?xml version="1.0" encoding="utf-8"?> <resources> <! ...
- Apache Hudi核心概念一网打尽
1. 场景 https://hudi.apache.org/docs/use_cases.html 近实时写入 减少碎片化工具的使用 CDC 增量导入 RDBMS 数据 限制小文件的大小和数量 近实时 ...
- Docker系列——InfluxDB+Grafana+Jmeter性能监控平台搭建(二)
在上一篇博文中,主要是讲了InfluxDB的配置,博文链接:https://www.cnblogs.com/hong-fithing/p/14453695.html,今天来分享下Jmeter的配置. ...
- C# List常用方法及Dictionary常用方法汇总
本文主要汇总了在开发过程中,使用List和Dictionary常用的方法,例如增.删.改.查.排序等等各种常用操作. 在平时的开发过程中,List和Dictionary是我们经常使用到的数据结构,而且 ...
- java面试-谈谈你对volatile的理解
一.volatile特性: volatile是Java虚拟机提供的轻量级的同步机制.主要有三大特性: 保证可见性 不保证原子性 禁止指令重排序 1.保证可见性 1)代码演示 AAA线程修改变量numb ...
- php添加excel更新数据表数据
公司有个需求,是用excel更新数据的,把错误的行列放到一个数组返回出来,正常的数据则插入,且返回数量 1.先需要引入phpspreadsheet,这里使用composer 安装 composer r ...
- 一文简述JAVA内部类和异常
内部类和异常 内部类 在一个类的内部定义的一个类,例如,A类中定义了一个B类,则B类相对于A类就是内部类,而A类相对于B类就是外部类 成员内部类 静态内部类 局部内部类 匿名内部类 成员内部类 pub ...
- 通过Fiddler 远程 对 安卓手机 iPhone 苹果手机 访问请求抓包 Android IOS14.4 fiddler代理 无法联网
Fiddler 中的设置 1 查看 Fiddler所在 电脑的内网 ip地址. (cmd > ipconfig 查看本机ipv4地址) 2 Fiddler 设置 允许远程设备连接: Fiddl ...
- lvs 负载均衡 _DR模式 _Python脚本
import paramiko vip='192.168.83.6' # 虚拟IP # direct_server_information ds_info={ 'ip':'192.168.83.5', ...
- Day17_104_IO_BufferReader接收用户输入
BufferReader接收用户输入 **标准输入\输出流 java.lang.System** - static InputStream in - static PrintStream out im ...