背景
- 在使用多线程场景下,大部分同学都知道使用synchronized关键词来加锁实现多线程下的数据一致性的保证,但是在jdk6以前,synchronized锁是一个十分重量级的锁,之所以说是十分重量级是因为通过synchronized加锁jdk会向操作系统申请锁,锁是操作系统中一种十分核心的资源,可以理解为一个操作系统的锁数量是有限的,当我们每次都去申请操作系统的锁是对锁资源的一种巨大的浪费,另外一方面,大家都知道操作系统分为用户态和内核态,用户态是应用程序所在的一种状态,包括jdk和我们的java程序其实都是运行在用户态的应用程序,而操作系统的锁是需要在内核态才可以使用,这时候就涉及到了用户态升级内核态的转变,这种转变是十分消耗性能和资源的,所以对我们的应用程序性能也是巨大的拖累
- 幸运的是,在jdk6的时候,jdk内部对synchronized做了大量的优化,引入了偏向锁、自旋锁等新锁的加入,在开始的时候锁只会存在偏向锁、自旋锁较为轻量级的锁,如果在轻量级的锁无法搞定问题的情况下才会升级为重量级锁,重量级锁就是操作系统的锁,在大部分情况下偏向锁和自旋锁即可解决大部分问题,避免了操作系统锁资源的浪费,也为我们的应用程序性能带来了巨大的提升
工具准备
在分析锁的时候我们需要JOL工具
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
准备工作
在开始分析锁的四种状态前,我们需要先了解对象在内存中的布局,我的操作系统为64位,以64为操作系统为例,我们先分析对象的布局
以上为对象在内存中的布局分布,分为对象头、实例数据和对齐填充
- 对象头:包含mark word和class pointer,mark word可以理解为对对象的一些信息的标记,后续我们会分析mark word中包含哪些信息
- class pointer:对象指针,此指针指向为当前对象所属的class是哪个,在64位系统中,一般class pointer占用8个字节,但是由于jvm默认开启-XX:+UseCompressedClassPointers,此参数表示jvm开启class pointer压缩,class pointer长度会从原有8字节压缩为4字节
- 数组长度:此项只有数组对象才存在
- 实例数据:对象各种属性及其值
- 对齐填充:为了保证内存的对齐,每个对象会占用8个字节的倍数,如果占用不够8的倍数,则会使用空数据填充至8字节的倍数,例如一个Id对象,其中包含了Integer类型的Id属性和Integer类型的age属性,我们可以分析出,mark word占用8字节,class pointer占用4字节,即对象头共占12字节,实例数据中,id占用4字节,age占用4字节,即实例数据共占用8字节,目前一共占用8+12=20字节,但是由于需要保证内存对齐,故需要填充4个空白字节,所以User对象一共占用了24个字节
public static void main(String[] args){
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
以上我们创建了一个空的Object对象,我们使用jol工具中的ClassLayout打印出内存布局
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
通过输出的日志我们可以看到,前三个为对象头(object header),每个header占用4个字节,共12个字节,12位置开始有4个字节描述为:loss due to the next object alignment,此4个字节为上述提到了对齐填充,所以Object对象占用了16个字节
public static void main(String[] args){
Object o = new Object();
System.out.println(ClassLayout.parseInstance(o).toPrintable());
synchronized (o) {
System.out.println(ClassLayout.parseInstance(o).toPrintable());
}
}
以上代码中,我们增加了synchronized锁,在增加锁之后重新输出内存布局
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) c0 09 f7 06 (11000000 00001001 11110111 00000110) (116853184)
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Process finished with exit code 0
可以看到第一个object header发生了变化,即mark word发生了变化,所以我们可以确认,锁标记记录在对象的mark word中
【注】:其实在mark word中包括以下信息:锁的状态、identity hashcode、GC信息
锁的四种状态
简单的来说,锁一共分为四个状态:new、偏向锁、轻量级锁(又称无锁、自旋锁、自旋适应锁)、重量级锁
【注】:hasCode仅会在调用hashCode()方法时才会生成,默认为空,可以调整jvm参数设置初始生成
new:初始创建对象状态
偏向锁:jdk6后使用synchronized加锁后默认是偏向锁,其中54位用于存储当前线程的指针
轻量级锁(无锁、自旋锁、自旋适应锁):当有多个线程开始竞争此资源的时候,偏向锁自动升级为轻量级锁,所谓轻量级锁其实就是CAS,通俗的解释,轻量级锁相当于给门上锁,至于在门里面的过程并不关心,通过循环不停的判断本线程能否上锁,如果可以上锁则加锁成功,不能上锁则接着循环,每次循环数量+1,当抢夺锁失败次数达到一定的数量后,轻量级锁升级为重量级锁【注:当有多个线程抢夺资源时,每个线程会生成一个Lock Record,轻量级锁中保存的是指向加锁成功的线程的Lock Record指针,相当于给门加锁贴标签】
重量级锁:重量级锁是操作系统级别锁,需要从用户态升级到内核态来加锁,锁的资源也是有限的,对性能会造成大幅度影响,当升级为重量级锁后,会有一个线程对资源加锁成功,其余线程进行队列等待,当加锁线程释放资源后通知队列中其他线程抢夺资源【注:重量级锁默认实现的是非公平锁,公平锁是FIFO,即队首线程优先获取锁,非公平锁是队列中所有线程同时抢夺,以实际抢夺到的线程为准】
锁降级
锁降级可以认为在特殊情况下发生,但是锁降级是没有任何意义的,因为锁降级说明没有任何线程抢夺此资源,则此资源会发生GC回收,既然已经开始执行GC回收资源了,那么此时降级锁也就没有任何意义了
锁粗化
锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗,对于一些极端情况,粗化锁,来提升加锁解锁开销,提升程序性能
一种场景:
public void doSomethingMethod(){
synchronized(lock){
//do some thing
}
//这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
synchronized(lock){
//do other thing
}
}
粗化后的结果:
public void doSomethingMethod(){
//进行锁粗化:整合成一次锁请求、同步、释放
synchronized(lock){
//do some thing
//做其它不需要同步但能很快执行完的工作
//do other thing
}
}
另一种场景:
for(int i=0;i<size;i++){
synchronized(lock){
}
}
粗化后的结果:
synchronized(lock){
for(int i=0;i<size;i++){
}
}
【注】:锁粗化的前提是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了
锁消除
我们都知道字符串拼接中,StringBuffer是线程安全的,StringBuilder是非线程安全的,是因为StringBuffer中的方法使用了synchronized修饰,但是在某些情况下加锁其实是没有必要的,jdk会自动执行优化
public void test(){
StringBuffer sb = new StringBuffer();
sb.append("a").append("b").append("c");
}
上述方法中,每次调用append方法的时候都需要加锁-解锁,极大的消耗资源,在java中,如果检测到StringBuffer对象仅在此方法中使用,则会消除append中的锁,从而提升效率
小短腿的个人公众号上线啦,后续技术分享将优先在公众号进行哈,大家可以关注公众号,小短腿有什么新的分享会及时同步哈