0%

synchronized相关-对象头&锁机制

前言

之前面试中有被问到synchronized及Java锁机制相关的问题,所以总结一下相关知识点。

对象头

对象头的结构

synchronized锁住的目标是Java中的对象,那么他到底依赖于什么控制的呢?
答案就是—对象头,他是对象在内存中的存储结构之一。
所以先来了解下对象头是啥样的:

一般对象的对象头由两部分组成
1.klass pointer(指针,指向其类元数据的信息),
2.Mark word(存储对象的运行时数据,包括哈希码,锁状态等)
如果是数组对象则还存有array length信息

Mark word

对象头的三个结构中,与synchronized关联紧密的就是第二部分 Mark word。
因为他维护着对象的锁标记信息。

1.identity_hashcode:对象的Hash值,这里注意如果对象的identity_hashcode不为空,那么他是没法进入偏向锁状态的,因为内存空间被占用了
2.age:GC分代年龄
3.biased_lock:偏向锁标志
4.lock:锁状态标志
5.thread:偏向锁状态偏向的线程标识
6.epoch:偏向时间戳
7.pointer_to_lock_record:指向线程栈中Lock Record的指针
8.pointer_to_heavyweight_monitor:指向堆中的monitor对象的指针

其中biased_block和lock即锁标识的信息:

锁升级

对象头中记录了对象不同的锁状态,当触发了对应条件时就会引起其状态的变化,这个过程被称作锁升级。

偏向锁

当有线程获取到锁资源时就会升级为偏向锁,并在对象头中记录偏向的线程标识,也就是获取到锁资源的线程。

引入偏向锁是因为在一个线程多次获取同一个锁的场景下,如果每次都按照竞争锁的方式去操作未免会造成平白的消耗。
而偏向锁记录了当前拥有锁的线程的标识信息,当同一线程多次去获取该锁时可以直接依据该标识判断,从而减小获取锁的消耗。

轻量级锁

偏向锁是无竞争场景下的,假设这样一种情况:
thread1先获取到了某个对象的锁,thread2慢了一步,获取失败。
那么此时如何处理thread2?
这里的设计是按照乐观的设想,thread1会很快释放锁资源,所以将偏向锁升级为轻量级锁,让thread2自旋不断尝试获取锁资源。

重量级锁

轻量级锁是在乐观设想的前提下让获取失败的线程自旋等待。
但考虑到最坏的情况,thread1因为某些原因占用时间超出了可接受的范围,如果让thread2一直自旋,甚至后面又来了N个线程也在自旋,这样就浪费了大量的CPU资源,显然是不合理的。
所以当线程自旋一定次数还未获得锁资源时,轻量级锁就会升级为重量级锁,让这些尝试获取锁资源的线程通通阻塞,等待锁被释放后再唤醒他们。

重量级锁是jdk1.6之前使用的锁机制,因为其底层依赖于互斥锁,涉及用户态和内核态的转换,进程的上下文切换等,性能影响较大,所以才引入偏向锁和轻量级锁来改善

关于偏向锁需要注意几点:
1). 因为无锁的内存结构中identity_hashcode与偏向锁中的thread的内存空间是冲突的,也就是两者只能存一个,当identity_hashcode不为空的时候,是无法偏向的,这时候会直接升级为轻量级锁
2). 有时候会发现状态是偏向锁,biased_lock也显示是1标识偏向锁,但其实thread是空的,这时候表示处于可以偏向的状态

小结

  为了写本篇文章着实找了许多博客参考,不过大部分都是一些概念,底层实现逻辑的描述较少。不过还
是对synchronized及锁机制有了更清晰的理解,这里照常做一下总结:
1.本文只介绍了对象头及synchronized相关的锁机制,如果想了解更多建议阅读参考中强烈推荐的那篇文章
2.关于synchronized的使用及理解,我认为主要把握住一点,他锁住的是对象(类对象或者实例对象)
3.偏向锁,轻量级锁,重量级锁的加锁释放锁的过程本来想写一下的但是反复写了几次都觉得说的不够清楚,
  所以就暂时不写出来误导人了,主要还有些细节没有验证清楚
4.验证锁的切换可以借用jol输出对象头信息观察锁标志的变化

参考

盘一盘 synchronized (一)—— 从打印Java对象头说起
深入理解synchronized底层源码
强烈推荐-》死磕Synchronized底层实现–概论