在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头,实例数据和对齐填充,这里我们就先介绍一下对象头。
在HotSpot虚拟机的对象头部分包括三类信息:
32位虚拟机:
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
其中对象头Mark Word(32为虚拟机)的结构为:
|--------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|--------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:0 | lock:01 | Normal |
|--------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:01 | Biased |
|--------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:00 | Lightweight Locked |
|--------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:10 | Heavyweight Locked |
|--------------------------------------------------------|--------------------|
| | lock:11 | Marked for GC |
|--------------------------------------------------------|--------------------|
其中各个部分的含义如下:
**lock:**为2位的锁状态标记位由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
biased_lock | lock | 状态 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold
选项最大值为15的原因。
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()
计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。
对于64位的虚拟机其Mark Word格式如下:
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|
Monitor 被翻译为监视器或管程,Monitor 的重要特点是,同一个时刻,只有一个 进程/线程 能进入 monitor 中定义的临界区,这使得 monitor 能够达到互斥的效果。每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 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 中 Owner 为 null当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED(阻塞)
Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足(线程调用 wait() 方法)进入 WAITING 状态的线程需要被notify唤醒才能再次尝试获取锁。
注意:
synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则
synchronized同步代码块原理:
public class SynchronizedCode {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
以上代码编译过后通过Idea的jclasslib查看main()方法的字节码如下:
0 getstatic #2
3 dup
4 astore_1
5 monitorenter # 进入同步方法,当前线程尝试获取对象锁
6 getstatic #3
9 iconst_1
10 iadd
11 putstatic #3
14 aload_1
15 monitorexit # 退出同步方法,当前线程释放对象锁
16 goto 24 (+8)
19 astore_2
20 aload_1
21 monitorexit # 退出同步方法,当前线程释放对象锁
22 aload_2
23 athrow
24 return
有两个退出同步方法的语句是因为为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行。多的那一个就是异常结束时被执行的释放monitor 的指令。
方法级别的 synchronized 不会在字节码指令中有所体现,它是隐式的,无需同果字节吗指令看来控制
public class SynchronizedCode {
public static void main(String[] args) {
}
public static synchronized void test(){
System.out.println("静态代码块!");
}
}
字节码如下:
synchronized的锁膨胀是jdk1.6对synchronized的优化,锁的状态总共有四种,**无锁,偏向锁,轻量级锁,重量级锁。**锁的升级是单向的,只能从低级的锁升级到高级的锁。锁的膨胀过程为无锁->偏向锁->轻量级锁->重量级锁。
无锁:当前没有线程获取到同步监视器就是无锁状态
偏向锁:轻量级锁在没有竞争时(就自己一个线程在获取锁),每次重入仍然需要执行 CAS 操作,所以Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,此时Mark Word 的结构也变为偏向锁结构,之后当这个线程再次请求锁时,发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
轻量级锁:倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级的锁。如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是同步周期没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是 synchronized。但如果存在同一时间多个线程访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录。
如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁,这时图示如下
如果 cas 失败,有两种情况
当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。
当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头,若成功,则解锁成功,若失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁的解锁流程。
重量级锁:如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
自旋锁优化:重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。因为线程状态的切换比较耗时。
自旋重试成功:
自旋重试失败:
6.锁消除:消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
//以下方法,同步代码块的锁对象为局部变量,不可能存在竞争
public void b() throws Exception {
Object o = new Object();
synchronized (o) {
x++;
}
}
参考链接:https://blog.csdn.net/javazejian/article/details/72828483(绝对大佬,十分完善!)
参考链接:https://www.jianshu.com/p/3d38cba67f8b