[Java 并发编程] Synchronized 锁升级

初识 Synchronized

  1. 使用 synchronized 修饰方法:这相当于为方法中的全部代码加上 this 对象锁,也就是将该类的实例作为锁对象。这表示,如果一个类中有多个方法被 synchronized 修饰,那么对于该类的同一实例,同一时间最多有一个方法被执行,对于不同实例则不受影响。这是比较粗粒度的锁控制。
  2. 使用 synchronized 修饰静态方法:对于静态方法,JVM 直接通过类名找到对应的 Klass 结构,定位到方法字节码然后执行,没有 this 这个参数的传递。反映到 Java 层面就是我们无法在静态方法内部访问 this 引用,也就是经常提到的,静态方法属于 Class 对象而不属于 Object 对象。所以,synchronized 修饰静态方法实际上是使用了 Class 对象作为监视锁。这意味着,同一时刻一个 JVM 进程内只能有一个线程访问该方法,这是非常粗粒度的锁控制。
  3. 使用 synchronized 同步块:这是更为推荐的做法。使用 synchronized 修饰代码块可以更细粒度地控制并发,从而避免不必要的竞态。诸如读写锁、分段锁都是这种思想的体现。

Synchronized 锁升级

  1. 无锁状态:

    当一个对象从未被当做锁来使用,或者说从未有线程来竞争这个对象,那么该对象处于无锁状态。无锁状态下 MarkWord 中保存的是该对象的 hashcode。我们知道只有调用对象的 hashcode() 方法时才会计算其 hashcode,因此更好的说法是,无锁状态下对象的 MarkWord 中预留了其 hashcode 的位置。

  2. 偏向锁:

    在同一时刻只有一个线程尝试占有资源时,进入偏向锁状态。偏向锁状态下的 MarkWord 保存抢到锁的线程 ID。刚刚提到,这个位置本来是为了保存对象的 hashcode 的,现在又要保存一个线程 ID,这就冲突了。因此,只有没计算过 hashcode 的对象才能进入偏向锁状态,否则其 hashcode 没有其他地方保存。

    偏向锁状态下,是没有竞争的,线程加锁过程如下:线程 ID 为自己的 ID?是 -> 直接访问共享资源;不是 -> 是否为 NULL?是 -> CAS 将自己的 ID 加入 MarkWord;不是 -> 升级轻量级锁。

    这表示,即使两个线程并不是同时,而是先后访问该资源,也会触发偏向锁的撤销,升级为轻量级锁。这是因为,线程在退出偏向锁时,不会撤销偏向锁,它的线程 ID 依然留在 MarkWord 中,不会置为 NULL,除非这个线程挂了,JVM 才会重新偏向。

    这引来了一个问题,就是,当线程 A 发现 MarkWord 中的线程 ID 不是自己,而是线程 B,那 A 能不能知道 B 是正在访问共享资源呢?还是已经退出访问呢?其实它是不知道的。但这很重要,如果 B 正在访问资源,那么它应当直接进入轻量级锁,A 则参与竞争;如果 B 已经退出访问,那就跟它没关系了,A 参与竞争。

    所以偏向锁是不能随便撤销的,必须等待一个全局安全点,在全局安全点,所有用户线程都处于挂起状态,这个过程如下:

    JVM 要进行偏向锁的撤销,则设置一个 global safe point 标志位,用户线程主动检查这个标志位,若为 true,则将自己挂起。那么用户线程在什么时候检查这个标志位呢?在到达安全点的时候。每个用户线程都有自己的安全点,这是 JVM 在代码的特定位置插入的,通常在方法的返回处和跳出循环后。

    在全局安全点,所有用户线程都挂起了,被称为 Stop The World。这个时候,JVM 才能保证 native 线程对堆的独占式访问,从而进行 GC、偏向锁撤销等操作。

    偏向锁撤销需要等待全局安全点,如果频繁发生撤销,可能导致严重的性能问题。因此 JDK 18 开始,偏向锁相关代码已经被彻底删除了,不再支持偏向锁。

  3. 轻量级锁

    线程竞争轻量级锁时,JVM 首先在抢锁线程的栈帧中建立一个锁记录。之后第一件事,线程要在锁记录中保存当前锁对象 MarkWord 的拷贝,也就是对象哈希码等信息。这是因为对象在轻量级锁状态下的 MarkWord 要保存指向线程锁记录的指针,没有空间保存哈希码了。待线程释放锁,还要将这些信息还原回 MarkWord。

    下一步是 CAS,这里的 CAS 涉及到这几个操作数:对象 MarkWord 的内存地址,期望的处于无锁状态的 MarkWord,指向自身锁记录的指针。因此这步操作就是:当且仅当 MarkWord 处于无锁状态时,将 MarkWord 设为自己的锁记录地址。如果操作成功,线程要将锁记录中的 owner 指针指向锁对象,这样在释放锁的时候才能找得到锁对象,接下来线程就可以访问共享资源了。

    如果操作失败,这个时候线程就要让锁对象关联一个 ObjectMonitor

  4. 重量级锁

    为锁对象关联 ObjectMonitor 的源码如下:

    java 复制代码
    ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
      for (;;) {
          // 分支1:对象正被其他线程轻量级锁定
          if (mark->has_locker()) {
              
              // 调用 omAlloc 函数,获得一个空闲的 ObjectMonitor 对象
              ObjectMonitor * m = omAlloc (Self) ;
              // 对新分配的 ObjectMonitor 进行初始化
              m->Recycle();
              m->_Responsible  = NULL ;
              m->OwnerIsThread = 0 ;
              m->_recursions   = 0 ;
              // 设置自旋等待策略的持续时间,这体现了适应性自旋
              m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; 
    
              // 这是一步 CAS,尝试将 MarkWord 的标记状态
              // 从轻量级锁原子性地置换为一个特殊的 INFLATING(膨胀中)状态
              // 这一步非常重要,它确保了同一时刻只有一个线程能成功地将某个对象的锁状态
              // 标记为 INFLATING,从而防止多个线程为同一个对象创建多个 ObjectMonitor。
              markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(),                                                              object->mark_addr(), mark) ;
              
              if (cmp != mark) {
                 omRelease (Self, m, true) ;
                 continue ;
              }
    			
              // 从线程栈的锁记录中,取出并返回轻量级锁创建时保存的无锁状态标记字。
              markOop dmw = mark->displaced_mark_helper() ;
              assert (dmw->is_neutral(), "invariant") ;
    
    		  // 将上面取出的无锁状态标记字设置到 ObjectMonitor 的 _header 字段,
              // 为了未来锁释放后能恢复对象为无锁状态。
              m->set_header(dmw) ;
              
              // 将当前持有轻量级锁的线程设置为 Moniter 的持有者
              m->set_owner(mark->locker());
              // 将锁对象与 Moniter 相关联
              m->set_object(object);
              // 将锁对象的 MarkWord 指向 Monitor
              object->release_set_mark(markOopDesc::encode(m));
              
              // 经历了上面三步则表示锁膨胀成功
              return m ;
          }
    
          // 分支2:对象处于无锁状态
          // 这是 JVM 决定跳过轻量级锁直接使用重量级锁的特殊情况
          assert (mark->is_neutral(), "invariant");
          // 依然获取一个空闲 Moniter
          ObjectMonitor * m = omAlloc (Self) ;
          // 保存无锁状态标记字
          m->set_header(mark);
    
          return m ;
      }
    }

现在锁对象已经关联了一个 ObjectMonitor,可以认为其已经进入重量级锁状态。接下来线程会进入 ObjectMonitor::enter(TRAPS) 方法来抢锁,这一部分的详细源码解读可以看我的另一篇博客:

[Java 并发编程\] 源码层面解读 ObjectMonitor-CSDN博客](https://blog.csdn.net/Steve_Albini/article/details/155351545?spm=1011.2415.3001.5331) 所以在这里我就简述了,线程进入这个方法后会先尝试将 `_owner` 指向自己,如果失败,则进入适应性自旋。自旋一段时间后无果,为该线程关联一个 `ParkEvent`,封装进 `cxq`,此时线程真正被挂起在 OS 内核。 这里的适应性自旋就是指,如果一个锁对象经常被自旋抢到,就说明这个锁适合自旋,JVM 会适当增加其自旋时长,反之则会减少其自旋时长。 看到这可能有些人会有疑问,就是自旋锁难道不是属于轻量级锁吗,怎么需要关联 `ObjectMonitor` 呢?其实你怎么说都可以,因为看问题的角度不同。自旋锁的源码的确存在于 `ObjectMonitor` 中,这个时候锁对象的状态也的确是 10,可以认为已经处于重量级锁状态;但是自旋锁从逻辑上讲应当是一种轻量级锁,这也是没问题的。

相关推荐
Cherry的跨界思维5 小时前
28、AI测试环境搭建与全栈工具实战:从本地到云平台的完整指南
java·人工智能·vue3·ai测试·ai全栈·测试全栈·ai测试全栈
MM_MS5 小时前
Halcon变量控制类型、数据类型转换、字符串格式化、元组操作
开发语言·人工智能·深度学习·算法·目标检测·计算机视觉·视觉检测
꧁Q༒ོγ꧂5 小时前
LaTeX 语法入门指南
开发语言·latex
njsgcs5 小时前
ue python二次开发启动教程+ 导入fbx到指定文件夹
开发语言·python·unreal engine·ue
alonewolf_995 小时前
JDK17新特性全面解析:从语法革新到模块化革命
java·开发语言·jvm·jdk
一嘴一个橘子5 小时前
spring-aop 的 基础使用(啥是增强类、切点、切面)- 2
java
sheji34165 小时前
【开题答辩全过程】以 中医药文化科普系统为例,包含答辩的问题和答案
java
古城小栈5 小时前
Rust 迭代器产出的引用层数——分水岭
开发语言·rust
ghie90906 小时前
基于MATLAB的TLBO算法优化实现与改进
开发语言·算法·matlab