synchronized锁原理monitor
monitor(监视器/管程)
java对象分三部分,
- 对象头
- 数据实例
- 填充
对象头分为
- 普通对象- markword(32bit)/klass word(32bit)(指向对应的class对象)
- 数组对象-多一个array length(32bit)数组长度
markword的结构
- hashcode(25) age(4) biased_lock:0(代表是否是偏向锁) | 01 (代表加锁状态) normal状态(正常状态)
- thread:54(线程id) epoch:2 age(4) biased_lock:1(代表是偏向锁)|01(代表加锁状态) biased(偏向锁)
- ptr_to_heavyweight_monitor(30,指向monitor的指针) |10(代表加了重量级 heavyweight lock(重量级锁定)
- ptr_to_lock_record:30 (30位代表锁记录的地址) |00(代表加轻锁) LightWeight Lock(轻量级锁定)
monitor是操作系统提供的,内部结构
-
waitset
-
entrylist(等待队列)
-
owner(monitor拥有者)
`具体步骤1是字节码中的monitorenter操作指令 步骤4是字节码中monitorexit指令
重量级锁的加锁流程:
1. thread0执行synchronize代码的时候,synchronized(obj)的obj对象的markword中ptr_to_heavyweight_monitor会指向一个monitor对象,执行cas操作将monitor的owner设置为thread0
2. thread1执行到synchronized代码时,发现obj的markword指向了一个monitor并且owner不为null 并且不为抢锁线程,这时会进入entrylist进行blocked
3. thread2也一样
4. thread0执行完同步代码退出synchronized,把obj markword里的数据还原比如hashcode age啥的,这些数据是存在monitor对象中的,然后根据不同的策略去唤醒entrylist的thread1和thread2的blocked线程,两个线程去抢owner
5. 如果出现异常,jvm会自动释放锁 执行第4步
`
因为获取monitor是系统指令,需要从用户态转为内核态,出现上下文切换啥的,比较耗费资源,所以java1.6版本对synchronized进行了优化
-
轻量级锁/使用场景是一个对象虽然有多个线程访问,但是不出现竞争使用时是错开的,这时使用轻量级锁来优化 不提供线程到互斥性
绝大多数情况下同步代码程序是交替执行的,这时不应该出现重量级锁
加锁流程:- 在当前线程栈内创建锁记录(lock record jvm层面的对象)对象(内部存储锁obj对象的mark word,还有一个对象指针记录)
- 让锁记录中的object reference指向锁对象,将锁对象的mark word值存入锁记录(这时锁记录记录的是hashcode age 01无锁状态,obj锁对象对象头中存储的是锁记录地址cas操作将锁状态改为轻量级锁状态 锁添加成功
- 如果obj锁对象中的状态已经是00锁记录指向的其他线程,这时加锁失败
cas替换失败了有两种情况
1.如果其他线程已经持有了该object的轻量级锁,这时出现了竞争,进行锁膨胀
2.如果是线程自己执行了锁重入,添加一条lock record在锁持有线程的栈帧中并指向obj锁对象
并进行cas操作修改锁对象的markword为轻量级锁状态(一定失败)因为现在锁对象是轻量级锁状态不是无锁状态 如果锁对象的markword的bit值指向了当前线程栈,说明当前持锁线程就是当前线程 只需要将新添加的lock record中所记录为null
释放锁流程:
- 退出同步代码块时,将锁记录的锁引用字段设置为null 如果有锁记录的displaced为null的情况表示有重入,重置锁记录表示重入计数减一
- 当锁记录不为null,这时使用cas操作恢复锁记录中markword给obj锁对象的对象头中
两种情况: - cas执行成功 解锁成功
- 失败说明轻量级锁进行了锁膨胀或者正在执行膨胀,这时需要执行重量级锁的解锁流程
锁膨胀:
thread0在无锁状态下获取到偏向锁,thread1来执行这个同步代码块并且thread0未执行同步代码块结束,锁对象的markword中存储到threadid不是当前线程 会将thread0的偏向锁升级为轻量级锁,thead1会自旋
轻量级锁出现了竞争(在尝试加轻量级锁的过程中,其他线程为此对象已经加上了轻量级锁),这时需要进行锁膨胀,升级为重量级锁
thread0持有着轻量级锁,thread1进来加轻量级锁,出现竞争这时thread1加锁失败进行锁膨胀,
1. thread1为obj对象申请空闲的monitor对象,cas操作将锁对象的状态改为膨胀中,如果失败说明正在膨胀或膨胀结束,再次自旋获取就行,将轻量级锁存储的markword值存入管程对象内
并将obj锁对象的mark word中指向monitor 修改锁对象的markword锁标识位为重量级锁,thread1自己进入entrylist进行blocked
2. 当thread0执行完同步代码块,解锁的时候发现锁的标识变了,mark word也不是指向栈帧中的锁记录了 monitor的owner指向自己的线程,这时执行重量级锁的解锁流程
自旋优化(适用于多核处理器)
重量级锁出现竞争的时候,使用自旋来优化
偏向锁(优先加偏向锁)
偏向锁假定只有一个线程使用锁
偏向锁的加锁流程:
执行到同步代码块执行monitorenter指令,
1. 在当前线程栈中找一个空闲的锁记录 指向当前锁对象
2. 查看对象头的markword中的锁标识位如果是01就代表是无锁状态或偏向锁状态,然后查看查看第三位如果是1代表是偏向锁状态
第一次使用cas操作将线程的id存入obj对象头的markword,之后发现这个id是自己就说明没有竞争,不用重新cas操作,只要不发生竞争,这个锁就归这个线程所有
撤销偏向状态:
如果调用对象的hashcode,会撤销obj锁对象的偏向状态
当申请obj对象锁的线程是不偏向的那个线程的时候(这时没有竞争)会提交撤销偏向锁的任务到vm线程到任务队列中,
线程的栈和共享的堆在safepoint是冻结状态,只有vm线程在运行gc操作 锁撤销操作等,vm线程会检查所有到线程,检查当前到持锁线程是否还存活
如果持锁线程已经死亡 直接把锁对象到markword改为无锁状态
如果持锁线程未死亡仍然在同步代码块内 会把锁对象的标识位改为轻量级锁状态 偏向锁标识为0
创建一个新的锁记录放入持锁线程的栈中锁记录指向锁对象 锁对象的指针指向锁记录,外部线程进行自旋检查,这时会升级为重量级锁因为有两个线程来竞争锁
如果不在同步代码块内那么
批量重偏向
当相同线程id的偏向锁们被同一个新线程撤销超过20次,重偏向给新的线程
批量撤销
当偏向锁撤销超过40次,整个类的所有对象都变为不可偏向的
锁消除
jit及时编译器在运行时会优化掉
轻量级锁与偏向锁的区别
轻量级锁假定是线程交替执行代码块,而偏向锁假定只有一个线程执行同步代码块
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)