为什么要加锁
- 在多进程的环境下,如果一个资源被多个进程共享,那么对资源的使用往往会表现的随机和无序,这显然是不行的。例如多个线程同时对控制台输出,每个线程都输出的是完整的句子但是多个线程同时同时输出,则输出的内容就会被完全打乱,获取不到本来的信息了。
- 对于这种共享资源,需要进行同步管理,资源在被一个线程占用时,其他线程只能阻塞等待。
- Java 的同步就是使用的对象锁机制来实现的,要使用资源则先获取资源对应的锁后才能操作。
一、 Synchronized 关键字的作用是给对象加锁
- java 中的多线程同步机制通过对象锁来实现,Synchronized 关键字则是实现对对象加锁来实现对共享资源的互斥访问。
- synchronized 关键字实现的是独占锁或者称为排它锁,锁在同一时间只能被一个线程持有。
- JVM 的同步是基于进入和退出监视器对象(Monitor 也叫管城对象)来实现的,每个对象实例都有一个 Monitor 对象,和 Java 对象一起创建并一起销毁。
- Java 编译器,在编译到带有synchronizedg 关键字的代码块后,会插入 monitorenter 和 monitorexit 指令到字节码中,monitorenter 也就是加锁的入口了,线程会为锁对象关联一个 ObjectMonitor 对象。
二、对象基于 ObjectMonitor 加锁的原理
2.1 对象在内存中的布局
2.2 ObjectMonitor 监视器
//结构体如下
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 ;
}
-
ObjectMonitor 象是一个 C++的结构体,用来维护当前持有锁的线程、阻塞等待锁释放的线程链表、调用了 wait 阻塞等待 notify 的线程链表。
-
_cxq 竞争列表:单项链表结构,竞争锁失败的线程,会通过 CAS 将包装成 ObjectWaiter 写入到链表头部,同时为了避免 插入和取出元素的竞争,Owner 会从列表尾部取出元素。
-
EntryList 锁候选者列表:双向链表结构,如果 EntryList 为空 Cxq 不为空,那么线程释放锁的时候,会将 cxq 中的数据移动到 EntryList 中,并制定 EntryList 列表的头结点线程作为 OnDeck 线程。
-
WaitSet:双向链表结构,保存由于不满足执行条件获取锁后主动释放锁 wait 的线程,在被 notify/notifyAll 后会重新参与锁竞争。
2.3 ObjectMonitor 基本工作机制
- 所有期待获得锁的线程,在锁已经被其它线程拥有的时候,这些期待获得锁的线程就进入了对象锁的entry set区域。
- 所有曾经获得过锁,但是由于其它必要条件不满足而需要wait的时候,线程就进入了对象锁的wait set区域 。
- 在wait set区域的线程获得Notify/notifyAll通知的时候,随机的一个Thread(Notify)或者是全部的Thread(NotifyALL)从对象锁的wait set区域进入了entry set中。
- 在当前拥有锁的线程释放掉锁的时候,处于该对象锁的entryset区域的线程都会抢占该锁,但是只能有任意的一个Thread能取得该锁,而其他线程依然在entry set中等待下次来抢占到锁之后再执行。
2.4 执行流程图
2.5 ObjectMonitor::enter() 加锁的过程
- 如果当前线程已经是 owner 则加锁直接成功,只是加锁重入次数recursions+1
- 如果当前线程没有被加锁 即 owner 为空,则尝试 CAS 竞争加锁
- 如果当前线程已经被锁定,则阻塞进入等待队列EntryList 等待释放后再竞争锁,如果 EntryList 超出阈值线程将会阻塞一直到线程数量减少或被其他线程唤醒。
2.6 ObjectMonitor 竞争锁的过程
- 加锁的过程就是多个线程尝试 CAS 操作将 ObjectMonitor 的 owner 设置为自身,并增加重入次数。
- 如果当前线程加锁失败,未能获取到锁,则线程会启动自适应自旋,会循环尝试加锁。这是为了避免线程阻塞的开销。
- 自旋结束仍未获取到锁,则会被包装成 ObjectWaiter 对象,通过 addwaiter 方法加入到 _cxq 竞争队列的头部
- 加入 cxq 队列后,线程仍会再次尝试 CAS 加锁操作,失败后就会被 park 挂起。直到被唤醒重新竞争锁。
2.7 ObjectMonitor::wait() 让出锁
- 如果线程执行后判断不满足后续运行条件,会选择调用 wait 进入等待状态
- 线程会被封装成 ObjectWaiter 对象,最后会被使用 park 方法挂起。
- 调用 wait 第一步会将自身加入到 _waitSet 这个双向链表,后续再调用ObjectMonitor::exit() 来释放锁
2.8 ObjectMonitor::exit() 释放锁的过程
- 持有锁的线程执行完 加锁的临界区代码后,会使用ObjectMonitor::exit()来释放锁。
- 释放锁会将当前的 _owner 设置为空
- 会根据策略,选择将 cxq 队列中的线程移动到 EntryList 队列中唤醒 EntryList 的头部节点 或者直接唤醒 cxq 队列的头部节点让其竞争锁。
- 锁被成功释放后,会将栈帧中的 MarkWord 替换回原来的对象头中。
2.9 Object::notify 方法 执行过程
- 如果 waitSet 为空,则直接结束
- 从 waitSet 头部取出线程节点一个 ObjectWaiter 对象,根据策略 QMode 决定,将线程节点放在哪儿可能放在 cxq 队列头部或者 EntryList 的头部或者尾部,或者被直接唤醒开始竞争锁。
- 这样下次锁被释放时,它就能重新参与竞争锁了。
三、 Java 对同步机制的优化
在 jdk1.6 之前,对于并发控制就只有synchronized 这种办法,如果一个线程已经获得锁,另一个线程就只能阻塞进入等待,后续的线程调度就只能由操作系统来控制了。操作系统对线程的调度,需要频繁的上下文切换,所以效率很低。
来到 jdk1.6 JVM 对加锁进行了一系列的优化
3.1 锁的升级机制
3.2 32 位 JVM 的 markWord 结构
3.3 偏向锁机制
- JDK 1.6 默认开启偏向锁,但是在 JDk15 之后就是默认关闭了,因为偏向锁给 JVM 增加了巨大的复杂性。
- 未加锁时,锁标志位是 01 并且 markword 中包含 HashCode 值的位置
- 施加偏向锁后,markword 中会保存锁的线程 id、epoh 时间戳等信息,同时偏向锁标识变为 1
- 开启偏向锁后,进行加锁会判断偏向锁的线程 id 是否和 markword 线程 id 一致,一致则说明加锁成功可以执行临界区代码;
- 如果不一致则检查是否已偏向某个线程,未偏向则使用 CAS 加锁;未偏向的情况下加锁失败或者存在偏向但不一致,则说明存在竞争。锁会升级成轻量级锁,或者重新偏向。
- 偏向锁只有在出现其他线程竞争时,才会释放,线程不会主动释放偏向锁。
- 偏向锁在调用 wait 方法时会直接升级成重量级锁,因为 wait 方法是重量级锁独有的。
- hashcode 一般会在第一次调用时填入 markword,如果对象已经计算过 hashcode 那么永远无法进入偏向锁状态。如果已经处于偏向锁状态收到计算 Hashcode 的请求,则会膨胀成为重量级锁,对象头指向重量级锁,重量级锁 ObjectMonitor 类中有字段可以记录未加锁状态的 MarkWord
3.4 轻量级锁
如果竞争不激烈,一次获取锁失败就立即进入阻塞状态,那么可能刚进入阻塞状态就立即被唤醒进行加锁。这就会带来上下文的切换,所以轻量级锁获取锁失败时,会进行一定次数或时间的自旋尝试反复获取锁。如果失败则再进入阻塞。
- 当发生多个线程竞争时,偏向锁会变为轻量级锁,锁标志位为00
- 获得锁的线程会先将偏向锁撤销(在安全点),并在栈桢中创建锁记录LockRecord,对象的MarkWord被复制到刚创建的LockRecord,然后CAS尝试将记录LockRecord的owner指向锁对象,再将锁对象的MarkWord指向锁,加锁成功
- 如果CAS加锁失败,线程会自旋一定次数加锁,再失败则升级为重量级锁
3.5 重量级锁(Synchronize 基于监视器实现的锁机制)
3.6 锁粗化
多次加锁操作在JVM内部也是种消耗,如果多个加锁可以合并为一个锁,就可减少不必要的开销。例如一个方法中将代码分成两个加锁的代码块并且是同一个锁对象,则可以合并为一次加锁过程。
3.7 锁消除
如果涉及变量只是一个线程的栈变量,不是共享变量,编译器会尝试消除锁
3.8 分段锁
分段锁不是真正的某种锁,而是使用锁的一种方式;主要就是将大对象拆成小对象,对大对象的加锁变成了对小对象的加锁,避免锁住整个对象。CurrentHashMap 就是这种操作
原文地址:https://blog.csdn.net/weixin_40979518/article/details/134772892
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_37854.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!