3.1 synchronized可重入特性
可重入:一个线程可以多次执行synchronized重复获取同一把锁。
(synchronized底层锁对象中包含了一个计数器(recursions),记录线程获得了几次锁。 当我们同一个线程获得了锁,计数器则会+1,执行完同步代码块,计数器-1。 直到计数器的数量为0,就释放这个锁对象。)
执行结果:
在输出“同步代码块1”之后,不需要等待锁释放,即可进入第二个同步代码块。这样的一个特性可以更好的封装代码(即:同步代码块中的代码,可以分成多个方法来写)。
3.2 synchronized不可中断特性
不可中断:线程二在等待线程一释放锁时,是不可被中断的。
当一个线程获得锁之后,该线程不释放锁,后一个线程会一直被阻塞或等待。
如果令线程一进入同步代码之后,一直持有锁,并且睡眠了;
此时线程二启动去尝试获取锁,获取失败之后变成堵塞状态,即便强行中断线程二,最后看到线程二的状态仍是堵塞的。
3.3 观察汇编
观察对下列代码的java –p 结果:
monitorenter:当我们进入同步代码块的时候会先执行monitorenter指令,每一个对象都会和一个monitor关联,监视器被占用时会被锁住,其他线程无法来获取该monitor。当其他线程执行monitorenter指令时,它会尝试去获取当前对象对应的monitor的所有权。
两个重要成员变量:
owner: 当一个线程获取到该对象的锁,就把线程当前赋值给owner。
recursions:会记录线程拥有锁的次数,重复获取锁当前变量会+1。
monitorenter执行流程:
1. 若monitor的进入次数为0,线程可以进入,并将monitor进入的次数设为1,当前线程成为montiro的owner;
2. 若线程已拥有monitor的所有权,允许它重入monitor,进入一次次数+1 ;
3. 若其他线程已经占有monitor,当前尝试获取monitor的线程会被阻塞,直到进入次数为变0,才能重新被再次获取。
monitorexit:能执行monitorexit指令的线程,一定是拥有当前对象的monitor所有权的。当执行monitorexit指令计数器减到为0时,当前线程就不再拥有monitor所有权。其他被阻塞的线程即可再一次去尝试获取这个monitor的所有权。
上面编译出来的指令,其实monitoreexit是有两个:需要保证如果同步代码块执行抛出了异常,则也需要释放锁对象。
synchronized如果抛异常了,会不会释放锁对象,答案:会。
同步方法编译后指令:
同步方法在反汇编后,会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenter、mointorexit。
4.1 monitor 监视器锁
每一个对象都会和一个monitor监视器关联,真正的锁都是靠monitor来完成。
源码:C++ http://hg.openjdk.java.net/jdk8/jdk8/hotspot/
java对象和monitor关联方式:
对象在内存中分为三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。对象头就包含了一个monitor的引用地址。
Monitor的成员属性:
/src/share/vm/runtime/objectMonitor.hpp
_recursions:记录线程线程获取锁的次数,获取到锁该属性则会+1,退出同步代码块则-1。
_owner:存储拥有monitor的所有权的线程。
_WaitSet:存储wait状态的线程。
_cxq :当线程之间开始竞争锁,如果锁竞争失败后,则会加入_cxq链表中。
_EntryList:当新线程进来尝试去获取锁对象,又没有获取到对象的时候,则会存储到_EntryList当中。
4.2 monitor 竞争
竞争场景:
当多个线程执行同步代码块的时候,这个时候就会出现锁竞争。
当线程执行同步代码块时,先执行monitorenter指令, 这个时候会调用interpreterRuntime.cpp中的函数
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) // 是否用偏向锁 if (UseBiasedLocking) { ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } else { // 重量级锁 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); }
竞争方式:
对于重量级锁,monitorenter函数中会调用 ObjectSynchronizer::slow_enter ,最终调用到ObjectMonitor::enter
基本操作流程如下:
1 通过CAS尝试把monitor的_owner属性设置为当前线程
2 若之前设置的owner等于当前线程,说明重入,执行_recursions ++ 记录重入次数。
3 若当前线程是第一次进入,设置_recursions = 1,_owner = 当前线程,该线程成功获得锁并返回。
4 如果获取锁失败,等待锁释放
4.3 monitor 等待
锁竞争失败后,会调用EnterI 函数
基本操作流程如下:
1 进入EnterI后,先会再次尝试获取锁对象
2 把当前线程封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ
3 在for循环中,通过CAS把node节点push到_cxq列表中(同一时刻可能有多个线程执行这个操作)
4 node节点push到_cxq 列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待唤醒
5 当前线程被唤醒时,会从挂起到点继续执行,通过TryLock再次尝试锁
4.4 monitor 释放
释放monitor过程:
基本操作流程如下:
1 退出同步代码块时会让_recursions - 1,当_recursions的值等于0的时候,说明线程释放了锁
2 根据不同的策略(由QMode来指定),最终获取到需要被唤醒的线程(用w表示)
3 最后调用ExitEpilog函数中,最终由unpark来执行唤醒操作
5.1 CAS 介绍
Compare And Swap
3个操作数:内存地址V、旧的预期值A、要更新的目标值B。
当内存地址V的值与预期值A相等时,将目标值B保存到内存当中,否则就什么都不做。 原子操作。
CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,失败的线程并不会挂起,只是被告知这次竞争失败,可以再次尝试。
5.2 synchronized锁升级过程
在JDK1.5以前,synchronized是一个重量级锁,在1.6以后,对synchronized做了大量的优化,包含偏向锁、轻量级锁、适应性自旋、锁消除、锁粗化等。
锁升级过程:无锁 à 偏向锁 à 轻量级锁 à 重量级锁。(只升不降)
在了解各种锁的特性之前,需要先搞清楚对象在内存中的布局。
5.3 对象的布局
对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
对象头:
当一个线程尝试访问sync修饰的代码块时,它先要获得锁,这个锁对象存在于对象头中。
以Hotspot为例,对象头里面主要包含了Mark Word(字段标记)、Klass Pointer (指针类型),如果对象是数组类型,还包含了数组的长度。
class Pointer :
用于存储对象的类型指针,JVM通过这个指针确定是哪个对象的实例。
实例数据:
类中定义的成员变量
对齐填充:
仅仅只是占位符。由于Hotspot的自动内存管理系统要求对象起始地址必须是8字节的整倍数,当对象的实例数据部分没有对齐时,就需要通过对齐填充来补齐。