决战圣地玛丽乔亚Day33 ---Synchronized && volatile
再Synchronized和ReetrantLock的区别:
1.Synchronized可以对静态方法,普通方法,代码块加锁。自动加锁释放锁。
ReetrantLock需要手动的加锁释放锁,加锁前需要创建一个ReetrantLock对象
2.Synchronized是非公平锁,竞争获取。ReetrantLock可以使用非公平锁和公平锁两种模式。
3.synchronized 是 JVM 层面通过监视器monitor实现的,而 ReentrantLock 是基于 AQS 实现的
4.Synchronized会有死锁的情况,不能中断。ReetrantLock可以响应中断lockInterruptibly
锁升级的过程:
jdk6之前,只有无锁和重量级锁。jdk6后引入了偏向锁和轻量级锁。
synchronized通过操作系统切换CPU状态来阻塞和唤醒线程,效率低下。所以引入偏向锁和轻量级锁。
首先要明白Synchronized加锁是对什么数据进行操作。
一个对象创建出来后,在内存中的布局大致是:
8字节的Markword
4字节的classPoin(对象引用,指向对象内存地址的变量)
实例数据
字节填充(例如8字节对其的JVM,需要填充一些字节进行对齐)
对于markword:
synchronized对对象加锁,主要改的就是markword数据。
1.升级的过程
无锁--->偏向锁
刚开始,无锁状态,对象没有被任何线程持有过锁,此时的标记位是01(是否偏向锁的标记是0),可以被任意线程持有锁。如果有线程此时获取了锁,是否偏向锁变为1,锁标志位还是01.
变为偏向锁的同时,线程栈中创建一个锁记录(LockRecord)并且会把markword拷贝到LockRecord中,LR中记录锁id和时间戳。同时被加锁对象的markword中的指针也会指向当前持有偏向锁线程的LockRecord。
当其他线程尝试获取对象锁,会去看LR的持有锁的id,如果相同就直接重入。
注意:LR是只有在偏向锁竞争的情况下才会产生,也就是偏向锁失效的情况下,才会用LR记录新的竞争者的信息。所以如果存在LR的偏向锁对象,这时候已经是轻量级锁的状态。
每次进入同步块(即执行monitorenter)的时候都会以从高往低的顺序在栈中找到第一个可用的Lock Record,并设置偏向线程ID;每次解锁(即执行monitorexit)的时候都会从最低的一个Lock Record移除。所以如果能找到对应的Lock Record说明偏向的线程还在执行同步代码块中的代码
注意:当JVM启动了偏向锁模式(Java 6和Java 7里是默认启动的),新创建对象的Mark Word中的ThreadID为0,说明此对象处于偏向锁状态(但未偏向任何线程),也叫作匿名偏向锁状态。
线程A第一次访问同步代码块时,先检查对象头Mark Word中锁标志位是否为01,依此判断此时对象是否处于无锁状态或者偏向锁状态;
若锁标志位是为01,然后判断偏向锁的标识是否为1:
2.1 如果不是,则进入轻量级锁逻辑(使用CAS竞争锁)(注意:此时不是使用CAS尝试获取偏向锁,而是直接升级为轻量级锁;原因是:当偏向锁的标识为0时,表明偏向锁在此对象上被禁用,禁用原因可能是JVM关闭了偏向锁模式,或该类刚经历过bulk revocation,等等。所以应该入轻量级锁逻辑);
2.2 如果是1,表明此对象是偏向锁状态,则进行下一步流程。
判断是偏向锁时,检查对象头Mark Word中记录的ThreadID是否是当前线程A的ID:
3.1 如果是,则表明当前线程A已经获得过该对象锁,以后线程A进入同步代码块时,不需要CAS进行加锁,只会往当前线程A的栈中添加一条Displaced Mark Word为空的Lock Record,用来统计重入的次数。如下图。
3.2 如果不是,则进行CAS操作,尝试将当前线程A的ID替换进Mark Word;
3.2.1 .如果当前对象锁的ThreadID为0(匿名偏向锁状态),则会替换成功(将Mark Word中的Thread id由匿名0改成当前线程A的ID,在当前线程A栈中找到内存地址最高的可用Lock Record,将线程A的ID存入),获得到锁,执行同步代码块。
3.2.2 .如果当前对象锁的ThreadID不为0,即该对象锁已经被其他线程B占用了,则会替换失败,开始进行偏向锁撤销。这也是偏向锁的特点,一旦出现线程竞争,就会撤销偏向锁。
偏向锁是不会主动释放的,只有遇到了其他线程竞争偏向锁时才会主动释放掉偏向锁。
关于偏向锁的撤销:
偏向锁的 撤销(revoke)是一个很特殊的操作,为了执行撤销操作,需要等待全局安全点,此时所有的工作线程都停止了执行。
由于偏向锁的移除需要在全局安全点的时候执行,所以如果并发高的情况,我们可以通过关闭偏向锁来调优系统性能。
1.等待到全局安全点,所有线程暂停,也就是被阻塞。包括当前持有偏向锁的。
2.检查持有偏向锁的线程的状态
如果还存活,JVM还能找得到持有偏向锁的线程。
是否还在执行同步代码快内容
如果仍在执行,升级偏向锁为轻量级锁,原持有偏向锁线程持有轻量级锁。
如果没有在执行,检查是否运行重偏向
允许重偏向:设置为匿名偏向锁(原持有对象释放偏向锁),当前是全局点被阻塞状态。在唤醒后,通过CAS把锁给当前想要锁的线程。
不允许重偏向:markword设置为无锁(未锁定不可偏向),然后升级为轻量级锁,进行CAS竞争。
唤醒暂停线程,从安全点继续执行代码。
add:匿名偏向
把偏向锁的延迟设置为0,-XX:BiasedLockingStartupDelay=0
锁是偏向锁,但是没有记录占有这个偏向锁的线程的指针。这种情况就是匿名偏向。
偏向锁--->轻量级锁
当处于偏向锁的状态下,如果有其他线程来竞争改对象的锁,首先撤销偏向锁升级为轻量级锁。然后线程A、B通过CAS自旋来获取锁,获取锁成功的线程记录LR,改标记位00;没有获取到锁的线程就自旋,超过一定次数的自旋后,会升级为重量级锁。
当没有处在偏向锁的状态下,线程之间直接通过CAS+自旋的方式进行竞争。
轻量级锁--->重量级锁
JVM是运行在用户态的,而要对一个对象上锁就要去内核态的OS申请锁,然后把这把锁返回到用户态。这个用户态到内核态对性能有一定的消耗。
抢占到重量级锁后,markdown里面记录的不再是LR的指针,而指向对象ObjectMonitor。
Monitor的本质是一个同步机制,保证同时只有一个线程能够进入临界区
Monitor:
-ContentionQueue:所有竞争锁的线程都会先到这个队列中阻塞。
-WaitSet等待队列,Owner线程被阻塞后,会加入等待队列,唤醒后加入EntryList
-EntryList阻塞队列,经过初次ContentionQueue筛选的的线程进入。
-Owner 拥有当前Monitor对象的线程
-Count 可重入锁的实现
对于没有抢占到重量级锁的线程,会被存入阻塞队列EntryList,持有重量级锁的线程执行完任务后,会唤醒EntryList的线程去抢占锁。
偏向锁的好处:
如果没有其他线程竞争,那么每次拿锁就是重入,降低了获取锁的代价。但是如果并发高,使用偏向锁还不如不用。
轻量级锁的好处:
通过多次的自旋尝试去获取锁,如果在可接受次数内拿到了锁,就不需要去进行用户态到内核态的切换。但是自旋也占用很多CPU资源,所以需要一个可接受范围的自旋次数。
轻量级锁比直接无脑用重量级锁要好一些。
重量级锁:
如果自旋次数过多,那么可能还不如通过用户态到内核态状态的切换去拿锁资源消耗的少,这种情况下重量级锁更合适一些。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!