thread:线程ID
age:分代年龄
epoch:用于偏向重定向和偏向撤销
一个对象创建时:
如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的
thread、epoch、age 都为 0
偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 - XX:BiasedLockingStartupDelay=0 来禁用延迟
如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、
age 都为 0,第一次用到 hashcode 时才会赋值
1) 测试延迟特性
@Slf4j(topic = "c.TestBiased") public class TestBiased { public static void main(String[] args) throws InterruptedException { Dog dog = new Dog(); log.debug(JolUtils.toPrintableSimple(dog)); Thread.sleep(4000);//代码进行延时,可以通过VM参数禁用延时 log.debug(JolUtils.toPrintableSimple(new Dog())); } } class Dog{ }
2) 测试偏向锁
//利用 jol 第三方工具来查看对象头信息(注意这里我使用工具类扩展了 jol 让它输出更为简洁) //添加虚拟机参数 -XX:BiasedLockingStartupDelay=0 @Slf4j(topic = "c.TestBiased") public class TestBiased { public static void main(String[] args) throws InterruptedException { Dog dog = new Dog(); log.debug(JolUtils.toPrintableSimple(dog));//打印锁对象头的Makeword信息 synchronized (dog){ log.debug(JolUtils.toPrintableSimple(dog)); } log.debug(JolUtils.toPrintableSimple(dog)); } }
3)测试禁用
在上面测试代码运行时在添加 VM 参数 -XX:-UseBiasedLocking
禁用偏向锁。进行测试:
4) 测试 hashCode
运行结果:
思考:为啥调用hashCode()方法后,偏向锁会被禁用呢?
当调用hashCode后,需要Makeword存放31位的hashCode值,但是偏向锁状态下的threadID占54位,导致没有空间再存hashCode,所以就从Biased -> Normal。
4.2 撤销(偏向锁)
1. 调用对象 hashCode()
调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,所以调用 hashCode 会导致偏向锁被撤销
轻量级锁会在锁记录中记录 hashCode
重量级锁会在 Monitor 中记录 hashCode
**注意:**在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
2. 其它线程使用对象
当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
//轻量级锁应用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的。 public static void test02(){ Dog dog = new Dog(); new Thread(()->{ log.debug(JolUtils.toPrintableSimple(dog));//打印锁对象头的Makeword信息 synchronized (dog){ log.debug(JolUtils.toPrintableSimple(dog)); } synchronized (TestBiased.class){//t1唤醒t2 TestBiased.class.notify(); } log.debug(JolUtils.toPrintableSimple(dog)); },"t1").start(); new Thread(()->{ synchronized (TestBiased.class){ try { TestBiased.class.wait();//t2先等t1执行完,再继续往下执行。(这样做是为了保证俩线程错开) } catch (InterruptedException e) { e.printStackTrace(); } } log.debug(JolUtils.toPrintableSimple(dog));//打印锁对象头的Makeword信息 synchronized (dog){ log.debug(JolUtils.toPrintableSimple(dog)); } synchronized (TestBiased.class){ TestBiased.class.notify(); } log.debug(JolUtils.toPrintableSimple(dog)); },"t2").start(); }
3.调用 wait/notify
wait和notify只有重量级锁有
//重量级锁应用场景:当多个线程交错执行,加锁时间未错开。对应着锁对象wait/notify的使用 public static void test03(){ Dog dog = new Dog(); Thread t1 = new Thread(() -> { log.debug(JolUtils.toPrintableSimple(dog)); synchronized (dog) { log.debug(JolUtils.toPrintableSimple(dog)); try { dog.wait(); } catch (InterruptedException e) { e.printStackTrace(); } log.debug(JolUtils.toPrintableSimple(dog)); } }, "t1"); t1.start(); Thread t2 = new Thread(() -> { try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (dog){ log.debug("notify"); dog.notify(); } log.debug(JolUtils.toPrintableSimple(dog)); }, "t2"); t2.start(); }
4.3 批量重偏向
撤销:从可偏向到不可偏向。也就是一个线程对对象使用完了,不用了,另外一个线程再访问。批量冲偏向是对撤销的优化。
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象
的 Thread ID。
当撤销偏向锁阈值超过 20 次后(指撤销类的对象总次数),jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程
public static void test04() throws InterruptedException { Vector <Dog> list = new Vector <>(); Thread t1 = new Thread(() -> { for (int i = 0; i < 30; i++) { Dog dog = new Dog(); list.add(dog); synchronized (dog) { log.debug(i + "\t" + JolUtils.toPrintableSimple(dog)); } } synchronized (list) { list.notify(); } }, "t1"); t1.start(); Thread t2 = new Thread(() -> { synchronized (list) { try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("===============> "); for (int i = 0; i < 30; i++) { Dog dog = list.get(i); log.debug(i + "\t" + JolUtils.toPrintableSimple(dog)); synchronized (dog) { log.debug(i + "\t" + JolUtils.toPrintableSimple(dog)); } log.debug(i + "\t" + JolUtils.toPrintableSimple(dog)); } }, "t2"); t2.start(); }
运行结果:
4.4 批量撤销
当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象
都会变为不可偏向的,新建的对象也是不可偏向的。
注意:这是40次是类级别的计数, 而不是对象级别的
static Thread t1,t2,t3; public static void main(String[] args) throws InterruptedException { test05(); } private static void test05() throws InterruptedException { Vector<Dog> list = new Vector<>(); int loopNumber = 38; t1 = new Thread(() -> { for (int i = 0; i < loopNumber; i++) { Dog d = new Dog(); list.add(d); synchronized (d) { log.debug(i + "\t" + JolUtils.toPrintableSimple(d)); } } LockSupport.unpark(t2); }, "t1"); t1.start(); t2 = new Thread(() -> { LockSupport.park(); log.debug("===============> "); for (int i = 0; i < loopNumber; i++) { Dog d = list.get(i); log.debug(i + "\t" + JolUtils.toPrintableSimple(d)); synchronized (d) { log.debug(i + "\t" + JolUtils.toPrintableSimple(d)); } log.debug(i + "\t" + JolUtils.toPrintableSimple(d)); } LockSupport.unpark(t3); }, "t2"); t2.start(); t3 = new Thread(() -> { LockSupport.park(); log.debug("===============> "); for (int i = 0; i < loopNumber; i++) { Dog d = list.get(i); log.debug(i + "\t" + JolUtils.toPrintableSimple(d)); synchronized (d) { log.debug(i + "\t" + JolUtils.toPrintableSimple(d)); } log.debug(i + "\t" + JolUtils.toPrintableSimple(d)); } }, "t3"); t3.start(); t3.join(); log.debug(JolUtils.toPrintableSimple(new Dog())); }
思考:加入t4线程后的对象状态是什么?
当loopNumber = 39时,锁对象(开始已经创建的这39个)状态都为Normal,不可偏向。由于撤销次数达到40,之后新创建的对象状态为Normal
当loopNumber < 39时,锁对象(开始已经创建的这loopNumber个)状态都为Normal,不可偏向。但是由于撤销次数 < 40,之后创建的新对象状态默认依旧为 偏向状态
5. 锁粗化
锁粗化就是,当多个方法重复调用锁synchronized ,比如在for 循环中,就可以相当于在synchronized中进行for循环,进行粗化
具体深入剖析待完善…
6. 锁消除
@Fork(1) @BenchmarkMode(Mode.AverageTime) @Warmup(iterations=3) @Measurement(iterations=5) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class MyBenchmark { static int x = 0; @Benchmark public void a() throws Exception { x++; } @Benchmark public void b() throws Exception { Object o = new Object(); synchronized (o) { x++; } } }
**原因分析:**Java运行时有一个JIT即时编译器,会对字节码进行进一步优化。其中一个手段就是逃逸分析锁消除 — 看局部变量是否逃离作用范围,对于b(),其中的 o对象并不会逃离方法范围,给其加锁没有意义,JIT就会吧synchronized优化掉,相当于没有加锁。所以a()和b()性能非常相近。
其中默认 JIT逃逸分析的开关是打开的,可以通过-XX:-EliminateLocks 进行关闭
关闭后运行之:
四、wait / notify原理
Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
WAITING线程和BLOCKED线程的区别?
WAITING:已经获得了锁,但是条件不满足,释放锁后进入WaitSet队列;
BLOCKED:没有获得锁,进入EntryList中等待,状态是BLOCKED
==相同点:==都处于阻塞状态,不占用CPU时间片,将来调度时候也不会考虑。
WAITING线程和BLOCKED线程的唤醒条件?
WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争
BLOCKED 线程会在 Owner 线程释放锁时唤醒