synchronized
synchronized
这里暂时只写了重量级锁,偏向锁,轻量级锁后续补充
monitor监视器锁
任何一个对象都有一个Monitor
与之关联,当且一个Monitor
被持有后,它将处于锁定状态。Synchronized
在JVM
里的实现都是基于进入和退出Monitor
对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter
和MonitorExit
指令来实现。
monitorenter
:每个对象都是一个监视器锁(monitor
)。当monitor
被占用时就会处于锁定状态,线程执行
monitorenter
指令时尝试获取monitor
的所有权,过程如下:
- 如果
monitor
的进入数为0,则该线程进入monitor
,然后将进入数设置为1,该线程即为monitor
的所有者; - 如果线程已经占有该
monitor
,只是重新进入,则进入monitor
的进入数加1; - 如果其他线程已经占用了
monitor
,则该线程进入阻塞状态,直到monitor
的进入数为0,再重新尝试获取monitor
的所有权;
monitorexit
:执行monitorexit
的线程必须是objectref
所对应的monitor
的所有者。指令执行时,monitor
的进入数减1,如果减1后进入数为0,那线程退出monitor
,不再是这个monitor
的所有者。其他被这个monitor
阻塞的线程可以尝试去获取这个 monitor
的所有权。
monitorexit
,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;
通过上面两段描述,我们应该能很清楚的看出Synchronized
的实现原理:
Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException
的异常的原因。
方法的同步:(加了Synchronized
的方法)
方法的同步并没有通过指令 monitorenter
和 monitorexit
来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED
标示符。JVM
就是根据该标示符来实现方法的同步的:
当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了,执行线程将先获取monitor
,获取成功之后才能执行方法体,方法执行完后再释放monitor
。在方法执行期间,其他任何线程都无法再获得同一个monitor
对象。
两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM
通过调用操作系统的互斥原语mutex
来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。
什么是moniter
可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。与一切皆对象一样,所有的Java对象是天生的Monitor
,每一个Java对象都有成为Monitor
的潜质。也就是通常说Synchronized
的对象锁,MarkWord
锁标识位为10,其中指针指向的是Monitor
对象的起始地址。在Java虚拟机(HotSpot
)中,Monitor
是由ObjectMonitor
实现的,其主要数据结构如下(位于HotSpot
虚拟机源码ObjectMonitor.hpp
文件,C++实现的):
ObjectMonitor(){
_header = NULL;
_count = 0; // 记录个数
_waiters = 0;
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; // 处于wait状态的线程, 会被加入到_WaitSet
_WaitSetLock = 0;
_Responisble = NULL;
_succ = NULL;
_cxq = NULL;
FreeNext = NULL;
_entryList = NULL; // 处于等待锁block状态的线程, 会被加入到该列表
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread = 0;
}
ObjectMonitor
中有两个队列,_WaitSet
和 EntryList
,用来保存ObjectWaiter
对象列表( 每个等待锁的线程都会被封装成
ObjectWaiter
对象 ),owner
指向持有ObjectMonitor
对象的线程,当多个线程同时访问一段同步代码时:
首先会进入 _EntryList
集合,当线程获取到对象的monitor
后,进入 _Owner
区域并把monitor
中的owner
变量设置为当前线程,同时monitor
中的计数器count
加1;
若线程调用 wait()
方法,将释放当前持有的monitor
,owner变量
恢复为null,count自减1,同时该线程进入 WaitSet
集合中等待被唤醒;
若当前线程执行完毕,也将释放monitor
(锁)并复位count
的值,以便其他线程进入获取monitor
(锁);
当多个线程同时请求某个对象锁时,对象锁会设置⼏种状态⽤来区分请求的线程:
-
Contention List
:所有请求锁的线程将被⾸先放置到该竞争队列 -
Entry List
:Contention List
中那些有资格成为候选⼈的线程被移到Entry List
-
Wait Set
:那些调⽤wait⽅法被阻塞的线程被放置到Wait Set
-
OnDeck
:任何时刻最多只能有⼀个线程正在竞争锁,该线程称为~ -
Owner
:获得锁的线程称为Owner
-
!Owner:释放锁的线程
当⼀个线程尝试获得锁时,如果该锁已经被占⽤,则会将该线程封装成⼀个
ObjectWaiter
对象插⼊到Contention List
的队列的队⾸,然后调⽤ park 函数挂起当前线程。
当线程释放锁时,会从Contention List
或EntryList
中挑选⼀个线程唤醒,被选中的线程叫做 Heir presumptive
即假定继承⼈,假定继承⼈被唤醒后会尝试获得锁,但 synchronized
是⾮公平的,所以假定继承⼈不⼀定能获得锁。这是因为对于重量级锁,线程先⾃旋尝试获得锁,这样做的⽬的是为了减少执⾏操作系统同步操作带来的开销。如果⾃旋不成功再进⼊等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,还有⼀个不公平的地⽅是⾃旋线程可能会抢占了Ready线程的锁。线程获得锁后调⽤ Object.wait
⽅法,则会将线程加⼊到WaitSet
中,当被 Object.notify
唤醒后,会将线程从WaitSet
移动到Contention List
或EntryList
中去。需要注意的是,当调⽤⼀个锁对象的 wait
或 notify
⽅法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。
同时,Monitor
对象存在于每个Java对象的对象头Mark Word
中(存储的指针的指向),Synchronized
锁便是通过这种方式
获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait
等方法会使用到Monitor
锁对象,所以必须在同步代码块中使用。监视器Monitor
有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具