偏向锁:
不存在竞争的场景, 偏向某个线程thread, 后续该线程再进入同步块的逻辑没有加锁解锁的开销,
例如StringBuffer.append().append().
偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个 4s 的延迟,之后才会对每个新建 的对象开启偏向锁模式,如果是类的Class对象一般就是之前创建的,会进入轻量级锁。
为什么要延迟?
JVM启动时装载配置,系统类初始化等过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。 如果启动时使用偏向锁,撤销偏向锁到升级为轻量级锁有开销,为了减少初始化时间,JVM默认延时加载偏向锁。
偏向锁是没有地方保存 hashcode, 调用hashcode方法就不会有偏向锁
未锁定线程状态:在调用Object#hashcode时,直接恢复为无锁状态
批量重偏向:一个线程(线程1)创建了大量对象并执行了初始的同步操 作,后来另一个线程(线程2)也来将这些对象作为锁对象进行操作,到达阈值20(Class级别统计)之前是偏向锁撤销后升级锁,之后就还是偏向锁(指向线程2)
偏向锁偏向一次之后不能再次重偏向
批量锁撤销:当一定时间内(默认25s)撤销偏向锁阈值超过 40 次(Class级别统计)后,这种明显多线程竞争剧烈的场景下使用偏向锁是不合适 的,于是这个对象锁的类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。
偏向锁撤销:
如果偏向锁由当前线程持有:不需要等到safe point再撤销,可以直接撤销后升级锁。
若不是当前线程,会被push到VM Thread中等到safepoint的时候再执行
轻量级锁:
线程间存在轻微的竞争(交替执行)的场景,通过CAS将mark word更新为指向lock record的指针,成功就是获取锁,失败膨胀
轻量级锁状态中指向lock record的指针:
虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,为什么要拷贝mark word? 用于轻量级锁解锁到无锁状态时 锁对象头数据恢复
重入:第一次入栈的lock record有Mark Word的拷贝数据,之后重入的lock record只有指向锁对象的指针,重入锁解锁的过程就是不断出栈的过程,最后一个出栈的lock reord刚好有Mark Word的拷贝数据
重量级锁:
多线程竞争激烈的场景 ,膨胀期间创建一个monitor对象,markword指向monitor
wait方法依赖monitor对象,每个实例对象只有一个monitor对象
当前线程先尝试使用 自旋+cas monitor对象的_owner变量(标识拥有该monitor的线程) , 成功就获取锁, 如果自旋失败则线程park,park操作就涉及到了用户态到内核态的切换,开销大。
自适应自旋:
自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。 在 Java 6 之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次 自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。
sychronized锁变化流程图
误区:
1. 无锁——>偏向锁——>轻量级锁——>重量级锁
不存在无锁——>偏向锁
如上图所示:如果满足延迟偏向的条件,之后创建的对象的markword都是偏向锁的状态,只是没有绑定线程。
锁优化:
锁粗化:
StringBuffer buffer = new StringBuffer();
/**
* 锁粗化
*/
public void append(){
buffer.append("aaa").append(" bbb").append(" ccc");
}
上述代码每次调用 buffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同 一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次 append方法时进行加锁,最后一次append方法结束后进行解锁。
锁消除:
锁消除是Java虚拟机在JIT编译期间,通过使用逃逸分析,去除不可能存在共享资源竞争的锁
逃逸分析:
方法逃逸(对象逃出当前方法)
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。
线程逃逸((对象逃出当前线程)
这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。