Java中的锁(锁分类、对象头、锁膨胀过程、自旋锁、偏向锁、轻量级锁及锁粗化)
对象头
对象头内存布局
使用jol查看对象头内存布局(查看锁标志时,注意大端存储和小端存储)
引入依赖
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.14</version> <scope>provided</scope> </dependency>
测试代码
public class StudyApplication { public static void main(String[] args) throws Exception { JolTest obj1 = new JolTest(); // 无锁打印 System.out.println(ClassLayout.parseInstance(obj1).toPrintable()); // HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁 //线程sleep 5s,确保虚拟机中偏向锁开启 Thread.sleep(5000); new Thread(new Runnable() { @Override public void run() { JolTest obj2 = new JolTest(); synchronized (obj2){ //当前锁对象第一次被线程获取 System.out.println(ClassLayout.parseInstance(obj2).toPrintable()); } } }).start(); //轻量级锁 new Thread(new Runnable() { @Override public void run() { synchronized (obj1){ //当前锁对象第一次被线程获取 System.out.println(ClassLayout.parseInstance(obj1).toPrintable()); } } }).start(); //重量级锁 new Thread(new Runnable() { @Override public void run() { synchronized (obj1){ //当前锁对象第一次被线程获取 System.out.println(ClassLayout.parseInstance(obj1).toPrintable()); } } }).start(); } } class JolTest{ int i = 10; }
测试结果
// 无锁 001 com.java.study.JolTest object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) c3 df 01 f8 (11000011 11011111 00000001 11111000) (-134094909) 12 4 int JolTest.i 10 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total // 偏向锁101 com.java.study.JolTest object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 05 c8 36 37 (00000101 11001000 00110110 00110111) (926337029) 4 4 (object header) a1 7f 00 00 (10100001 01111111 00000000 00000000) (32673) 8 4 (object header) c3 df 01 f8 (11000011 11011111 00000001 11111000) (-134094909) 12 4 int JolTest.i 10 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total // 轻量级锁00 com.java.study.JolTest object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) 60 d9 d6 07 (01100000 11011001 11010110 00000111) (131520864) 4 4 (object header) a1 7f 00 00 (10100001 01111111 00000000 00000000) (32673) 8 4 (object header) c3 df 01 f8 (11000011 11011111 00000001 11111000) (-134094909) 12 4 int JolTest.i 10 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total //重量级锁10 com.java.study.JolTest object internals: OFFSET SIZE TYPE DESCRIPTION VALUE 0 4 (object header) aa 24 1d 39 (10101010 00100100 00011101 00111001) (958211242) 4 4 (object header) a1 7f 00 00 (10100001 01111111 00000000 00000000) (32673) 8 4 (object header) c3 df 01 f8 (11000011 11011111 00000001 11111000) (-134094909) 12 4 int JolTest.i 10 Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total锁膨胀的过程
锁优化
锁消除
指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。
锁消除判定主要依据来源于逃逸分析的数据支持,如果判断一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无需进行
偏向锁锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他线程获取,则持有偏向锁的线程再也不需要进行同步
HotSpot 虚拟机在启动后有个 4s 的延迟才会对每个新建的对象开启偏向锁
启用参数:
-XX:+UseBiased Locking
JDK6后HotSpot默认值
偏向锁和HashCode的联系:
- 当对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态
- 当一个对象正处于偏向锁状态,又收到计算一致性哈希码请求时,它的偏向锁会被立即撤销,并且锁会膨胀成重量级锁
自旋锁
让线程执行一个忙循环(自旋),从而让线程等待
启用参数:
- -XX:+UseSpinning JDK6后HotSpot默认值
- -XX:+PreBlockSpin 默认十次
自适应自旋
JDK6中优化,自旋时间不再是固定,而是由前一次在同一个锁上自旋时间及锁的拥有者状态来决定的
- 如果在同一个锁对象,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能成功,进而允许自旋等待持续相对更长的时间,比如持续100次忙循环
- 另一方面,如果对某个锁,自旋很少获得过锁,那在以后获取这个锁时,有可能直接忽略过自旋过程,以避免浪费处理器资源
优缺点
- 优点: 如果能很快获得锁,减少上下文切换
- 缺点: 如果锁被占用时间很长,自旋过程只能白白消耗处理器资源
在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志为01状态),虚拟机首先将在当前线程栈帧中建立一个名为锁记录(Lock Record)的空间,用户存储锁对象目前的Mark Word的拷贝,(官方为这个拷贝加了一个Displaced前缀,即Displaced Mark Word);
然后虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且Mark Word的锁标志更新为00,表示处于轻量级锁状态;
如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的 Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。
如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时 Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
上面描述的是轻量级锁的加锁过程,它的解锁过程也同样是通过CAS操作来进行的,如果对象的 Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的 Mark Word和线程中复制的 Displaced Mark Word替换回来。假如能够成功替换,那整个同步过程就顺利完成了;如果替换失败,则说明有其他线程尝试过获取该锁,就要在释放锁的同时,唤醒被挂起的线程。
轻量级锁能提升程同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。
偏向锁、轻量级锁状态转化:
如果一系列的连续操作都对同一个对象反复加锁解锁(甚至加锁操作出现在循环体中),即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能损耗;所以如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
参考:
- 《深入理解Java虚拟机》
- 《Java并发编程的艺术》
- https://zhuanlan.zhihu.com/p/137849590
- 锁--自旋锁、阻塞锁、可重入锁、悲观锁、乐观锁、读写锁、偏向所、轻量级锁、重量级锁、锁膨胀、对象锁和类锁
- 自旋锁、阻塞锁、可重入锁、悲观锁、乐观锁、读写锁、偏向所、轻量级锁、重量级锁、锁膨胀、对象锁和类锁
- java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁
- java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁
- java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁
- Java线程并发中常见的锁--自旋锁 偏向锁 轻量级锁 轻量级锁
- 牛客网Java刷题知识点之垃圾回收算法过程、哪些内存需要回收、被标记需要清除对象的自我救赎、对象将根据存活的时间被分为:年轻代、年老代(Old Generation)、永久代、垃圾回收器的分类
- java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁
- java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁
- 自旋锁、阻塞锁、可重入锁、悲观锁、乐观锁、读写锁、偏向所、轻量级锁、重量级锁、锁膨胀、对象锁和类锁
- 虚拟机内部对锁的优化——偏向锁、轻量级锁、锁膨胀、自旋锁、锁消除
- Java -- 偏向锁、轻量级锁、自旋锁、重量级锁
- 自旋锁、阻塞锁、可重入锁、悲观锁、乐观锁、读写锁、偏向所、轻量级锁、重量级锁、锁膨胀、对象锁和类锁
- 再学 JAVA基础(5)对象转型,多态。【子类对象的实例化过程】
- [转载]解析Java类和对象的初始化过程_J2EE_Java开发_软件开发-编程-IT资源网
- 从C到Java,从过程到对象之二
- java对象的创建过程
- [转贴]全面解析Java中的类和对象的初始化过程
- java实例化对象的过程
- Java:对象创建和初始化过程