之前学习了线程的一些相关知识,今天系统的总结下来

目录

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(学习笔记)的更多相关文章

  1. 《深入理解Java虚拟机》学习笔记

    <深入理解Java虚拟机>学习笔记 一.走近Java JDK(Java Development Kit):包含Java程序设计语言,Java虚拟机,JavaAPI,是用于支持 Java 程 ...

  2. Java四种引用--《深入理解Java虚拟机》学习笔记及个人理解(四)

    Java四种引用--<深入理解Java虚拟机>学习笔记及个人理解(四) 书上P65. StrongReference(强引用) 类似Object obj = new Object() 这类 ...

  3. Java虚拟机内存溢出异常--《深入理解Java虚拟机》学习笔记及个人理解(三)

    Java虚拟机内存溢出异常--<深入理解Java虚拟机>学习笔记及个人理解(三) 书上P39 1. 堆内存溢出 不断地创建对象, 而且保证创建的这些对象不会被回收即可(让GC Root可达 ...

  4. 【Java】「深入理解Java虚拟机」学习笔记(1) - Java语言发展趋势

    0.前言 从这篇随笔开始记录Java虚拟机的内容,以前只是对Java的应用,聚焦的是业务,了解的只是语言层面,现在想深入学习一下. 对JVM的学习肯定不是看一遍书就能掌握的,在今后的学习和实践中如果有 ...

  5. 《深入理解 Java 虚拟机》学习笔记 -- 内存区域

    <深入理解 Java 虚拟机>学习笔记 -- 内存区域 运行时数据区域 主要分为 6 部分: 程序计数器 虚拟机栈 本地方法栈 Java 堆 方法区 如图所示: 1. 程序计数器(线程私有 ...

  6. 《深入理解java虚拟机》学习笔记之编译优化技术

    郑重声明:本片博客是学习<深入理解Java虚拟机>一书所记录的笔记,内容基本为书中知识. Java程序员有一个共识,以编译方式执行本地代码比解释方式更快,之所以有这样的共识,除去虚拟机解释 ...

  7. 《深入理解java虚拟机》学习笔记之虚拟机即时编译详解

    郑重声明:本片博客是学习<深入理解java虚拟机>一书所记录的笔记,内容基本为书中知识. Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块 ...

  8. 《深入理解 java虚拟机》学习笔记

    java内存区域详解 以下内容参考自<深入理解 java虚拟机 JVM高级特性与最佳实践>,其中图片大多取自网络与本书,以供学习和参考.

  9. 《深入理解Java虚拟机》学习笔记之类加载

    之前在学习ASM时做了一篇笔记<Java字节码操纵框架ASM小试>,笔记里对类文件结构做了简介,这里我们来回顾一下. Class类文件结构 在Java发展之初设计者们发布规范文档时就刻意把 ...

随机推荐

  1. 「洛谷3292」「BZOJ4568」「SCOI2016」幸运数字【倍增LCA+线性基+合并】

    [bzoj数据下载地址]不要谢我 先讲一下窝是怎么错的... \(MLE\)是因为数组开小了.. 看到异或和最大,那么就会想到用线性基. 如果不会线性基的可以参考一下我的学习笔记:「线性基」学习笔记a ...

  2. redis jedis使用

    jedis就是集成了redis的一些命令操作,封装了redis的java客户端.提供了连接池管理.一般不直接使用jedis,而是在其上再封装一层,作为业务的使用.如果用spring的话,可以看看spr ...

  3. jvm学习笔记二(减少GC开销的建议)

    一:触发主GC(Garbage Collector)的条件 JVM进行次GC的频率很高,但因为这种GC占用时间极短,所以对系统产生的影响不大.更值得关注的是主GC的触发条件,因为它对系统影响很明显.总 ...

  4. Pandas系列(三)-缺失值处理

    内容目录 1. 什么是缺失值 2. 丢弃缺失值 3. 填充缺失值 4. 替换缺失值 5. 使用其他对象填充 数据准备 import pandas as pd import numpy as np in ...

  5. 第三节:总结.Net下后端的几种请求方式(WebClient、WebRequest、HttpClient)

    一. 前言 前端调用有Form表单提交,ajax提交,ajax一般是用Jquery的简化写法,在这里不再过多介绍: 后端调用大约有这些:WebCient.WebRequest.Httpclient.W ...

  6. webpack学习笔记——项目引入zepto及tap事件失效的解决

    先要npm下来zepto:npm install zepto 然后npm下来exports-loader和script-loader 配置如下: JavaScript // webpack.confi ...

  7. Linux性能工具图册-便于查阅

    该图表示了,Linux系统哪种问题用哪种工具

  8. SQLserver 数据库高版本无法还原到低版本的数据解决方法

    sql server 数据库的版本只支持从上往下兼容.即高版本可以兼容低版本 .低版本不能兼容低版本.通常我们在开发时会用比较高的版本.但是部署到客户那边可能他们的数据库版本会比较低. 我们可以通过导 ...

  9. sessionStorage数组、对象的存储和读取

    一个对象的demo如下: var obj = { name:"name", age:18, love:"美女" } sessionStorage.setItem ...

  10. 一些Js操作

    一.after()和before()方法的区别 after()——其方法是将方法里面的参数添加到jquery对象后面去:    如:A.after(B)的意思是将B放到A后面去:    before( ...