1、Synchronized锁的状态
根据锁对象对象头里面的二进制信息,可以将锁分为分为4种情况:
- 未偏向
1.1 无锁不可偏向:调用过对象的hash函数,hash位置有值,如果偏向的话线程id的值存贮空间不够,因此此时没有偏向,也不可偏向,Mark Word的后三位为001;
1.2 无锁可偏向:没有调用过对象的hash函数,hash位置没有值(全0),如果偏向的话线程id的值存贮空间够用,因此可以偏向,但是还没有偏向,也称为匿名偏向,Mark Word的后三位为101; - 偏向锁
已经偏向一个线程,以前的hash值得位置和unused位置存放偏向的线程的id,Mark Word的后三位为101; - 轻量锁
当有新线程来加锁以偏向的锁对象,偏向锁升级为轻量锁,Mark Word的前62位存放lock record对象指针,后两位为00; - 轻量锁
当升级为轻量锁后继续有线程竞争,轻量锁升级为重量锁,Mark Word的前62位存放lock record对象指针,后两位为10;
以上为synchronize锁的几种状态
2、Synchronized锁的升级场景和过程
JVM虚拟机默认是延时4秒打开偏向锁的,我们这里通过参数XX:BiasedLockingStartupDelay=0设置为延时0秒。 以下情况均为延时0秒
- 初始状态
A a = new A();
//a.hashCode()
//只要调用hashCode方法,就不可以偏向
synchronize(a){
}
假设当前我们是t1线程,当我们执行上面的代码时,a就进入了偏向锁状态。首先当a被new出来时,它的对象头里的信息是无锁可偏向状态。当执行到关键字synchronize时,JVM会进行一系列判断,比如偏向锁是否打开、是否已经偏向等等。这时候a是匿名偏向的(锁状态码为101),所以JVM会把t1的线程id设置到a对象的Mark Word的对应位置,此时a就偏向线程t1,当synchronize代码块执行完,a对象里面的Mark Word信息也不会变。如何改变a对象的Mark Word:JVM会现在内存中生成一个匿名偏向锁的Mark Word信息,然后生成一个偏向t1线程的偏向锁的Mark Word信息(t1id+101),然后用CAS比较内存中匿名偏向Mark Word信息跟a对象Mark Word信息是否相同(相同说明此时是匿名偏向,可以偏向t1,否则就不可以偏向t1),相同就将偏向t1线程的Mark Word信息设置到a对象Mark Word中,完成a对t1的偏向。
- 偏向锁的效率——从偏向到偏向
A a = new A();
//a.hashCode()
//只要调用hashCode方法,就不可以偏向
synchronize(a){
//todo xxxx
}
//todo xxxx
synchronize(a){
//todo xxxx
}
当t1线程再次对a对象加锁时,因为通过计算a对象中Mark Word的线程id号等于t1的线程号,所以不需要使用CAS重新设置a对象的Mark Word,并且a对象直接偏向线程t1。因为CAS消耗性能,所以偏向锁效率高。
3. 从偏向到轻量
当t1对a加锁以后,t2线程也来对a加锁,此时a就会从偏向锁升级到轻量锁。这时JVM首先会先将a对象的MW修改为无锁不可偏向(001),然后在线程栈生成一个Lock Record(LR)对象,然后在内存中生成一个无锁不可偏向的Mark Word,并将该Mark Word信息设置到LR对象中的 displaced_header中,并且通过CAS比较内存中生成的无锁不可偏向的Mark Word与a的MW是否相同,相同则将a的MW信息设置为指向LR对象的指针。同时LR中还有一个属性obj_ref指向a对象。升级为轻量锁时a对象的MW中锁信息位为00,当synchronize代码块执行完后,将LR中的displaced_header信息重新还原到a对象的MW中,此时a的锁信息位为001(无锁不可偏向)
4. 从轻量到轻量
当t2结束后,此时t3又来对a加锁。 因为此时a的锁信息位为001,此时首先在内存中生成一个无锁不可偏向的MW,并将该MW设置到LR中的displaced_header中,通过CAS比较内存中生成的无锁不可偏向的Mark Word与a的MW是否相同,相同则将a的MW信息设置为指向LR对象的指针。此时a对象的MW中锁信息位为00,当synchronize代码块执行完后,将LR中的displaced_header信息重新还原到a对象的MW中,此时a的锁信息位为001(无锁不可偏向)。从轻量到轻量和从偏向到轻量过程基本相同。
5. 批量重偏向
当t1线程对A类的n个对象加锁,此时这n个对象全部偏向t1。当t1结束后,t2线程同样对n个线程加锁,由于这些锁是偏向t1的,所以这时这n个对象会逐个撤销偏向锁,变成轻量锁。当n>20时,前20个对象逐个撤销偏向锁,变成轻量锁,当对第21个对象加锁时,JVM会认为关于A类对象的偏向有问题,于是将当前正在执行的线程中所有加锁的A类对象全部偏向各自的当前线程。如果当前线程只有t2,那么剩下的A类对象全部偏向t2。如果t2撤销20次后还有其他线程,并且其他线程也正在执行,并且所有线程加锁的A类的对象都不相同,这样才能保证批量偏向各自线程。
6. 批量撤销
当t2对n个对象加锁结束后,t3线程来继续给这n个对象加锁。由于此时n个对象中,前20个是轻量锁,剩下的是偏向t2的锁,当t3执行到第21个时,会对偏向t2的偏向锁进行撤销变为轻量锁。当撤销20个后(此时是第40个A的对象,也就是A对象的偏向锁被撤销40次——t2撤20次,t3撤20次),JVM会认为A这个类有问题,会将A的所有对象的偏向锁全部撤销且不可再偏向,并且新建A的对象也是不可偏向的。
7. 重量锁
当两个线程同时竞争一个轻量锁就会升级为重量锁。重量锁需要进入内核态,需要操作系统去排队,synchronize利用mutex互斥量+park()方法完成。直接调用wait()方法会直接进入到重量锁,也只有进入重量锁才能调用wait()方法。因为wait队列存在于ObjectMonitor对象中,ObjectMonitor对象就是重量锁,而只有升级为重量锁才能将ObjectMonitor对象关联到锁对象,锁对象关联到ObjectMonitor对象调用wait方法才不会出错。所以只有进入重量锁才能调用wait方法,调用wait方法也直接进入重量锁。