说明

以下案例都是在开启了偏向锁,且关闭了偏向延迟之后进行的。
都知道:一个对象第一次加锁为偏向锁,另一个线程来加锁会升级为轻量锁,就像下面的结果。

  1. public static void main(String[] args) throws InterruptedException {
  2. A a = new A();
  3. Thread t1 = new Thread(() -> {
  4. log.error("{} 加锁前: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));
  5. synchronized (a){
  6. log.error("{} 加锁中: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));
  7. }
  8. log.error("{} 加锁后: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));
  9. });
  10. t1.setName("t1");
  11. t1.start();
  12. t1.join();
  13. log.error("===================t2====================");
  14. Thread t2 = new Thread(() -> {
  15. log.error("{} 加锁前: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));
  16. synchronized (a){
  17. log.error("{} 加锁中: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));
  18. }
  19. log.error("{} 加锁后: {}",Thread.currentThread().getName(), ClassLayout.parseInstance(a).toPrintableTest(a));
  20. });
  21. t2.setName("t2");
  22. t2.start();
  23. t2.join();
  24. }

输出结果
image.png
按照步骤分别解释一下

  1. 第一次创建的对象,默认是101,代表的是无锁可偏向
  2. 代表的是偏向锁已经偏向了t1,只是低三位的状态码还是101,只不过对象头前面的56位已经带上了线程的id,以及epoch信息,这里为了方便,我就没打印
  3. t1已经执行完毕同步块,可以视为t1 已经将锁释放了,但是偏向锁释放以后,对象头还是没变的,还是t1 id + 101。原因可以参考偏向锁的文章

synchronized 偏向锁

  1. t2来获取锁,此时对象头中存的是 t1id+101。
  2. JVM发现此时对象头中是一把偏向t1的偏向锁,然后进行偏向撤销,将对象头的锁标识升级成为了轻量锁,并且处于加锁状态000
  3. t2释放锁,将对象头置为无锁不可偏向的状态,001

偏向撤销达到20次

来看看这种情况,创建类A的30个对象到一个list中,t1,新去加锁,t1执行完成以后,然后t2去加锁。看看输出结果

  1. public static void main(String[] args) throws InterruptedException {
  2. List<A> list = new ArrayList<>();
  3. log.error("==============t1================");
  4. Thread t1 = new Thread(() -> {
  5. for (int i = 1; i <= 30; i++) {
  6. A a = new A();
  7. log.error("{} ,{},加锁前: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));
  8. synchronized (a){
  9. log.error("{} ,{},加锁中: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));
  10. }
  11. log.error("{} ,{},加锁后: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));
  12. list.add(a);
  13. }
  14. });
  15. t1.setName("t1");
  16. t1.start();
  17. t1.join();
  18. log.error("===================t2====================");
  19. Thread t2 = new Thread(() -> {
  20. for (int i = 1; i <= list.size(); i++) {
  21. A a = list.get(i-1);
  22. log.error("{} ,{},加锁前: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));
  23. synchronized (a){
  24. log.error("{} ,{},加锁中: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));
  25. }
  26. log.error("{} ,{},加锁后: {}",Thread.currentThread().getName(),i, ClassLayout.parseInstance(a).toPrintableTest(a));
  27. }
  28. });
  29. t2.setName("t2");
  30. t2.start();
  31. t2.join();
  32. }

控制台中
image.png
发现,t2到了20次的时候,也就是经历了20次的偏向撤销,那么再次获取锁的时候,又变成了偏向锁。并且偏向t2。这是为什么呢?
由于偏向撤销是很耗资源的操作,JVM中对偏向撤销进行了优化,当一个类的对象偏向撤销次数达到了20的时候,就会去修改元空间中类模板中epoch的值,了解偏向锁加锁流程的同学应该知道,偏向锁加锁过程中,有一段这样的代码

  1. //如果当前对象的epoch不等于类字节码中的epoch,则尝试重新偏向
  2. else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
  3. // 构造一个偏向当前线程的mark word
  4. markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
  5. if (hash != markOopDesc::no_hash) {
  6. new_header = new_header->copy_set_hash(hash);
  7. }
  8. // CAS替换对象头的mark word
  9. if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
  10. if (PrintBiasedLockingStatistics)
  11. (* BiasedLocking::rebiased_lock_entry_count_addr())++;
  12. }
  13. else {
  14. // 重偏向失败,代表存在多线程竞争,则调用monitorenter方法进行锁升级
  15. CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
  16. }
  17. success = true;
  18. }

而所谓的epoch是否过期,其实做的就是这个操作:判断当前对象中的epoch和元空间中类模板中的epoch值是否一致,不一致则证明过期,需要走重偏向流程。

偏向撤销达到40次

来看看更加极端的情况,创建类A的40个对象到一个list中,t1,新去加锁,t1执行完成以后,然后t2去加锁。然后t3继续对每个对象进行加锁。最后我们new一个A类的对象出来,输出一下对象头的锁标识信息。

  1. public static void main(String[] args) throws InterruptedException {
  2. List<A> list = new ArrayList<>();
  3. log.error("==============t1================");
  4. Thread t1 = new Thread(() -> {
  5. for (int i = 1; i <= 40; i++) {
  6. A a = new A();
  7. log.error("{} ,{},加锁前: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));
  8. synchronized (a) {
  9. log.error("{} ,{},加锁中: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));
  10. }
  11. log.error("{} ,{},加锁后: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));
  12. list.add(a);
  13. }
  14. });
  15. t1.setName("t1");
  16. t1.start();
  17. t1.join();
  18. log.error("===================t2====================");
  19. Thread t2 = new Thread(() -> {
  20. for (int i = 1; i <= 40; i++) {
  21. A a = list.get(i - 1);
  22. log.error("{} ,{},加锁前: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));
  23. synchronized (a) {
  24. log.error("{} ,{},加锁中: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));
  25. }
  26. log.error("{} ,{},加锁后: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));
  27. }
  28. });
  29. t2.setName("t2");
  30. t2.start();
  31. t2.join();
  32. //注意当t2执行完成的时候,其实也才经历了20次的偏向撤销,因为t2执行的时候,后面20次,走的都是重偏向的流程
  33. //此时,list中,前20个存的是轻量锁对象,后20个存的是偏向t2的偏向锁对象。
  34. log.error("==================t3=======================");
  35. Thread t3 = new Thread(() -> {
  36. for (int i = 1; i <= 40; i++) {
  37. A a = list.get(i - 1);
  38. log.error("{} ,{},加锁前: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));
  39. synchronized (a) {
  40. log.error("{} ,{},加锁中: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));
  41. }
  42. log.error("{} ,{},加锁后: {}", Thread.currentThread().getName(), i, ClassLayout.parseInstance(a).toPrintableTest(a));
  43. }
  44. });
  45. t3.setName("t3");
  46. t3.start();
  47. t3.join();
  48. //当t3执行完成,把list中后面20个偏向t2的对象,经过偏向撤销升级成了轻量锁。
  49. //所以到这里,整个A类的对象,总共经历了40次的偏向撤销
  50. A newA = new A();
  51. log.error("newA : {}",ClassLayout.parseInstance(a).toPrintable(a));
  52. }

主要是看最后的newA打印的信息:

image.png
居然直接就是001,无锁不可偏向的标识。要知道,在我们这个程序中,前40个A类的对象被创建出来都是无锁可偏向的状态。而当偏向撤销达到了40次,JVM直接就将A类产生的对象改为了无锁不可偏向的状态

当达到了40次的偏向撤销,JVM会去修改元空间中当前类字节码的数据,让其再次实例化的对象直接就是关闭偏向锁的状态。其实JVM自动做升级也不奇怪,因为相对于频繁的偏向撤销来说,直接升级到轻量锁模式可能更加节省资源吧。