序
在Java 中的对象 – 对象基本信息中我们主要讲述了对象的基本信息,围绕对象的组成,我们针对对象的创建流程、大小以及对象头的信息有了详细的介绍,最后还延伸了对象头和锁的关系,那么接下来我们就针对对象头和锁的关系聊一聊剩下的问题。
旧博客翻新,原博客写于 Mar 19,2020
问题
问题五:对象头和锁的关系
Java
中的加锁方式简要来分可以分为使用synchronized
和使用Lock
两种方式,其中synchronized
是使用的java
对象的内置锁来实现的,而Lock
的锁方式,是基于AQS
来实现的,这里主要讲的是synchronized
对应的对象内置锁,因为这个锁才和Java
对象有着千丝万缕的联系。
Java 1.6
对synchronized
进行了大幅度的优化,其性能也有了大幅度的提升。Java 1.6
引入了偏向锁和轻量级锁的概念,减少了获得锁和释放锁的消耗。在Java 1.6
之后,加上原有的重量级锁,锁一共有4
种状态,分别是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。目前由于**Java
虚拟机规范**中并没有明确虚拟机是否支持锁升级和降级,因此不同的虚拟机,可能虚拟机支持锁策略也不同,部分虚拟机可能仅支持锁的升级,不支持锁降级;但是这里必须要提一下,HotSpot VM
是支持锁的升降级的,但是由于降级效率较低,基本可以认为不支持降级。
接下来,先来看看对象中的几种锁以及这些锁是如何进行升级的。
无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源,如果没有冲突就修改成功退出,否则就继续循环尝试,如果有多个线程修改同一个值,必定会有一个线程修改成功,而其他修改失败的线程会不断重试直到修改成功。无锁基于CAS
实现,在某些条件下,无锁的效率还是较高的,但是无锁无法完全替代有锁。
偏向锁
偏向锁是指同一段代码一直被一个线程访问,那么该线程会自动获取锁,降低获取锁的代价。
在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争关系,所以出现了偏向锁,其目标就是当只有一个线程执行的时候,同步代码块能够提高性能。
当一个线程访问同步代码块并获取锁的时候,会先在对象的Mark Word
中记录锁偏向的线程ID
,在线程进入和退出同步块时不再通过CAS
操作来加锁和解锁,而是检测Mark Word
中记录的线程ID
是否是当前线程ID
;引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行逻辑,因为轻量级锁的获取及释放依赖多次CAS
原子指令,而偏向锁只需要在置换Thread ID
的时候依赖一次CAS
操作即可。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁,偏向锁的撤销需要等待全局安全点(SafePoint
)时进行,在安全点时,会先暂停持有偏向锁的线程,判断锁对象是否处于被锁定的状态,如果被锁定,那么将锁升级为轻量级锁,其他线程自旋获取轻量级锁,否则先将该对象锁恢复为无锁,之后将锁偏向另外的线程。
轻量级锁
当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取,不会阻塞。
当线程进入同步块的时候,此时同步对象状态有两种,一种是无锁,另外一种是持有轻量级锁。那么便有以下两种情况:
- 无锁时(标志位位
01
,是否偏向锁为0
)虚拟机会首先在当前线程的栈帧中建立一个名为锁记录(Lock Record
)的空间,用于存储锁对象当前的Mark Word
的拷贝(即Displaced Mark Word
),然后将对象头中的Mark Word
复制到锁记录中;拷贝成功之后,虚拟机将使用CAS
操作尝试将对象的Mark Word
更新为指向Lock Record
里Displaced Mark Word
地址。此时CAS
操作如果成功,那么就表示当前线程拥有了该对象的锁,并且对象Mark Word
的锁标志被设置为00
,表示此对象处于轻量级锁定状态。如果CAS
操作失败,那么说明在当前线程尝试更新Mark Word
的时候,有一个线程已经更新完了Mark Word
中的信息,当前线程只能进入阻塞状态。 - 线程进入同步块,发现已经存在线程持有了该对象锁时,那么当前线程就会先判断对象的
Mark Word
是否是当前线程的栈帧,如果是,说明存在一个递归调用关系在里面,那么直接进入同步块内部执行,否则说明存在多个线程竞争锁,如果当前没有线程在等待,那么该线程就会自旋进行等待;但是当自旋超过一定的次数,或者超过一个线程在等待,那么轻量级锁就会升级为重量级锁。
重量级锁
升级为重量级锁是,锁标志的状态值变为10
,此时Mark Word
中存储的是指向重量级锁的指针,这时等待锁的线程都会进入阻塞状态。
综述
偏向锁通过对比Mark Word
解决加锁问题,避免执行CAS
操作,而轻量级锁是通过用CAS
操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能,重量级锁是将除了拥有锁的线程意外的线程都阻塞。
问题六:hashCode是什么?hashCode和对象的关系?
从上面几个问题的解答,我们能够发现,在对象处于无锁状态时,Mark Word
中存储的是对象的hashcode
,这个我们很熟悉,在每一个类中都会存在一个native
方法,那就是hashcode()
,这个方法用于计算对象的哈希值,可以和equals()
方法配合来比较两个对象是否相等。这里插一句,不同的对象可能会生成相同的hashcode
值,所以不能只根据hashcode
值判断两个对象是否是同一对象,但是如果两个对象的hashcode
值不等,则必定是两个不同的对象,也就是说在比较对象是否为同一对象的时候,先判断两个对象的hashcode
是否真正相等,如果不相等,那么肯定不是同一对象,如果相等,然后使用equals
方法,如果相等就是同一对象。
一般来说,我们都是认为hashcode
对应的是对象的内存地址,其实这样的说法不太准确。举个例子:我们都知道垃圾整理算法会将存活的对象移动到内存的连续区域,那么移动前后这个对象对应的地址肯定发生了变化,那这个对象对应的hashcode
难道变化了?肯定不会变化!
某些情况下,我们会重写equals()
方法,这样会导致hashCode()
方法也会被重写,进而我们我们通过object.hashCodde()
获取到的hashcode
值肯定不会是对象头中存储的hashcode
值,那么此时如何获取到对象头中的hashcode
值呢?其实Java
中还存在一个对象hashcode
值的方式,那就是通过使用System.identityHashCode(Object)
来获取对象的hashcode
值,此时获取到的永远都是Mark Word
中的值。
那么,hashcode值究竟是如何生成的呢?我想还是需要去看看源码
在synchroizer.cpp中的get_next_hash()方法中,介绍了几种计算hashcode
的算法,如下所示(有删减):
|
|
根据上面的代码,可以知道,有如下几种算法:
hashCode == 0
:系统生成的随机数hashCode == 1
:对对象的内存地址进行二次计算hashCode == 2
:硬编码1
(用于敏感性测试)hashCode == 3
:一个字增的序列hashCode == 4
:对象的内存地址,转为int
hashCode == 5
:Marsaglia’s xor-shift scheme with thread-specific state
在global.hpp中,我们可以看出,JDK 1.8使用的计算hashcode
的算法为5
, 也就是Marsaglia’s xor-shift scheme with thread-specific state ,所以hashcode
的值与对象的内存地址,没有什么关系。
|
|
注:可以使用参数-XX:hashCode=[0-5]
来改变默认的算法。
问题七:如何验证hashCode
对偏向锁的影响
在Java 中的对象 – 对象基本信息文章中我们知道,对象Mark Word
中有存有对象的hashCode
,但是锁在升级的时候,会发现对象Mark Word
中没有保存hashCode
,那么如果我们此时使用对象hashCode
的话该怎么办。
如图所示,在无锁时,Mark Word
中有25bit
用来存储对象hashCode
,但是锁升级之后,Mark Word
中并没有保存对象hashCode
引用RednaxelaFX
的话来解释一下:
- 当一个对象已经计算过 identity hash code,它就无法进入偏向锁状态;
- 当一个对象当前正处于偏向锁状态,并且需要计算其 identity hash code 的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;
- 重量锁的实现中,ObjectMonitor 类里有字段可以记录非加锁状态下的 mark word,其中可以存储 identity hash code 的值。或者简单说就是重量锁可以存下 identity hash code。
- 因为 mark word 里没地方同时放 bias 信息和 identity hash code。HotSpot VM 是假定“实际上只有很少对象会计算 identity hash code ”来做优化的;换句话说如果实际上有很多对象都计算了 identity hash code 的话,HotSpot VM 会被迫使用比较不优化的模式。
- 请一定要注意,这里讨论的 hash code 都只针对 identity hash code。用户自定义的 hashCode() 方法所返回的值跟这里讨论的不是一回事。Identity hash code 是未被覆写的 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(Object) 所返回的值。
为了验证hashCode
对偏向锁的影响,我们可以使用代码来进行测试:
- 首先,引入
JMH
框架
|
|
- 然后,编写测试代码,我们这里使用别人的代码,代码如下:
|
|
为了避免JVM
花费4
秒触发优化,我们在这里启动了JVM
参数-XX:BiasedLockingStartupDelay=0
,最后运行结果如下:
|
|
从结果中我们可以看到使用自定义hashCode
的lock/unlock
速度比使用原始哈希码的速度快了4
倍到5
倍,而当两个线程同时去争抢锁时,lock/unlock
速度几乎是相同的,说明偏向锁被禁止了。
为了说明偏向锁被禁用之后两者之间没有差异,我们再添加-XX:-UseBiasedLocking
参数来进行测试:
|
|
可以看到禁用偏向锁之后两者之间没有了差异,说明hashCode
对偏向锁是存在影响的……
总结
通过两篇文章,解答七个问题之后,关于对象和锁的关系也暂时告一段落了,从开始仅仅是想知道一个对象多大,到后面不断的深挖,看到这么多背后的东西,感觉还是挺奇妙的,也说明了学习是一个不断深挖的过程,同时也是一个不断总结过程,之后可能会再考虑深入去复习一下AQS
相关知识。
参考
- 《深入理解Java虚拟机》 - 周志明
- JVM锁简介:偏向锁、轻量级锁和重量级锁
- How does the default hashCode() work?