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

目录

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. [Coci2015]Divljak

    题目描述  Alice有n个字符串S_1,S_2...S_n,Bob有一个字符串集合T,一开始集合是空的. 接下来会发生q个操作,操作有两种形式: “1 P”,Bob往自己的集合里添加了一个字符串P. ...

  2. Elasticsearch6.5.2 X-pack破解及安装教程

    先正常安装 elasticSearch, kibana. 1. 如果是6.5.2版本,可以直接下载jar文件:https://download.csdn.net/download/bigben0123 ...

  3. ajax实现长连接

    项目需求:需要实时的读取日志文件里的数据,并且使用Echart实时更新折线图. 使用ajax实现客户端与服务器端的数据传输. 目的:我想通过ajax与服务器建立一个长连接,服务器会不断的传输数据给前台 ...

  4. 《从Paxos到ZooKeeper分布式一致性原理与实践》学习笔记

    第一章 分布式架构 1.1 从集中式到分布式 集中式的特点: 部署结构简单(因为基于底层性能卓越的大型主机,不需考虑对服务多个节点的部署,也就不用考虑多个节点之间分布式协调问题) 分布式系统是一个硬件 ...

  5. 分布式监控系统开发【day37】:填充表配置项目(三)

    一.注册站点初始化数据库 1.目录结构 2.初始化数据库 python3 manage.py makemigrations python3 manage.py migrate #django2.0之前 ...

  6. makefile $@, $^, $<, $? 表示的意义

    ref:https://www.cnblogs.com/gamesun/p/3323155.html $@  表示目标文件$^  表示所有的依赖文件$<  表示第一个依赖文件$?  表示比目标还 ...

  7. TPS、并发用户数、吞吐量关系

    TPS.并发用户数.吞吐量关系 摘要 主要描述了在性能测试中,关于TPS.并发用户数.吞吐量之间的关系和一些计算方法. loadrunner TPS 目录[-] 一.系统吞度量要素: 二.系统吞吐量评 ...

  8. 第四节:Task的启动的四种方式以及Task、TaskFactory的线程等待和线程延续的解决方案

    一. 背景 揭秘: 在前面的章节介绍过,Task出现之前,微软的多线程处理方式有:Thread→ThreadPool→委托的异步调用,虽然也可以基本业务需要的多线程场景,但它们在多个线程的等待处理方面 ...

  9. 四十四、Linux 线程——线程同步之死锁以及线程和信号

    44.1 死锁 死锁: 两个线程试图同时占有两个资源,并按不同的次序锁定相应的共享资源 解决方式: 按相同的次序锁定相应的共享资源 使用函数 pthread_mutex_trylock(),它是函数 ...

  10. python 写代码笔记 2017.6.15

    其实并不是越复杂的代码越好,简单高效才是好. 关键是思路和逻辑,还有多看别人写的代码. 学习到了:)