文章目录
Java锁synchronized关键字学习系列之重量级锁
synchronized的底层实现
我们通过下面这段代码来了解一下synchronized的底层实现
public class RnEnterLockDemo {
public void method() {
synchronized (this) {
System.out.println("start");
}
}
}
然后我们需要通过javap
命令来查看生成的字节码文件。
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。
所以我们首先需要编译出字节码文件
javac RnEnterLockDemo.java
然后我们在使用javap
javap -c RnEnterLockDemo.class
之后我们就可以查看生成出来的内容了。
Compiled from "RnEnterLockDemo.java"
public class org.example.RnEnterLockDemo {
public org.example.RnEnterLockDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void method();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String start
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
}
对于synchronized关键字而言,javac在编译时,会生成对应的monitorenter和monitorexit指令分别对应synchronized同步块的进入和退出,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。
synchronized关键字和synchronized方法,的字节码略有不同,然后我们来看看synchronized方法。
代码如下:
public class RnEnterLockDemo2 {
public synchronized void method() {
System.out.println("start");
}
}
然后我们进行如下命令进行查看
javap -v RnEnterLockDemo2.class
public class org.example.RnEnterLockDemo2
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // org/example/RnEnterLockDemo2
super_class: #6 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #17 // start
#4 = Methodref #18.#19 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #20 // org/example/RnEnterLockDemo2
#6 = Class #21 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 method
#12 = Utf8 SourceFile
#13 = Utf8 RnEnterLockDemo2.java
#14 = NameAndType #7:#8 // "<init>":()V
#15 = Class #22 // java/lang/System
#16 = NameAndType #23:#24 // out:Ljava/io/PrintStream;
#17 = Utf8 start
#18 = Class #25 // java/io/PrintStream
#19 = NameAndType #26:#27 // println:(Ljava/lang/String;)V
#20 = Utf8 org/example/RnEnterLockDemo2
#21 = Utf8 java/lang/Object
#22 = Utf8 java/lang/System
#23 = Utf8 out
#24 = Utf8 Ljava/io/PrintStream;
#25 = Utf8 java/io/PrintStream
#26 = Utf8 println
#27 = Utf8 (Ljava/lang/String;)V
{
public org.example.RnEnterLockDemo2();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public synchronized void method();
descriptor: ()V
flags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String start
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
}
SourceFile: "RnEnterLockDemo2.java"
而对于synchronized方法而言,javac为其生成了一个ACC_SYNCHRONIZED
关键字,在JVM进行方法调用时,发现调用的方法被ACC_SYNCHRONIZED
修饰,则会先尝试获得锁。
但是在JVM底层,对于这两种synchronized语义的实现大致相同。
所以无论是偏向锁还是轻量级锁还是重量级锁,都是依靠着monitorenter
和monitorexit
这两个指令或者是ACC_SYNCHRONIZED
关键字(我们现在大部分都只是讨论monitorenter
和monitorexit
),然后jvm通过这两个指令或者ACC_SYNCHRONIZED
关键字,进行加锁(加什么锁和怎么加锁都是jvm底层来进行实现的)和解锁(如何解锁也是jvm底层实现的)
想更加了解jvm底层如何实现的,可以看看大佬的博客,真的写的很详细写的很好。https://github.com/farmerjohngit/myblog
monitorenter
和monitorexit
关于这两条指令的作用,我们直接参考JVM规范中描述:
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1。
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref. The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor的所有权。
内置锁(ObjectMonitor)
Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
说白了,Java的Monitor,就是JVM(如Hotspot)为每个对象建立的一个类似对象的实现,用于支持Monitor实现(实现了Monitor同步原语的各种功能)。
重量级锁是依赖对象内部的monitor(监视器/管程)来实现的 ,而monitor 又依赖于操作系统底层的Mutex Lock(互斥锁)实现,这也就是为什么说重量级锁比较“重”的原因了,操作系统在实现线程之间的切换时,需要从用户态切换到内核态,成本非常高。在学习重量级锁的工作原理前,首先需要了解一下monitor中的核心概念:
//结构体如下
ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; //线程的重入次数
_object = NULL;
_owner = NULL; //标识拥有该monitor的线程
_WaitSet = NULL; //等待线程组成的双向循环链表,_WaitSet是第一个节点
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //多线程竞争锁进入时的单向链表
FreeNext = NULL ;
_EntryList = NULL ; //_owner从该双向循环链表中唤醒线程结点,_EntryList是第一个节点
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
- owner:标识拥有该monitor的线程,初始时和锁被释放后都为null
- cxq (ConnectionList):竞争队列,所有竞争锁的线程都会首先被放入这个队列中
- EntryList:候选者列表,当owner解锁时会将cxq队列中的线程移动到该队列中
- WaitSet:因为调用wait()或wait(time)方法而被阻塞的线程会被放在该队列中
- count:monitor的计数器,数值加1表示当前对象的锁被一个线程获取,线程释放monitor对象时减1
- recursions:线程重入次数
上图简单描述多线程获取锁的过程,当多个线程同时访问一段同步代码时,首先会进入 Entry Set当线程获取到对象的monitor 后进入 The Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1,若线程调用wait()
方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。当有线程调用notify()
或notifyAll()
方法时,也会释放持有的monitor,并唤醒WaitSet的线程重新参与monitor的竞争。
若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
重量级锁原理
当升级为重量级锁的情况下,锁对象的mark word中的指针不再指向线程栈中的lock record,而是指向堆中与锁对象关联的monitor对象。当多个线程同时访问同步代码时,这些线程会先尝试获取当前锁对象对应的monitor的所有权:
- 获取成功,判断当前线程是不是重入,如果是重入那么recursions+1
- 获取失败,当前线程会被阻塞,等待其他线程解锁后被唤醒,再次竞争锁对象
在重量级锁的情况下,加解锁的过程涉及到操作系统的Mutex Lock进行互斥操作,线程间的调度和线程的状态变更过程需要在用户态和核心态之间进行切换,会导致消耗大量的cpu资源,导致性能降低。
总结
其实JVM已经对synchronized进行了优化。可以直接用,至于锁的力度如何,JVM底层已经做好了我们直接用就行。(如果你不关心底层如何实现的话,当然大多数人学了都是为了面试哈哈哈【面试造火箭,入职拧螺丝】或者是加强对JVM的理解,真正工作中应用基本没有用到,反正我就是这样。不过也可以学习到里面的一些设计思想也是不错的)。关于重量级锁这方面我还是有很多不懂和不理解,网上的资料很多,说法也有丢丢偏差,还需要更多的时间总结和理解沉淀,只是粗略的学习了下,之后当然会继续深入的去学习了。下面也有一些我查看和参考的链接,如果有兴趣的小伙伴也可以深入去学习…
参考
java 偏向锁、轻量级锁及重量级锁synchronized原理