理解java关键字Synchronized(学习笔记)
之前学习了线程的一些相关知识,今天系统的总结下来
目录
1. Java对象在堆内存中的存储结构
2. Monitor管程
3. synchronized锁的状态变换以及优化
4. synchronized的同步性和可见性
5. jvm调优参数设置
6. 总结
1.Java对象在堆内存中的存储结构
要想明白synchronized,必须先搞清楚Java对象在堆中的内存区域:
实例数据:存放类的属性信息及其父类的属性信息,在JVM分配策略的影响下,相同的宽度的字段会被分配到一起
(longs和doubles宽度相同,shorts和chars宽度相同等),而且子类的数据可能会插入到父类的字段间隙。
填充数据:填充补齐的作用,HotSpot要求对象所占空间的大小必须为8的整数倍,
若对象的实例数据以及对象头所占空间大小已经是8字节的整数倍,则该区域不会存在,否则补全。
对象头:该区域是我们的重点,它主要分为两个部分:运行时数据区MarkWord和类型指针以及数组长度(不一定存在)。
一. 运行时数据区保存了运行时对象的信息:HashCode,GC分代,GC标志,锁状态标志以及线程持有的锁,偏向线程ID等信息。
二. 类型指针就是指向类的元数据指针,在Hotspot中该指针指向对象的类对象数据区(jdk1.8中方法区已经被替代成元数据区)。
三. 数组长度,若该对象为数组,还要保存数组的长度信息。
我们的重点是MarkWord数据区,该区域的数据结构不是固定的,一般大小为32bit/32位,64bit/64位。
下图为64位下的MarkWord存储结构。
|
偏向锁标识位 |
锁标识位 |
锁状态 |
存储内容 |
|
0 |
01 |
未锁定 |
hash code(31),年龄(4) |
|
1 |
01 |
偏向锁 |
线程ID(54),时间戳(2),年龄(4) |
|
无 |
00 |
轻量级锁 |
栈中锁记录的指针(64) |
|
无 |
10 |
重量级锁 |
monitor的指针(64) |
|
无 |
11 |
GC标记 |
空,不需要记录信息 |
2.Monitor管程
Synchronized在不同的情境下有四种状态:无锁,偏向锁,轻量级锁,重量级锁。而四种方式的实现主要依靠monitor对象。Monitor是对象天生自带的一个对象,它有多种实现方式,其中一个为对象创建同时也创建了monitor(也可能是在线程持有该锁时创建),若一个线程持有了该对象的锁,那么该对象的monitor对象状态为锁定状态。
在openjdk中查看objectmonitor.hpp的源码部分如下
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,//等待线程数
_recursions = 0;//重入次数
_object = NULL;//寄生的对象。
_owner = NULL;//指向获得ObjectMonitor对象的线程
_WaitSet = NULL;//处于wait状态的线程,会被加入到wait set;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//处于等待锁block状态的线程,会被加入到entry list;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
}
对象锁的线程状态被记录在该对象中。
我们只看对象封装的几个重要字段:
_owner记录当前获取该对象锁的线程。
_WaitSet:当前正在等待获取该锁处于阻塞状态的线程集合(Set保证等待中的线程不可重复)
_EntryList: 当前处于等待状态的线程处于该集合中。
_count: 记录了持有该锁的线程数,若一个线程获取了该对象锁,则计数器+1,执行wait方法后该对象减1,
同时 _owner对象被置位NULL,代表此时没有线程持有该锁。
3.synchronized锁的状态变换以及优化
在jdk1.6之后,java的锁就进行了一系列的优化以解决资源抢占以及程序执行效率问题。
锁的膨胀方向:无锁->偏向锁->轻量级锁->重量级锁
锁状态的详情如下:
无锁:共享数据 没有没任何线程所占用。
偏向锁:大部分情况下,锁不存在竞争,总是由同一线程多次获取。当锁在第一次被线程所获取的时候,标志位变为01(偏向模式),同时进行CAS操作获取当前线程的id记录到markword中,若CAS操作成功,那么线程在此进入同步代码块且线程id已经和markword中的一致时,则不需要任何同步操作(locking,unlocking,update等),注意是不会有任何同步操作。若当有另一个线程尝试获取该锁时,则宣布偏向模式结束,锁状态将会恢复为01(为锁定)或00(轻量级锁)。
轻量级锁:在方法进入同步方法时,若此时同步方法没有被锁定,那么(标志位01),虚拟机会在当前线程所在方法的栈帧中开辟一个空间用来保存锁记录(Lock Record),用于存储当前锁所在的对象的MarkWord的拷贝,在抢占资源时,jvm会进行CAS操作进行失败重试让当前锁所在对象的MarkWord更新为指向Lock Record的指针,若更新成功,代表抢占锁成功,MarkWord变为00,表示处于轻量级锁状态。若更新失败,jvm会先检查当前锁所在对象的MarkWord是否已经指向了线程的栈帧,若已经指向,则说明锁已经被当前线程所持有,那么久可以进入同步块;若没有指向当前线程的栈帧,就说明有至少两个线程在抢占同一把锁,则该锁会膨胀为重量级锁。锁标志位变为10,后面抢占的线程若没有获取到该锁就得进入阻塞状态了。
偏向锁和轻量级锁的区别:两中状态非常相似,但在无竞争的情况下却有区别:轻量级锁在无竞争情况下是通过CAS操作进行失败重试来消除锁,而偏向锁在无竞争情况下直接取消了CAS。
自旋锁(jdk1.4引入): 在只有两个线程争抢同一把锁的情境下,在线程A试图进入同步方法或者同步代码块时,若该同步方法或同步代码块已经被线程B抢先进入,那么此时线程A需要挂起,直到其他线程B执行完才能恢复继续进行锁的抢占,但是大部分情况下锁被某个线程持有只持续很短的时间(单次持有锁到释放锁所消耗的时间),所以线程A在很短的时间内挂起再恢复是不值得的,于是就让线程A在B持有锁之后不释放CPU资源,而是继续循环等待锁,直到获取到锁,称为自旋。适用于线程单次持有锁到释放同一把锁所消耗的时间很短的情况,否则其他线程长时间循环等待锁,不释放cpu资源只会更加消耗资源。
自适应自旋锁(jdk1.6引入):为了解决自旋锁所带来的可能出现的问题,此时一把锁的在等待其他线程释放锁时,自旋的次数由前一次上持有该锁的自旋时间以及当前持有该锁的线程的状态来决定。
锁的去除优化
在JIT编译时,jvm会进行扫描,去除不可能存在竞争情况的锁。看一个例子:
public class Sync{
private String name;
public Sync(String name){
this.name = name;
}
public synchronized String changeName(String name){
this.name = name;
return name;
}
}
Public class Test{
Public void test(String name){
Sync sync = new Sync();
Sync.changeName(“John”);
}
public static void mian(String[] args){
Test test = new Test();
for(int i = 0; i < 100; i++){
test.test(“Jack”);
}
}
}
在上边这个例子中,jvm会进行JIT优化,去除synchronized锁,因为sycn对象生存周期始终在java虚拟机栈中,不可能存在锁竞争的情况。
锁的去除优化
public static String test2(String name){
Sync sync = new Sync(“Tom”);
for(int i = 0 ; i< 100 ; i++){
sync.changeName(name);
}
}
在上边这个例子中,由于changeName是同步方法,在这个for循环中,每次进入和退出循环都要进行lock和unlock操作,但是这是没有必要的操作,于是jvm会将synchronized加到循环的外边,只进行一次lock和unlock操作。
4.synchronized的同步性和可见性
同步性:synchronized修饰的方法或者代码块只允许一个线程进入,只有当该线程退出同步区域时,才允许下一个线程进入。
可见性:当线程释放锁是,当前线程会把本地内存中的共享变量立即刷新到主内存中。保证其他线程获取该共享变量的锁时,获取到的是共享变量的最新值。而当线程获取锁时,当线程对共享变量的拷贝会被置为无效,强制当前线程的共享变量是从主内存中拿到的最新值,从而保证可见性。
5.jvm调优参数设置
自旋锁:-XX:PreBlockSpin 来更改自旋的次数,默认为自旋10次。由于jdk1.6只有加入了自适应的自旋锁,自旋锁会自己判断自旋锁的自旋次数,更智能。
偏向锁:-XX:UseBiasedLocking 来设置是否启用偏向锁。-XX:BiasedLockingStartupDelay 来设置java程序启动后延迟开启偏向锁的时间
6.总结
通过synchronized的底层原理可以了解到synchronized是如何保证同步和共享变量的,以及在具体到代码层面时,jvm又是如何进行进行优化的,以及在不同的场景下如何进行参数调优。
阅读参考书籍《深入理解jvm》
理解java关键字Synchronized(学习笔记)的更多相关文章
- 《深入理解Java虚拟机》学习笔记
<深入理解Java虚拟机>学习笔记 一.走近Java JDK(Java Development Kit):包含Java程序设计语言,Java虚拟机,JavaAPI,是用于支持 Java 程 ...
- Java四种引用--《深入理解Java虚拟机》学习笔记及个人理解(四)
Java四种引用--<深入理解Java虚拟机>学习笔记及个人理解(四) 书上P65. StrongReference(强引用) 类似Object obj = new Object() 这类 ...
- Java虚拟机内存溢出异常--《深入理解Java虚拟机》学习笔记及个人理解(三)
Java虚拟机内存溢出异常--<深入理解Java虚拟机>学习笔记及个人理解(三) 书上P39 1. 堆内存溢出 不断地创建对象, 而且保证创建的这些对象不会被回收即可(让GC Root可达 ...
- 【Java】「深入理解Java虚拟机」学习笔记(1) - Java语言发展趋势
0.前言 从这篇随笔开始记录Java虚拟机的内容,以前只是对Java的应用,聚焦的是业务,了解的只是语言层面,现在想深入学习一下. 对JVM的学习肯定不是看一遍书就能掌握的,在今后的学习和实践中如果有 ...
- 《深入理解 Java 虚拟机》学习笔记 -- 内存区域
<深入理解 Java 虚拟机>学习笔记 -- 内存区域 运行时数据区域 主要分为 6 部分: 程序计数器 虚拟机栈 本地方法栈 Java 堆 方法区 如图所示: 1. 程序计数器(线程私有 ...
- 《深入理解java虚拟机》学习笔记之编译优化技术
郑重声明:本片博客是学习<深入理解Java虚拟机>一书所记录的笔记,内容基本为书中知识. Java程序员有一个共识,以编译方式执行本地代码比解释方式更快,之所以有这样的共识,除去虚拟机解释 ...
- 《深入理解java虚拟机》学习笔记之虚拟机即时编译详解
郑重声明:本片博客是学习<深入理解java虚拟机>一书所记录的笔记,内容基本为书中知识. Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块 ...
- 《深入理解 java虚拟机》学习笔记
java内存区域详解 以下内容参考自<深入理解 java虚拟机 JVM高级特性与最佳实践>,其中图片大多取自网络与本书,以供学习和参考.
- 《深入理解Java虚拟机》学习笔记之类加载
之前在学习ASM时做了一篇笔记<Java字节码操纵框架ASM小试>,笔记里对类文件结构做了简介,这里我们来回顾一下. Class类文件结构 在Java发展之初设计者们发布规范文档时就刻意把 ...
随机推荐
- python yield 理解与用法
1.一句话快速理解 yield 等于 return 这么简单理解 2.详细说明: yield和return的关系和区别了,带yield的函数是一个生成器,而不是一个函数了 这个生成器有一个函数就是n ...
- Python判断自定义的参数格式是否正确
import argparse def args_validation(valid_list, valid_value): assert valid_value in valid_list, 'inv ...
- vegas 为盖斯
vegas 为盖斯 S键 分割素材U键 分开视频和音频I键渲染开始O渲染结束 默认布局 为盖斯新建项目的参数 剪好后渲染 插入字幕
- Numpy系列(一)- array
初始Numpy 一.什么是Numpy? 简单来说,Numpy 是 Python 的一个科学计算包,包含了多维数组以及多维数组的操作. Numpy 的核心是 ndarray 对象,这个对象封装了同质数据 ...
- redis发布/订阅
发布订阅简介 Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息,消息之间通过channel传递. 准备工作 两台安装了redis的机器(虚拟 ...
- DTO/DO等POJO对象的使用场景和 orika-mapper 框架的使用
对于项目而言, 我们一般会有DAO->Service->Controller分层设计, 这些层次体现了每层的作用, 而层次之间的数据传递对象设计很少被提及, 下面是一个相对完整的数据转换过 ...
- esnext:Function.prototype.toString 终于有规范了
从 ES1 到 ES5 的这 14 年时间里,Function.prototype.toString 的规范一字未变: An implementation-dependent representati ...
- C#控件数组批量生成控件
在编写C#窗体应用程序的时候,有时候需要生成好多个功能相似的同一种控件(比如数字键盘按键.单选框等),这时候使用窗体编辑器,费时费力,不便于修改.因此可以采用批量生成控件的形式. 以批量生成按钮为例 ...
- String.intern() 方法__jdk1.6与jdk1.7/jdk1.8的不同
1.为什么要使用intern()方法 intern方法设计的初衷是为了重用string对象,节省内存 用代码实例验证下 public class StringInternTest { static f ...
- JS遍历数组的操作(map、forEach、filter等)
1.map的用法 定义:原数组被“映射”成对应新数组 代码示例: var users = [ {name: "张含韵", "email": "zhan ...