Java的并发编程领域使用锁。例如,在多线程环境中,为了避免某些线程安全问题,可能会出现意外问题。所以我整理了一系列关于JDK锁的问题。我会帮助大家更深入地了解它们。(约翰f肯尼迪)。
同步真的是重量级锁吗?
这个问题相信大多数人在面试中见过,回答是否定的。这要根据JDK版本来判断。JDK版本在1.5之前的版本中使用同步锁定的原理如下:
在需要锁定的资源前后分别添加“monitorenter”和“monitorexit”命令。线程需要进入代码临界区域时,首先必须参与“锁定”(本质上是获得monitor的权限)。可以看出,在旧模式下获取锁的开销比较大。因此,后来JDK的作者在JDK中设计了lock接口,并以CAS方式实现了锁,从而提高了性能。(阿尔伯特爱因斯坦,Northern Exposure,Northern Exposure)
但是,在竞争激烈的时候,采用CAS的方式可能会一直无法锁定。不管进行多少CAS,如果浪费CPU的话,性能损失可能比同步更大。因此,在这种情况下,不如直接升级锁的方式让操作系统介入。
因此,后面会出现锁升级的说法。
升级同步锁定。
偏转锁定
Synchronized升级过程的第一步是升级到偏转锁定。偏向锁本质上是让锁记住请求的线程。
大多数情况下,单线程访问锁定是常见的。JDK的作者在重新配置synchronized时,在对象头上设计了bit位以记录锁定信息。特别是,以下实际案例表明了这一点:
public static void main(string[]args)throws interrupted exception {
Object o=new Object();
Sy(“尚未进入同步块”);
Sy ('markword :' cla (o))。to printable();
//默认JVM启动有预热阶段,因此默认情况下不打开偏转锁定
t(5000);
Object b=new Object();
Sy(“尚未进入同步块”);
Sy ('markword :' cla (b))。toprintable();
Synchronized (o){
Sy(“进入同步块”);
Sy ('markword :' cla (o))。to printable();
}
}
必须引入第三方从属关系,以便能够查看对象头信息。
Dependency
GroupIdorg.o/groupId
工件-核心/工件id
//根据此版本号,查看的内容格式也有所不同
版本0.16/版本
/dependency
控制台输出结果如下:
尚未进入同步块
# warning : unable to attach service ability agent . you can try again with escalated privileges . two options 3360 a)use-djj B)ECC
马克沃德: Java . lang . object object internals :
OFF SZ TYPE DESCRIPTION VALUE
0 8(object header : mark)0 x 0000000000000001(非双ableAge: 0 0)
8 4(对象标题:类)0xf 80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
space losses : 0 bytes internal 4 bytes external=4 bytes total
尚未进入同步块
马克沃德: Java . lang . object object internals :
OFF SZ TYPE DESCRIPTION VALUE
0
8 (object header: mark) 0x0000000000000005 (biasable; age: 0) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total 进入到了同步块 markword:java.lang.Object object internals: OFF SZ TYPE DESCRIPTION VALUE 0 8 (object header: mark) 0x00007000050ee988 (thin lock: 0x00007000050ee988) 8 4 (object header: class) 0xf80001e5 12 4 (object alignment gap) Instance size: 16 bytes Space losses: 0 bytes internal + 4 bytes external = 4 bytes total这个案例中,如果你仔细观察控制台的内容,可以发现,当JVM刚启动的时候,对象头部的锁标志位是无锁状态。但是过了一整子(大概4秒之后),就会变成一个biasable的状态。如果需要调整这个延迟的时间,可以通过参数 -XX:BiasedLockingStartupDelay=0 来控制。
这里我解释下biasable的含义:
biasable是JVM帮我们设置的状态,在这种状态下,一旦有线程访问锁,就会直接CAS修改对象头中的线程id。如果成功,则直接升级为偏向锁。否则就会进入到锁的下一个状态--轻量级锁。
ps:JVM因为在启动预热的阶段中,会有很多步骤使用到synchronized,所以在刚启动的前4秒中,不会直接将synchronized锁的标记升级为biasable状态。这是为了减少一些不必要的性能损耗。
轻量级锁
当锁被一个线程访问的时候,它会变成偏向锁的状态,那么当新的线程再次访问该锁的时候,锁会有什么变化吗?
这里我整理了一张锁的变化流程图,如下所示:
为了验证这个过程,我们可以通过下边这个案例来实践下:
public static void main(String[] args) throws InterruptedException {
// 睡眠 5s
T(5000);
Object o = new Object();
Sy("未进入同步块,MarkWord 为:");
Sy(Cla(o).toPrintable());
synchronized (o){
Sy(("进入同步块,MarkWord 为:"));
Sy(Cla(o).toPrintable());
}
Thread t2 = new Thread(() -> {
synchronized (o) {
Sy("新线程获取锁,MarkWord为:");
Sy(Cla(o).toPrintable());
}
});
();
();
Sy("主线程再次查看锁对象,MarkWord为:");
Sy(Cla(o).toPrintable());
synchronized (o){
Sy(("主线程再次进入同步块,MarkWord 为:"));
Sy(Cla(o).toPrintable());
}
synchronized (b) {
Sy(("主线程再次进入同步块,并且调用hashCode方法,MarkWord 为:"));
b.hashCode();
Sy(Cla(b).toPrintable());
}
}
然后我们来观察下执行的结果:
/Library/Java/JavaVirtualMachine -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:63267,suspend=y,server=n -javaagent:/Users/linhao/Library/Cache -D -classpath "/Library/Java/JavaVirtualMachine IDEA.app/Contents/lib; 并发编程03.查看内存布局信息.MarkWordDemo_4
Connected to the target VM, address: '127.0.0.1:63267', transport: 'socket'
# 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
未进入同步块,MarkWord 为:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
未进入同步块,MarkWord 为:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
进入同步块,MarkWord 为:java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fe8a5009805 (biased: 0x0000001ffa294026; epoch: 0; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
新线程获取锁,MarkWord为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x000070000ba03908 (thin lock: 0x000070000ba03908)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
主线程再次查看锁对象,MarkWord为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
主线程再次进入同步块,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000700009f87980 (thin lock: 0x0000700009f87980)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
主线程再次进入同步块,并且调用hashcode方法,MarkWord 为:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fe8a51391ea (fat lock: 0x00007fe8a51391ea)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Disconnected from the target VM, address: '127.0.0.1:63267', transport: 'socket'
Process finished with exit code 0
通过在控制台中的打印内容我们可以发现,锁的状态一共经历了以下几个变化步骤:
- biasable状态
在这个状态下,锁是一个带偏向的状态,此时如果有线程请求的话。不过如果是刚启动JVM的状态的话,对象头部会是non-biasable状态,只有等jvm预热了一段时间(大约是4秒),才会留转变为biasable状态。
- biased状态
当第一个请求获取到锁的时候,锁的状态会变成偏向锁状态,也就是biased。如果在处于偏向锁状态的时候,还有新的线程参与锁的抢夺,那么就会发生锁的升级,进入到轻量级锁状态阶段。
- thin lock状态
可以看到,当一个锁已经经历过偏向锁状态之后,后去如果再有其他线程访问它,它就会升级为轻量级锁的状态,也就是thin lock状态。
- fat lock状态
当我们在同步代码块中调用hashcode方法的时候,会发现,锁的对象头部会多出一个叫做fat lock的状态,这就意味着,此时该锁已经升级为了重量级锁的状态了。
重量级锁
当一把锁已经处于轻量级锁的状态时,如果此时又有多个线程来尝试获取锁,那么锁就会被多个线程以自旋的方式来请求访问,当访问的次数达到一定上限之后,synchronized就会自动升级为重量级锁的状态了。
当升级为重量级锁的情况下,锁对象的mark word中的指针不再指向线程栈中的lock record,而是指向堆中与锁对象关联的monitor对象。当多个线程同时访问同步代码时,这些线程会先尝试获取当前锁对象对应的monitor的所有权:
- 获取成功,判断当前线程是不是重入,如果是重入那么recursions+1
- 获取失败,当前线程会被阻塞,等待其他线程解锁后被唤醒,再次竞争锁对象
在重量级锁的情况下,加解锁的过程涉及到操作系统的Mutex Lock进行互斥操作,线程间的调度和线程的状态变更过程需要在用户态和核心态之间进行切换,会导致消耗大量的cpu资源,导致性能降低。
有哪几种方式可以使一把锁升级为重量级状态?
- 调用wait方法
- 在同步代码块中调用对象的hashcode方法
最后我绘制了一张锁升级的流程图和大家分享下:
小结
其实JVM已经对synchronized进行了优化。可以直接用,至于锁的力度如何,JVM底层已经做好了我们直接用就行。不过作为一名工程师,了解这些底层原理还是可以增加我们自身内部的功力的。
原文链接: