一.Synchronized的实现原理
在Java中,每个对象有一个监视器monitor,当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时会尝试获取monitor的所有权,过程如下:
1.如果monitor的进入数 entry count为0,则该线程成功进入monitor,然后将 entry count 设为1,该线程即为monitor的拥有者。
2.如果该线程已经占有了该monitor,则会再次进入,entry count 加1。
3.如果其他线程已经占有了该monitor,则该线程进入阻塞状态,知道monitor的entry count 为0,再尝试获取monitor。
线程执行monitorexit指令来退出monitor,执行该指令的线程必须是monitor的拥有者。该指令执行时,entry count数减1,如果减1后entry count 为0,那线程退出monitor,此时其他被此monitor阻塞的线程可以去获取该monitor的所有权。
对于同步代码块,在编译后,代码块的前后会增加monitorenter和monitorexit指令,使代码块具有同步鲜果。
对于同步方法,其常量池中多了ACC_SYNCHRONIZED标识符,执行线程需要先获取该monitor,成功后才能执行方法体。
二.Synchronized的优化
Synchronized用的锁是存在Java对象头里的Mark Word中。其中有2bit作为标志位来记录储存的数据类型:
00:轻量级锁
10:重量级锁
11:GC标记
01:偏向锁
在Java SE 1.6中,为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。目前,Java锁一共有四种状态,从低到高依次为:无锁、偏向锁、轻量级锁和重量级锁,这几种状态随着竞争情况逐渐升级。锁只能升级不能降级。
1.偏向锁
经研究,大部分情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此引入偏向锁。当一个线程访问同步块并获取锁时,会在对象头的Mark Word中和栈帧的锁记录里存储偏向锁的线程ID,以后该线程再进入和退出时,不需要再进行CAS操作来加锁和解锁,只需要简单测试一下Mark Word中的ID是否正确即可。如果成功,则已经获得锁,如果失败,则检查标志位是否为01偏向锁:是的话,则进行CAS将对象头的偏向锁指向当前线程,否则使用CAS竞争锁,成功获取锁,说明没有多线程冲突,若失败,则锁膨胀为轻量级锁。偏向锁适合于单线程访问同步资源。
偏向锁使用了一种竞争出现才释放锁的机制,当其他线程尝试竞争偏向锁时,才会释放锁。撤销偏向锁需要等待全局安全点(没有正在执行的字节码),首先会暂停拥有偏向锁的线程,检查该线程是否活着,若不处于活动状态,则将对象头设置为无锁状态。若活着,则会将拥有锁的栈帧中的锁记录和对象头的Mark World重新偏向其他线程活着锁膨胀。
2.轻量级锁
轻量级锁的加锁:JVM会在当前线程的栈帧中创建用于储存锁记录的空间,并将对象头的Mark World 复制到锁记录中,然后通过CAS将对象头中的Mark World 替换为指向锁记录的指针。如果成功,获得锁,如果失败,表示其他线程竞争锁,线程便会尝试自旋来获取锁。
轻量级锁的解锁:使用CAS操作将Mark World替换会对象头,成功,则没有竞争,失败,膨胀为重量级锁。
轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
3.重量级锁
线程竞争不适用自旋,不会消耗cpu,但是线程会被阻塞,响应缓慢,适合追求吞吐量,同步块执行时间较长的情况。
下面附上锁的转换图: