说明
以下案例都是在开启了偏向锁,且关闭了偏向延迟之后进行的。
都知道:一个对象第一次加锁为偏向锁,另一个线程来加锁会升级为轻量锁,就像下面的结果。
public static void main(String[] args) throws InterruptedException {A a = new A();Thread t1 = new Thread(() -> {log.error("{} 加锁前: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));synchronized (a){log.error("{} 加锁中: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));}log.error("{} 加锁后: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));});t1.setName("t1");t1.start();t1.join();log.error("===================t2====================");Thread t2 = new Thread(() -> {log.error("{} 加锁前: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));synchronized (a){log.error("{} 加锁中: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));}log.error("{} 加锁后: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));});t2.setName("t2");t2.start();t2.join();}
输出结果
按照步骤分别解释一下
- 第一次创建的对象,默认是101,代表的是无锁可偏向
- 代表的是偏向锁已经偏向了t1,只是低三位的状态码还是101,只不过对象头前面的56位已经带上了线程的id,以及epoch信息,这里为了方便,我就没打印
- t1已经执行完毕同步块,可以视为t1 已经将锁释放了,但是偏向锁释放以后,对象头还是没变的,还是t1 id + 101。原因可以参考偏向锁的文章
- t2来获取锁,此时对象头中存的是 t1id+101。
- JVM发现此时对象头中是一把偏向t1的偏向锁,然后进行偏向撤销,将对象头的锁标识升级成为了轻量锁,并且处于加锁状态000
- t2释放锁,将对象头置为无锁不可偏向的状态,001
偏向撤销达到20次
来看看这种情况,创建类A的30个对象到一个list中,t1,新去加锁,t1执行完成以后,然后t2去加锁。看看输出结果
public static void main(String[] args) throws InterruptedException {List<A> list = new ArrayList<>();log.error("==============t1================");Thread t1 = new Thread(() -> {for (int i = 1; i <= 30; i++) {A a = new A();log.error("{} ,{},加锁前: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));synchronized (a){log.error("{} ,{},加锁中: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));}log.error("{} ,{},加锁后: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));list.add(a);}});t1.setName("t1");t1.start();t1.join();log.error("===================t2====================");Thread t2 = new Thread(() -> {for (int i = 1; i <= list.size(); i++) {A a = list.get(i-1);log.error("{} ,{},加锁前: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));synchronized (a){log.error("{} ,{},加锁中: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));}log.error("{} ,{},加锁后: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));}});t2.setName("t2");t2.start();t2.join();}
控制台中
发现,t2到了20次的时候,也就是经历了20次的偏向撤销,那么再次获取锁的时候,又变成了偏向锁。并且偏向t2。这是为什么呢?
由于偏向撤销是很耗资源的操作,JVM中对偏向撤销进行了优化,当一个类的对象偏向撤销次数达到了20的时候,就会去修改元空间中类模板中epoch的值,了解偏向锁加锁流程的同学应该知道,偏向锁加锁过程中,有一段这样的代码
//如果当前对象的epoch不等于类字节码中的epoch,则尝试重新偏向else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {// 构造一个偏向当前线程的mark wordmarkOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);if (hash != markOopDesc::no_hash) {new_header = new_header->copy_set_hash(hash);}// CAS替换对象头的mark wordif (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {if (PrintBiasedLockingStatistics)(* BiasedLocking::rebiased_lock_entry_count_addr())++;}else {// 重偏向失败,代表存在多线程竞争,则调用monitorenter方法进行锁升级CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);}success = true;}
而所谓的epoch是否过期,其实做的就是这个操作:判断当前对象中的epoch和元空间中类模板中的epoch值是否一致,不一致则证明过期,需要走重偏向流程。
偏向撤销达到40次
来看看更加极端的情况,创建类A的40个对象到一个list中,t1,新去加锁,t1执行完成以后,然后t2去加锁。然后t3继续对每个对象进行加锁。最后我们new一个A类的对象出来,输出一下对象头的锁标识信息。
public static void main(String[] args) throws InterruptedException {List<A> list = new ArrayList<>();log.error("==============t1================");Thread t1 = new Thread(() -> {for (int i = 1; i <= 40; i++) {A a = new A();log.error("{} ,{},加锁前: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));synchronized (a) {log.error("{} ,{},加锁中: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));}log.error("{} ,{},加锁后: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));list.add(a);}});t1.setName("t1");t1.start();t1.join();log.error("===================t2====================");Thread t2 = new Thread(() -> {for (int i = 1; i <= 40; i++) {A a = list.get(i - 1);log.error("{} ,{},加锁前: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));synchronized (a) {log.error("{} ,{},加锁中: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));}log.error("{} ,{},加锁后: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));}});t2.setName("t2");t2.start();t2.join();//注意当t2执行完成的时候,其实也才经历了20次的偏向撤销,因为t2执行的时候,后面20次,走的都是重偏向的流程//此时,list中,前20个存的是轻量锁对象,后20个存的是偏向t2的偏向锁对象。log.error("==================t3=======================");Thread t3 = new Thread(() -> {for (int i = 1; i <= 40; i++) {A a = list.get(i - 1);log.error("{} ,{},加锁前: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));synchronized (a) {log.error("{} ,{},加锁中: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));}log.error("{} ,{},加锁后: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));}});t3.setName("t3");t3.start();t3.join();//当t3执行完成,把list中后面20个偏向t2的对象,经过偏向撤销升级成了轻量锁。//所以到这里,整个A类的对象,总共经历了40次的偏向撤销A newA = new A();log.error("newA : {}",ClassLayout.parseInstance(a).toPrintable(a));}
主要是看最后的newA打印的信息:

居然直接就是001,无锁不可偏向的标识。要知道,在我们这个程序中,前40个A类的对象被创建出来都是无锁可偏向的状态。而当偏向撤销达到了40次,JVM直接就将A类产生的对象改为了无锁不可偏向的状态
当达到了40次的偏向撤销,JVM会去修改元空间中当前类字节码的数据,让其再次实例化的对象直接就是关闭偏向锁的状态。其实JVM自动做升级也不奇怪,因为相对于频繁的偏向撤销来说,直接升级到轻量锁模式可能更加节省资源吧。
