Synchronized锁分析
对象在堆中存储结构
1.Mark Work(2个字来存储)
(32位系统)
synchronized就是表中的重量级锁,包含一个指向一个monitor对象的指针。
2.关于monitor对象JVM中使用ObjectMonitor来实现(C++)
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
当多个线程同时访问同一代码块时会先进入_EntryList集合中(进入集合前会先封装成ObjectWaiter对象),获得锁的线程_Owner将指向该线程(ObjectWaiter)对象,并且将_count变量加1,没有获得锁的对象将会在_EntryList中等待。如果_Owner指向的线程对象(ObjectWaiter)调用了wait方法将会释放掉锁进入_WaitSet集合中去等待被唤醒(等待其他线程调调用该对象锁的notify方法,调用notify方法后会在_WaitSet集合中随机选择一个ObjectWaiter元素添加到_EntryList集合中,该被添加的ObjectWaiter元素和之前_EntryList集合中ObjectWaiter元素同样去竞争锁资源。调用notifyAll则是将_WaitSet集合中所有元素添加到_EntryList集合中),同时_count减1。当然_Owner也可直接释放锁并相关值被清除,以便其他ObjectWaiter获得monitor。
小结:synchronized是通过对象头中指向monitor对象的方式来获得锁的。所以说每个对象都关联了一个monitor,这也是为什么Object中存在 wait/notify/notifyAll方法的原因(所有对象都有monitor这个特性)。
3.关于显示锁和隐式锁
1.使用同步代码块时通过javap查看字节码时我们可以看到有monitorenter 和 monitorexit等指令,表示加锁和释放锁的过程。
2.将synchronized加在方法上时class字节码上没有显示的 monitorenter 和 monitorexit 等指令,JVM时通过方法中的flag标志来判断的ACC_SYNCHRONIZED 该方法是否加上了锁。
4.JVM对锁的优化(锁升级)
偏向锁:在没有锁竞争的环境下,当一个线程去获得对象锁时会先检查markwork是否开启偏向锁模式,开启就通过CAS将线程ID写入markwork中去,该线程下次再来获得锁时也只需要简单的比较线程ID避免了获得锁和释放锁这些耗时的操作。当然如果不同线程要获得相同的锁时,偏向锁会升级成轻量级锁。
轻量锁:线程要获得对象锁时先判断是否处于锁定状态,没有锁定标记位位01,通过CAS将mark work中记录拷贝到线程的栈帧中去(Lock record),然后将mark work更新为指向栈帧中的指针,标记位变为00表示该线程获得轻量级锁。如果CAS操作失败表示有多个线程在竞争该锁,此时可能升级成重量级锁。轻量级锁适合线程之间交替执行的常见,避免了同步操作带来的开销。如果同一个时间有多个线程获得锁对象轻量级锁需要额外的CAS操作然后升级成重量级锁,还不如直接使用重量级锁。需要注意的是CAS操作失败之后轻量级锁并不会直接升级成重量级锁而是进行一定的自旋如果任然无法获得再升级成重量级锁。
重量锁:synchronized锁,对应一个monitor,monitor涉及到操作系统的mutex lock ,涉及到用态和内核态之间的切换十分耗时。
5.关于锁消除:
JVM对于一些不存在竞争但是存在锁的代码块进行了锁的消除,就是去掉锁,来提升效率。如:
一个方法中有局部变量StringBuilder 当其连续调用append方法是设计到锁的获得 释放 获得 释放……。此时JVM判断不存在锁竞争所以将锁进行消除。
6.synchronized可重入
当一个线程重复去获得一把已经持有的锁时,该锁的计数器会加一。
7.线程的中断于锁
1.线程中断
通过调用线程的interrupt方法来使线程抛出InterruptedException异常来中断线程。
I. 对于正常运行的线程调用该线程的interrupt方法仅仅是将线程的中断标记位ture,我们需要手动去判断线程的中断标记。
II. 对于Tread.sleep()是调用线程的interrupt方法时会抛出异常,我们需要捕获异常。对于
III. 对于正在通过synchronized方式等待锁的线程调用interrupt方法时也不会中断线程即抛出异常
IV. 对于阻塞在wait方法上的线程调用线程的interrupt方法会抛出异常
8.关于wait 和 notify / notifyAll 方法
当我们调用这些方法时我们必须处于synchronized作用域范围内,因为这些方法的调用说明线程已经获得了该对象上的锁了。