Synchronized 实现原理
记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized。对于当时的我们来说,synchronized是如此的神奇且强大。我们赋予它一个名字“同步”,也成为我们解决多线程情况的良药,百试不爽。但是,随着学习的深入,我们知道synchronized是一个重量级锁,相对于Lock,它会显得那么笨重,以至于我们认为它不是那么的高效。随着Javs SE 1.6对synchronized进行各种优化后,synchronized不会显得那么重。
synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法进入临界区,同时还可以保证共享变量的内存可见性。
1、实现原理
Java中的每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法,锁是当前实例对象。
- 静态同步方法,锁时当前类的Class对象。
- 同步代码块,锁是synchronized括号里配置的对象。
当一个线程访问同步代码块时,必须先获得锁才能执行同步代码,当退出或者跑出异常时必须释放锁。我们会疑惑锁到底存在哪里?锁里面又会保存什么信息?
JVM规范定义:JVM基于进入与退出Monitor对象与来实现方法同步和代码块同步:
- 代码块同步:使用monitorenter和monitorexit指令实现。
- 方法同步:使用另外一种方式,但是同样是使用这两个指令实现。只是具体表现形式有所不同
public class SynchronizedTest {
public synchronized void test1() {
}
public void test2() {
synchronized (this) {
}
}
}
使用javap -verbose SynchronizedTest.class工具查看生成的class文件信息分析synchronized实现:
省略部分代码,如下所示:
{
public com.zero.test.SynchronizedTest();
.....
public synchronized void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 5: 0
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
......
}
从上面可以看出同步代码块使用monitorenter和monitorexit指令实现的,同步方法使用方法修饰符上的ACCSYNCHRONIZED实现。无论哪一种方式,其本质都是一个对象的监视器(monitor)进行获取。
在我们继续分析深入之前,要先了解两个概念:Java对象头、Monitor。
1.1、 Java对象头
synchronized用的锁是存在Java对象头里的,Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。
Mark Word
Java对象头里的Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。Java对象头一般占有两个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit),但是如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。,32位 JVM 的Mark Word的默认存储结构如下表格:
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
---|---|---|---|---|
无锁状态 | 对象的hashCode | 对象的分代年龄 | 0 | 01 |
在运行期间,Mark Word里存储的数据随着锁标志位的变化而变化
Monitor
我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。
与一切皆对象一样,所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
2、synchronize性能优化
我们知道synchronized是重量级锁,效率不怎么滴,同时这个观念也一直存在我们脑海里,不过在JDK 1.6中对synchronize的实现进行了各种优化,使得它显得不是那么重了,那有哪些优化手段?
2.1、 锁优化
JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
2.1.1、 自旋锁
线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间,为了这一段很短的时间频繁地阻塞和唤醒线程是非常不值得的。
何谓自旋锁?
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。
2.1.2、 适应自旋锁
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
它怎么做呢?
线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
2.1.3、 锁消除
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。锁消除的依据是逃逸分析的数据支持。
我们在使用一些JDK的内置API时,如StringBuffer、Vector、HashTable等,这个时候会存在隐形的加锁操作。
比如StringBuffer的append()方法,Vector的add()方法:
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for(int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
在运行这段代码时,JVM可以明显检测到变量vector没有逃逸出方法vectorTest()之外,所以JVM可以大胆地将vector内部的加锁操作消除。
2.1.4、锁粗化
我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小,仅在共享数据的实际作用域中才进行同步。这样做的目的是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。 在大多数的情况下,上述观点是正确的。但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗化的概念。
那什么是锁粗化?
就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
如上面实例:vector每次add的时候都需要加锁操作,JVM检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到for循环之外。
2.1.5、偏向锁
引入偏向锁主要目的是:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。轻量级锁的加锁解锁操作是需要依赖多次CAS原子指令的。那么偏向锁是如何来减少不必要的CAS操作呢?我们可以查看Mark work的结构就明白了。其实只要简单的测试下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败:则需要再测试下偏向锁标识是否设置成1(表示当前是偏向锁),如果没有设置,那么只能使用CAS竞争锁了,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
流程如下:
- 检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;
- 若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);
- 如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);
- 通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;
- 执行同步代码块。
释放锁偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是没有正在执行的代码)。
其步骤如下:
- 暂停拥有偏向锁的线程,判断锁对象石是否还处于被锁定状态;
- 撤销偏向锁,恢复到无锁状态(01)或者轻量级锁的状态。
2.1.6、轻量级锁
引入轻量级锁的主要目的是在只有少量线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:
- 判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
- JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
- 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
释放锁轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:
- 取出在获取轻量级锁保存在Displaced Mark Word中的数据;
- 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,否则执行(3);
- 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。
2.1.7、重量级锁
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
开设这个公众号将自己的知识分享,以后会持续输出,希望给读者朋友们带来帮助。客官觉得有用请点赞或收藏,关注公众号JavaStorm,你将发现一个有趣的灵魂。
3、 锁的对比
下面对偏向锁、轻量级锁和重量级锁进行比较:
表3 各种锁的优缺点及适用场景
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,与执行非同步方法仅存在纳秒级的差距 | 如果线程间存在竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的情况 |
轻量级锁 | 竞争的线程不会堵塞,提高了程序的响应速度 | 始终得不到锁的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常块,只有两个线程竞争锁 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程堵塞,响应时间缓慢 | 追求吞吐量,同步块执行速度比较慢,竞争锁的线程大于2个 |
表格来源:《Java并发编程的艺术》第16页
客官觉得有用请点赞或收藏,关注公众号JavaStorm,你将发现一个有趣的灵魂。祝大家有个愉快的周末!
关注公众号 JavaStorm 获取最新内容
参考资料:
- 周志明:《深入理解Java虚拟机》
- 方腾飞:《Java并发编程的艺术》
Synchronized 实现原理的更多相关文章
- synchronized实现原理
线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据.因此为了解决这个问题,我们可能需要这样一个方案, ...
- 深入理解Java并发之synchronized实现原理
深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...
- Java并发编程原理与实战九:synchronized的原理与使用
一.理论层面 内置锁与互斥锁 修饰普通方法.修饰静态方法.修饰代码块 package com.roocon.thread.t3; public class Sequence { private sta ...
- 深入理解并发编程之----synchronized实现原理
版权声明:本文为博主原创文章,请尊重原创,未经博主允许禁止转载,保留追究权 https://blog.csdn.net/javazejian/article/details/72828483 [版权申 ...
- Java精通并发-synchronized关键字原理详解
关于synchronized关键字原理其实在当时JVM的学习[https://www.cnblogs.com/webor2006/p/9595300.html]中已经剖析过了,这里从研究并发专题的角度 ...
- Java多线程和并发(八),synchronized底层原理
目录 1.对象头(Mark Word) 2.对象自带的锁(Monitor) 3.自旋锁和自适应自旋锁 4.偏向锁 5.轻量级锁 6.偏向锁,轻量级锁,重量级锁联系 八.synchronized底层原理 ...
- synchronized实现原理及其优化-(自旋锁,偏向锁,轻量锁,重量锁)
1.synchronized概述: synchronized修饰的方法或代码块相当于并发中的临界区,即在同一时刻jvm只允许一个线程进入执行.synchronized是通过锁机制实现同一时刻只允许一个 ...
- synchronized底层原理
synchronized底层语义原理 Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现. 在 Java 语言中,同步用的最多的地方可能是被 syn ...
- synchronized运行原理以及优化
线程安全问题 线程不安全: 当多线程并发访问临界资源时(可共享的对象),如果破坏原子操作,可能会造成数据不一致. 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可以保证其正确性. 原子操作 ...
- synchronized的原理
synchronized的使用 synchronized是一个java中的关键字,是基于JVM层面的,用于保证java的多线程安全,它具有四大特性,可用于完全替代volatile: 原子性:所谓原子性 ...
随机推荐
- postgreysql
基础 syntax * \help 生成所有的pg命令 * abort 终止事务/work * alter aggregate 修改聚合函数的定义 ALTER AGGREGATE name ( typ ...
- python使用@property @x.setter @x.deleter
@property可以将python定义的函数“当做”属性访问,从而提供更加友好访问方式,但是有时候setter/deleter也是需要的. 1>只有@property表示只读. 2>同时 ...
- Asp.net自定义控件开发任我行(附1)-属性一览众山小
元数据属性应用于服务器控件及其成员,从而提供由设计工具.ASP.NET 页分析器.ASP.NET 运行库以及公共语言运行库使用的信息.当页开发人员在可视化设计器中使用控件时,设计时属性能改进开发人员的 ...
- HTTP-响应状态
响应状态 有多种响应状态,如:200代表成功,301跳转,404找不到页面,502服务器错误 1xx消息 —— 请求已被服务器接收,继续处理 2xx成功 —— 请求已成功被服务器接收.理解.并接受 3 ...
- vlc无法播放.flv视频文件
解决方法:https://videoconverter.wondershare.com/vlc/flv-not-displaying-video-vlc-media-player.html. 在pre ...
- RESTful-rest_framework认证组件、权限组件、频率组件-第五篇
认证组件.权限组件.频率组件总结: 认证组件格式: 1 写一个认证类 from rest_framework.authentication import BaseAuthentication cla ...
- 微信小程序--问题汇总及详解之picker 增、删
<block wx:for="{{salesList}}" wx:for-index="index" wx:key="id" wx:f ...
- P4513 小白逛公园
题目背景 小新经常陪小白去公园玩,也就是所谓的遛狗啦… 题目描述 在小新家附近有一条“公园路”,路的一边从南到北依次排着 nnn 个公园,小白早就看花了眼,自己也不清楚该去哪些公园玩了. 一开始,小白 ...
- [LOJ#2329]「清华集训 2017」我的生命已如风中残烛
[LOJ#2329]「清华集训 2017」我的生命已如风中残烛 试题描述 九条可怜是一个贪玩的女孩子. 这天她在一堵墙钉了 \(n\) 个钉子,第 \(i\) 个钉子的坐标是 \((x_i,y_i)\ ...
- MyBatis输出sql需要log4j.properties配置
# Global logging configuration log4j.rootLogger=info,stdout,console,logfile # MyBatis logging config ...