JVM在编译synchronzied时,会编译成monitorenter monitorexit指令,是一种JVM规范
synchronzied锁的种类
轻量锁:多个线程交替执行
(一个线程A先拥有一把锁,然后另一个线程B过来争取锁。B持有的这个对象锁不立刻膨胀,先自旋一段时间,如果B自旋超时还没持有这个对象,则锁升级)
重量锁:互斥
偏向锁:对象初始化是可偏向状态
开启了偏向延迟
-XX:BiasedLockingStartupDelay=0
在线程进入同步代码块之前(当对象第一次进入),首先判断这个对象是否可偏向即是JVM是否开启了偏向延迟,开启是可偏向状态,否则是无锁。可偏向状态升级膨胀为偏向锁,无锁升级为轻量锁。
批量撤销重偏向
以class为单位,为每个class维护一个偏向锁撤销计数器。每一次该class的对象发生偏向撤销操作是,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象也会有一个对应的epoch字段,每个处于偏向锁状态对象的mark word中也有该字段,其初始值为创建该对象时,class中的epoch值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的站,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获取锁时,发现当前对象的epoch值和class不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其mark word的Thread Id改为当前线程ID
CAS 之前线程id 当前线程id 期望退出状态(无锁状态) 失败将会膨胀
线程复用线程id
package com.thread;
public class MyJolTest13 {
static A a;
static List<A> list = new ArrayList<>( );
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
a = new A();
System.out.println( "t1" );
Thread t1 = new Thread(){
public void run() {
synchronized (a) {
list.add( a );
System.out.println( ClassLayout.parseInstance( a ).toPrintable() );
}
}
};
t1.start();
t1.join();
// Thread tnull = new Thread( ){
// @Override
// public void run() {
// super.run();
// }
// };
// tnull.start();
System.out.println( "t2" );
// System.out.println(ClassLayout.parseInstance( a ).toPrintable());
Thread t2 = new Thread(){
public void run() {
synchronized (a){
System.out.println("---------"+ClassLayout.parseInstance( a ).toPrintable());
}
}
};
t2.start();
}
}
打印结果。示例代码开启空线程后,线程id不同。(这里仅仅是一种打印现象没有底层源码支持)
打印初始化信息
-XX:+PrintFlagsInitial
BiasedLockingBulkRebiasThreshold 20 偏向锁批量重偏向阈值
BiasedLockingBulkRevokeThreshold 40 偏向锁批量撤销阈值
BiasedLockingStartupDelay 4000 偏向锁延时时间
批量重偏向
public class MyJolTest14 {
static A a;
// -XX:+PrintFlagsInitial
static List<A> list = new ArrayList<>( );
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
a = new A();
System.out.println( "t1" );
System.out.println( ClassLayout.parseInstance( a ).toPrintable() );
Thread t1 = new Thread(){
public void run() {
for (int i = 0; i <30 ; i++) {
A a = new A();
synchronized (a){
list.add( a );
}
if(i==3){
System.out.println( "t1"+ClassLayout.parseInstance( a ).toPrintable() );
}
}
}
};
t1.start();
t1.join();
Thread tnull = new Thread( ){
@Override
public void run() {
super.run();
}
};
tnull.start();
System.out.println( "t2" );
Thread t2 = new Thread(){
A a15;
int k=0;
public void run() {
for (A a:list) {
synchronized (a){
if(k==18||k==19||k==29){
System.out.println(k+"---------"+ClassLayout.parseInstance( a ).toPrintable());
}
if(k==28){
a15=list.get( 15 );
}
}
k++;
}
synchronized (a15) {
System.out.println( "----15-----" + ClassLayout.parseInstance( a15 ).toPrintable() );
//此时打印的是轻量级锁
}
}
};
t2.start();
}
}
代码解析:t1 线程实例化相同类A的多个对象并放入集合且使用synchronized关键字进行同步对象,由于休眠了5s所以集合中的对象都是偏向锁指向线程t1。t2对集合中对象进行synchronized同步执行,由于t1线程已经改变为偏向t1,所以到t2这里锁要升级,会多次撤销偏向锁升级为轻量锁。当到达阈值20时,jvm会对接下来的对象进行批量重偏向,所以接下来的对象都是偏向指向线程t2不再是轻量
批量重偏向阈值20
测试结果:
代码中k=19时,是第20次。根据测试结果证明BiasedLockingBulkRebiasThreshold偏向锁批量重偏向的阈值是20。效果是20以后都是偏向锁直接偏向t2线程
简单说明原理:对于a.class类,有一个计数器和初始epoch值(00)。当进入t2线程,前19个都是升级为轻量锁。当第20个时,此时计数器达到20,JVM认为这个类频繁升级有问题,将会对epoch值+1(01)。对于t1线程,如果对象还存活将存活的对象的epoch值赋值为新的epoch值(01)。对于t1线程已经失效的,在t2线程中重新偏向t2。对象的epoch值是根据class类中赋值的。前19个还是轻量级锁(synchronized (a15)打印可以证明)
批量撤销阈值40
理论:假如t1线程创建100个对象a的集合。t2线程首先进行20次撤销,然后重偏向到t2,20之后直接使用if条件判断没有撤销直接偏向t2。t3线程过来,首先撤销偏向t2升级为轻量级锁,当再次达到20就不会重新偏向t3。剩余的直接批量膨胀轻量级锁
synchronized原理总结:
synchronized锁的种类及对象头lock:
偏向锁(101)、轻量级锁(000)、重量级锁(010)
synchronized锁的膨胀过程:
当线程访问同步代码块。首先查看当前锁状态是否是偏向锁(可偏向状态)
1、如果是偏向锁:
1.1、检查当前mark word中记录是否是当前线程id,如果是当前线程id,则获得偏向锁执行同步代码块。
1.2、如果不是当前线程id,cas操作替换线程id,替换成功获得偏向锁(线程复用),替换失败锁撤销升级轻量锁(同一类对象多次撤销升级达到阈值20,则批量重偏向)
2、升级轻量锁(纯理论可结合源码)
升级轻量锁对于当前线程,分配栈帧锁记录lock_record(包含mark word和object-指向锁记录首地址),对象头mark word复制到线程栈帧的锁记录 mark word存储的是无锁的hashcode(里面有重入次数问题,涉及源码不深入)。
3、重量级锁(纯理论可结合源码)
CAS自旋达到一定次数升级为重量级锁(多个线程同时竞争锁时)
重量级锁:存储在ObjectMonitor对象,里面有很多属性ContentionList、EntryList 、WaitSet、owner。当一个线程尝试获取锁时,如果该锁已经被占用,则该县城封装成ObjectWaiter对象插到ContentionList队列的对首,然后调用park挂起。该线程锁时方式会从ContentionList或EntryList挑一个唤醒。线程获得锁后调用Object的wait方法,则会加入到WaitSet集合中(当前锁或膨胀为重量级锁)
CAS参数:内存指针,预期值,期望值。从内存取值与预期值比较,如果相同则更改为期望值