为什么加锁

一、 Synchronized 关键字作用是给对象加锁

  1. java 中的多线程同步机制通过对象锁来实现,Synchronized 关键字则是实现对对象加锁实现共享资源的互斥访问
  2. synchronized 关键字实现的是独占锁或者称为排它锁,锁在同一时间只能被一个线程持有。
  3. JVM 的同步基于进入退出监视器对象(Monitor 也叫管城对象)来实现的,每个对象实例都有一个 Monitor 对象,和 Java 对象一起创建并一起销毁
  4. Java 编译器,在编译到带有synchronizedg 关键字代码块后,会插入 monitorentermonitorexit 指令字节码中,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 ;  
}  
  1. ObjectMonitor 是 Java 中的一种同步机制,通常被描述一个对象,和 Java 对象一起创建一同销毁

  2. 一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。

  3. ObjectMonitor 象是一个 C++的结构体,用来维护当前持有锁的线程、阻塞等待锁释放的线程链表调用wait 阻塞等待 notify 的线程链表

  4. 其中有几个关键属性** EntryList、WaitSet、cxq、owner、recursions**

  5. _cxq 竞争列表单项链表结构,竞争锁失败的线程,会通过 CAS 将包装成 ObjectWaiter 写入到链表头部,同时为了避免 插入取出元素的竞争,Owner 会从列表尾部取出元素

  6. EntryList 锁候选者列表双向链表结构,如果 EntryList 为空 Cxq 不为空,那么线程释放锁的时候,会将 cxq 中的数据移动到 EntryList 中,并制定 EntryList 列表的头结点线程作为 OnDeck 线程。

    1. OnDeck可以进行锁竞争的线程,如果线程是 OnDeck 状态,那么可以进行 tryLock 操作,如果失败则重新回到 EntryList 的头部
    2. 因为 cxq 中的线程可以自旋,所以 OndeckThread 仍然有可能竞争失败
  7. WaitSet双向链表结构保存由于不满足执行条件获取锁后主动释放锁 wait 的线程,在被 notify/notifyAll 后会重新参与锁竞争。

  8. owner:指向持有 ObjectMonitor 对象的线程

  9. recursions:记录当前锁的重入次数

2.3 ObjectMonitor 基本工作机制

  1. 所有期待获得锁的线程,在锁已经被其它线程拥有的时候,这些期待获得锁的线程就进入了对象锁的entry set区域
  2. 所有曾经获得过锁,但是由于其它必要条件不满足而需要wait的时候,线程就进入了对象锁的wait set区域
  3. 在wait set区域的线程获得Notify/notifyAll通知的时候,随机的一个Thread(Notify)或者是全部的Thread(NotifyALL)从对象锁的wait set区域进入了entry set中。
  4. 当前拥有锁的线程释放掉锁的时候,处于该对象锁的entryset区域的线程都会抢占该锁,但是只能有任意的一个Thread能取得该锁,而其他线程依然在entry set中等待下次来抢占到锁之后再执行

2.4 执行流程图

2.5 ObjectMonitor::enter() 加锁过程

  1. 如果当前线程已经是 owner 则加锁直接成功,只是加锁重入次数recursions+1
  2. 如果当前线程没有被加锁 即 owner 为空,则尝试 CAS 竞争加锁
  3. 如果当前线程已经被锁定,则阻塞进入等待队列EntryList 等待释放后再竞争锁,如果 EntryList 超出阈值线程将会阻塞一直到线程数量减少或被其他线程唤醒

2.6 ObjectMonitor 竞争锁的过程

  1. 加锁的过程就是多个线程尝试 CAS 操作将 ObjectMonitor 的 owner 设置为自身,并增加重入次数。
  2. 如果当前线程加锁失败,未能获取到锁,则线程会启动适应自旋,会循环尝试加锁。这是为了避免线程阻塞的开销。
  3. 自旋结束仍未获取到锁,则会被包装成 ObjectWaiter 对象,通过 addwaiter 方法加入到 _cxq 竞争队列头部
  4. 加入 cxq 队列后,线程仍会再次尝试 CAS 加锁操作失败后就会被 park 挂起。直到被唤醒重新竞争锁。

2.7 ObjectMonitor::wait() 让出锁

  1. 如果线程执行判断不满足后续运行条件,会选择调用 wait 进入等待状态
  2. 线程会被封装成 ObjectWaiter 对象,最后会被使用 park 方法挂起。
  3. 调用 wait 第一步会将自身加入到 _waitSet 这个双向链表,后续再调用ObjectMonitor::exit() 来释放锁

2.8 ObjectMonitor::exit() 释放锁的过程

  1. 持有锁的线程执行完 加锁的临界区代码后,会使用ObjectMonitor::exit()来释放锁。
  2. 释放锁会将当前的 _owner 设置为空
  3. 会根据策略选择将 cxq 队列中的线程移动到 EntryList 队列唤醒 EntryList 的头部节点 或者直接唤醒 cxq 队列的头部节点让其竞争锁。
  4. 锁被成功释放后,会将栈帧中的 MarkWord 替换回原来的对象头中。

2.9 Object::notify 方法 执行过程

  1. 如果 waitSet 为空,则直接结束
  2. 从 waitSet 头部取出线程节点一个 ObjectWaiter 对象,根据策略 QMode 决定,将线程节点放在哪儿可能放在 cxq 队列头部或者 EntryList 的头部或者尾部,或者被直接唤醒开始竞争锁。
  3. 这样下次锁被释放时,它就能重新参与竞争锁了。

三、 Java 对同步机制的优化

jdk1.6 之前,对于并发控制就只有synchronized 这种办法,如果一个线程已经获得锁,另一个线程就只能阻塞进入等待,后续的线程调度就只能由操作系统控制了。操作系统对线程的调度需要频繁的上下文切换,所以效率很低。
来到 jdk1.6 JVM 对加锁进行了一系列优化

3.1 锁的升级机制

3.2 32 位 JVM 的 markWord 结构

yuque_diagram.png

3.3 偏向锁机制

  1. JDK 1.6 默认开启偏向锁,但是在 JDk15 之后就是默认关闭了,因为偏向锁给 JVM 增加了巨大的复杂性。
  2. 未加锁时,锁标志位是 01 并且 markword包含 HashCode 值的位置
  3. 施加偏向锁后,markword 中会保存锁的线程 id、epoh 时间戳等信息,同时偏向锁标识变为 1
  4. 开启偏向锁后,进行加锁会判断偏向锁的线程 id 是否和 markword 线程 id 一致,一致则说明加锁成功可以执行临界区代码
  5. 如果不一致则检查是否已偏向某个线程,未偏向则使用 CAS 加锁;未偏向的情况下加锁失败或者存在偏向但不一致,则说明存在竞争。锁会升级轻量级锁,或者重新偏向。
  6. 偏向锁只有在出现其他线程竞争时,才会释放,线程不会主动释放偏向锁。
  7. 偏向锁在调用 wait 方法时会直接升级重量级锁,因为 wait 方法是重量级锁独有的。
  8. hashcode 一般会在第一次调用时填入 markword,如果对象已经计算hashcode 那么永远无法进入偏向锁状态。如果已经处于偏向锁状态收到计算 Hashcode请求,则会膨胀成为重量级锁,对象头指向重量级锁,重量级锁 ObjectMonitor 类中字段可以记录未加锁状态的 MarkWord

3.4 轻量级

如果竞争不激烈,一次获取锁失败就立即进入阻塞状态,那么可能刚进入阻塞状态就立即被唤醒进行加锁。这就会带来上下文切换,所以轻量级锁获取锁失败时,会进行一定次数或时间的自旋尝试反复获取锁。如果失败则再进入阻塞。

3.5 重量级锁(Synchronize 基于监视器实现的锁机制)

  • 竞争线程激烈,锁则继续膨胀,变为重量级锁,也是互斥锁,锁标志位为10,MarkWord其余内容被替换为一个指向对象锁Monitor的指针

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进行投诉反馈,一经查实,立即删除

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注