1.三种锁的简介
内置锁是JVM提供的最便捷的线程同步工具,在代码块或方法声明上添加synchronized关键字即可使用内置锁。对其优化使的juc包中的各种类性能提高。
重量级锁:内置锁在Java中被抽象为监视器锁(monitor)。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。内核态与用户态的切换上不容易优化,但通过自旋锁,可以减少线程阻塞造成的线程切换
轻量级锁:减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
偏向锁:轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
偏向锁、轻量级锁、重量级锁适用于不同的并发场景:
偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
重量级锁:有实际竞争,且锁竞争时间长。
2.锁的底层原理
线程的开启底层实现
new Thread(()-> System.out.println("How to begin run method ")).start();
private native void start0();
jvm 中会对应一个start0方法和OS函数交互,通过jni技术回调run方法。
在OS控制台输入指令
man pthread_create
加锁的底层实现
synchronized (this){ System.out.println(“get lock”);
man pthread_mutex_init 该os函数会初始化一锁对象
man pthread_mutex_lock 该os函数会将锁对象进行锁定
3,浅析对象头
从openjdk文档中获取对象头的定义 http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
OOP-Klass二分模型 可参考https://blog.csdn.net/jyxmust/article/details/88255594 简单了解下
JVM中对象定义的注释
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
4.从具体代码来看对象头信息
为了能够查看jvm中对象头的布局信息,openjdk提供了jol工具进行对象的查看,http://openjdk.java.net/projects/code-tools/jol/ 引入依赖即可
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
/**
class A{
private boolean flag = false;
private String name = null;
}
对象头的布局信息 展开源码
对象的hashcode存储 展开源码
package com.passion;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
import static java.lang.System.out;
/**
由于这里是小端存储,所以打印出来的hashcode值和二进制位中是反向的,可以借助工具验证 https://tool.oschina.net/hexconvert/
标红的16进制是我们通过hashcode方法打印的结果,标红的1-7Bit位的信息是二进制 hashcode,所以可以确定java对象头当中的mark work里面的后七个字节存储的是hashcode信息,
那么第一个字节当中的八位分别存的 就是分带年龄、偏向锁信息,和对象状态,这个8bit分别表示的信息如下图(其实上图也有信息),这个图会随着对象状态改变而改变, 下图是无锁状态下
关于对象状态一共分为五种状态,分别是无锁、偏向锁、轻量锁、重量锁、GC标记。
5.代码演示三种锁
jvm启动会有默认偏向锁延迟。
轻量锁验证 展开源码
package com.passion;
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
/**
添加jvm启动参数关闭偏向锁延迟,或者线程睡眠4-5秒也可验证开启偏向锁
//关闭延迟开启偏向锁
-XX:BiasedLockingStartupDelay=0
//禁止偏向锁
-XX:-UseBiasedLocking
//启用偏向锁
-XX:+UseBiasedLocking
偏向锁验证 展开源码
package com.passion;
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
/**
偏向锁解锁后还是偏向锁,但是可以看到对象头记录的hashcode没有改变。
验证重量锁的产生,当有多个线程争强锁资源时就会发生。
重量锁验证 展开源码
package com.passion;
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
/**
6.三种锁之间的性能对比
package com.passion;
import java.util.concurrent.CountDownLatch;
/**
@author: liupc
@description: 锁性能测试
**/
public class LockCompitition {
static CountDownLatch c = new CountDownLatch(1000000000);
public static void main(String[] args) throws InterruptedException {
TestCount countTest = new TestCount();
long start = System.currentTimeMillis();
for (int i = 0; i < 2; i++) {
new Thread(() -> {
while (c.getCount() > 0) {
countTest.parse();
}
}).start();
}
// for (int i = 0 ; i <1000000000 ; i++ ){
// countTest.parse();
// }
c.await();
long end = System.currentTimeMillis();
System.out.println(end - start);
}
}
class TestCount {
int i = 0;
public synchronized void parse() {
i++;
LockCompitition.c.countDown();
}
}
锁性能测试 展开源码
偏向锁 9338ms
轻量锁 24913ms
重量锁 35838ms
7.锁分配和膨胀过程可以参考下图: