前言
文接上篇,本文将继续介绍 Synchronized,感兴趣的小伙伴继续跟博主一起讨论下。您的 关注、点赞、收藏 都将是小编持续创作的动力!
一、synchronized锁的底层实现
在探讨synchronized锁的底层实现原理之前,我们先来了解下java对象在内存中的结构
1. 对象的内存布局
以64位虚拟机为例:
从上面的这张图里面可以看出,对象在内存中的结构主要包含以下几个部分:
- 对象头:
- Mark Word(标记字段):关于锁的信息。对象的Mark Word部分占4个字节/8个字节,表示对象的锁状态(比如轻量级锁的标记位,偏向锁标记位),另外还可以用来配合GC分代年龄、存放该对象的hashCode等。
- Klass Pointer(Class对象指针):Class对象指针的大小也是4个字节/8个字节,其指向的位置是对象对应的Class对象(其对应的元数据对象)的内存地址。
- 数组长度:如果对象是数组类型,占用4个字节/8个字节,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
- Instance Data(对象实际数据):这里面包括了对象的所有成员变量,其大小由各个成员变量的大小决定,比如:byte和boolean是1个字节,short和char是2个字节,int和float是4个字节,long和double是8个字节,reference是4个字节。
- padding data(对齐):如果上面的数据所占用的空间不能被8整除,padding则占用空间凑齐使之能被8整除。被8整除在读取数据的时候会比较快.
2. 对象的创建过程
2.1. 检查类对象是否被实例化过
jvm要检查类A是否已经被加载到了内存,即类的符号引用是否已经在常量池中,并且检查这个符号引用代表的类是否已被加载、解析和初始化过的。如果还没有,需要先触发类的加载、解析、初始化。然后在堆上创建对象。
2.2. 为新生对象分配内存
2.3. 完成实例数据部分的初始化
内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB的话,这一个工作也可以提前至TLAB分配时进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
2.4. 完成对象头的填充
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头之中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
在上面工作都完成之后,java程序开始调用<init>方法完成初始复制和构造函数,所有的字段都为零值,这样一个真正可用的对象才算完全创建出来。
3. synchronized锁基于对象内存模型和对象创建过程的实现原理
对象头是我们需要关注的重点,它是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word
和 Class Metadata Address
组成,其中Mark Word
存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address
是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
ObjectMonitor() { _header=NULL; _count=0; //锁计数器_waiters=0, _recursions=0; _object=NULL; _owner=NULL; _WaitSet=NULL; //处于wait状态的线程,会被加入到_WaitSet_WaitSetLock=0 ; _Responsible=NULL ; _succ=NULL ; _cxq=NULL ; FreeNext=NULL ; _EntryList=NULL ; //处于等待锁block状态的线程,会被加入到该列表_SpinFreq=0 ; _SpinClock=0 ; OwnerIsThread=0 ; }
每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态.
ObjectMonitor中有两个队列WaitSet和EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装ObjectWaiter对象),owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入EntryList 集合,当线程获取到对象的monitor 后进入 _owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁).
monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因.
二、JVM对synchronized的优化
上一篇文章中提到JVM对synchronized的优化机制中的一种:锁膨胀 ,这篇文章我们将继续介绍其它集中机制:锁消除、锁粗化、自旋锁。
感兴趣的小伙伴可以回过头去阅读一下:「 代码性能优化 」作为一名Java程序员,你真的了解 synchronized 吗?(一)
1、锁消除
先解释一个概念:JIT。
Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行的特别频繁时,会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机会把这些代码编译成本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT编译器,不是Java虚拟机内必须的部分)。
锁消除即代码中不存在锁竞争的地方使用了synchronized,jvm会自动帮你优化掉。具体解释就是:只有一个线程会用到,不会引起多个线程竞争的就没必要加锁了。
实例:
/*** 锁消除*/publicclassDemo1 { staticObjectobject=newObject(); publicstaticvoidfun1(){ Objecto=newObject(); synchronized(o){ System.out.println("Hello World!"); } } publicstaticvoidmain(String[] args) { for (inti=0; i<10; i++) { newThread(()->{ Demo1.fun1(); },String.valueOf(i)).start(); } } }
main函数中,每个线程都会创建一个对象,各线程都有自己的私有资源,并不会引起线程之间的竞争,相当于每个线程都有一把锁,所以synchronized的存在毫无意义,程序编译过程中,JIT即时编译器会无视它。
2、锁粗化
锁粗化即JIT会将首尾相接,前后相邻且都是锁同一个对象的代码块,JIT即时编译器就会把这几个synchronized块合并为一个大块。通过扩大锁的范围,避免反复加锁和释放锁。比如下面fun2经过锁粗化优化之后就和fun1执行效率一样了。
实例:
publicclassHelloWorld { publicstaticvoidmain(String[] args) throwsException { Stringuser=newString("小明"); intcount=0; //调用func1longstart1=System.nanoTime(); fun1(user,count); System.out.println( System.nanoTime() -start1); //调用func2longstart2=System.nanoTime(); fun2(user,count); System.out.println( System.nanoTime() -start2); } privatestaticvoidfun1(Stringuser,intcount){ for (inti=0; i<100; i++) { count++; } } privatestaticvoidfun2(Stringuser,intcount){ for (inti=0; i<100; i++) { synchronized (user) { count++; } } } }
执行结果:
5034
5007
3、自旋锁与自适应自旋锁
轻量级锁失败后,JVM虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
自旋锁:一般情况下,共享数据的锁定状态持续时间较短,切换线程影响程序执行效率,通过让线程执行循环等待锁的释放,不释放内存资源。如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式。但是这种机制也有缺点:如果锁被其他线程长时间占用,一直不释放资源,会带来许多的性能开销。
自适应自旋锁:自旋锁优化方式的进一步优化,它的自旋的次数不再固定,其自旋的次数由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定,这就解决了自旋锁带来的影响性能开销缺点。
总结
以上内容 从synchronized锁的底层实现、JVM对synchronized的优化两方面介绍了 synchronized 的知识,请继续关注博主,接下里会继续就 synchronized 的用法 展开讨论。
下一篇主题
根据获取的锁分类来分析synchronized的用法
参考
致谢
莫笑少年江湖梦,谁不少年梦江湖.
本篇内容参考自互联网及开源社区,感谢前人的经验、分享和付出,让我们可以有机会站在巨人的肩膀上眺望星辰大海!