关于java的偏向锁和轻量级锁
基础知识 java中锁有四种级别,分别是
其中 偏向锁 是锁仅获取一次,后续加锁只需要判断对象头里记录的线程是不是当前线程,如果是就直接进入同步。这样可以免去频繁的加锁
而如果有别的线程来竞争这个锁,那么这个锁就会升级成轻量级锁,线程通过自旋去竞争锁。
锁升级过程是不可逆 的
偏向锁 在开启偏向锁(默认开启)的情况下,一个锁将由第一个获取锁的线程偏向,对象头里将会记录对应的线程信息。以后这个线程只需要对比记录的线程是否一致,如果一致则不需要加锁解锁操作
要注意的是,即使线程已经结束了,偏向锁状态也不会消失。而是等到另一个线程来获取这个锁,因为和记录不一致,将升级成轻量级锁
轻量级锁 轻量级锁是通过cas+自旋来尝试获取锁,首先利用CAS尝试把对象原本的Mark Word 更新为Lock Record的指针,成功就说明加锁成功,然后执行相关同步操作。如果不成功,则开始自旋,不断去CAS。如果指定自旋次数内无法获取锁,则升级为重量级锁,这一过程代表着有过多的线程在竞争锁,才会导致某个线程无法获取锁
测试用例 根据上述知识,设计一个演示程序。基本逻辑如下
首先有一个线程每隔1秒去获取锁,此时锁应该为偏向锁 程序运行5.5秒后,另外起线程去获取锁,0.5秒是为了错开竞争,保证之前那个线程不会在此时来获取锁。由于线程不一致,锁升级为轻量级锁 再次间隔5.5秒后,再起线程每隔1秒去获取锁,此时会和第一个线程竞争,锁应该升级为重量级锁 以下为一个两个线程竞争锁的程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 private static Object sync = new Object ();public static void main (String[] args) { VirtualMachine vm = VM.current(); long mark = vm.getLong(sync, 0 ); System.out.println(lockDes(mark)); new Thread (new Runnable () { @Override public void run () { while (true ) { long l = System.nanoTime(); long start = System.currentTimeMillis(); synchronized (sync) { long mark = vm.getLong(sync, 0 ); System.out.printf("%s获取了锁 %d %d %s %n" , Thread.currentThread().getName(), System.nanoTime() - l, (System.currentTimeMillis() - start) / 1000 , lockDes(mark)); } try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); try { TimeUnit.SECONDS.sleep(5 ); TimeUnit.MILLISECONDS.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } long l = System.nanoTime(); long start = System.currentTimeMillis(); System.out.println("开始竞争" ); synchronized (sync) { mark = vm.getLong(sync, 0 ); System.out.printf("只竞争一次的%s获取了锁 %d %d %s %n" , Thread.currentThread().getName(), System.nanoTime() - l, (System.currentTimeMillis() - start) / 1000 , lockDes(mark)); } try { TimeUnit.SECONDS.sleep(5 ); TimeUnit.MILLISECONDS.sleep(500 ); } catch (InterruptedException e) { e.printStackTrace(); } new Thread (new Runnable () { @Override public void run () { while (true ) { long l = System.nanoTime(); long start = System.currentTimeMillis(); synchronized (sync) { long m = vm.getLong(sync, 0 ); System.out.printf("第三个线程%s获取了锁 %d %d %s %n" , Thread.currentThread().getName(), System.nanoTime() - l, (System.currentTimeMillis() - start) / 1000 , lockDes(m)); try { TimeUnit.SECONDS.sleep(1 ); } catch (InterruptedException e) { e.printStackTrace(); } } } } }).start(); }private static String lockDes (long mark) { long bits = mark & 0b11 ; switch ((int ) bits) { case 0b11 : return "(marked: GC)" ; case 0b00 : return "(thin lock: 轻量级锁)" ; case 0b10 : return "(fat lock: 重量级锁)" ; case 0b01 : int tribits = (int ) (mark & 0b111 ); switch (tribits) { case 0b001 : return "(non-biasable)" ; case 0b101 : return "(biased: 偏向锁)" ; } } return "错误数据" ; }
输出如下:
Thread-0获取了锁 82570 0 (biased: 偏向锁) Thread-0获取了锁 28500 0 (biased: 偏向锁) Thread-0获取了锁 26974 0 (biased: 偏向锁) Thread-0获取了锁 29410 0 (biased: 偏向锁) Thread-0获取了锁 25658 0 (biased: 偏向锁) Thread-0获取了锁 19214 0 (biased: 偏向锁) 开始竞争 只竞争一次的main获取了锁 2132418 0 (thin lock: 轻量级锁) Thread-0获取了锁 19542 0 (thin lock: 轻量级锁) Thread-0获取了锁 24973 0 (thin lock: 轻量级锁) Thread-0获取了锁 28656 0 (thin lock: 轻量级锁) Thread-0获取了锁 29874 0 (thin lock: 轻量级锁) Thread-0获取了锁 30851 0 (thin lock: 轻量级锁) 第三个线程Thread-1获取了锁 46264 0 (thin lock: 轻量级锁) Thread-0获取了锁 943671726 0 (fat lock: 重量级锁) 第三个线程Thread-1获取了锁 1384640 0 (fat lock: 重量级锁) Thread-0获取了锁 77250 0 (fat lock: 重量级锁) 第三个线程Thread-1获取了锁 574245 0 (fat lock: 重量级锁) 第三个线程Thread-1获取了锁 21957 0 (fat lock: 重量级锁) 第三个线程Thread-1获取了锁 26252 0 (fat lock: 重量级锁) Thread-0获取了锁 2004668640 2 (fat lock: 重量级锁) 第三个线程Thread-1获取了锁 1563713 0 (fat lock: 重量级锁) 第三个线程Thread-1获取了锁 34069 0 (fat lock: 重量级锁)
流程 借助网上的一篇博客 ,来梳理一下过程
1、主线程来竞争锁 2、判断锁为偏向锁,且指向的线程0依旧存活 3、暂停线程0 4、将锁升级为轻量级锁 5、继续执行线程0 6、主线程开始自旋 7、主线程执行 8、主线程释放锁 9、线程0获取锁,此时应该为轻量级锁
解释 首先上面代码中用了jol 来获取锁状态
1 2 3 4 5 6 7 <dependency > <groupId > org.openjdk.jol</groupId > <artifactId > jol-core</artifactId > <version > 0.16</version > </dependency >
主要是获取对象头低3位
锁的升级过程基本可以理解了,但是有一个问题就是:一个对象初始状态怎么就是偏向锁了?
查阅jdk源码(markOop.hpp,从jdk6到jdk11,这部分都没有改动),关于markWord 部分注释如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
另外还有jol 中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 private static String parseMarkWord (long mark) { long bits = mark & 0b11 ; switch ((int ) bits) { case 0b11 : return "(marked: " + toHex(mark) + ")" ; case 0b00 : return "(thin lock: " + toHex(mark) + ")" ; case 0b10 : return "(fat lock: " + toHex(mark) + ")" ; case 0b01 : String s = "; age: " + ((mark >> 3 ) & 0xF ); int tribits = (int ) (mark & 0b111 ); switch (tribits) { case 0b001 : int hash = (int )(mark >>> 8 ); if (hash != 0 ) { return "(hash: " + toHex(hash) + s + ")" ; } else { return "(non-biasable" + s + ")" ; } case 0b101 : long thread = mark >>> 10 ; if (thread == 0 ) { return "(biasable" + s + ")" ; } else { return "(biased: " + toHex(thread) + "; epoch: " + ((mark >> 8 ) & 0x2 ) + s + ")" ; } } default : return "(parse error)" ; } }
关于偏向锁部分,重点在于倒数第三位的baised_lock 。 虽然名字叫偏向锁标记,但是我看下来结果更像是baiseable_lock ——是否可以偏向的标志;如果是1,代表这个对象可以拥有偏向锁;区分是否已经获取了偏向锁,则靠高54位是否为0,获取了偏向锁的情况下,这54bit应该是对应的threadId。
在使用了 -XX:-UseBiasedLocking 关闭偏向锁后,这一位就都变成0了。