从MESA模型到锁升级:synchronized性能逆袭的底层逻辑

简介: 管程(Monitor)是管理共享资源并发访问的核心机制,基于MESA模型实现,通过入口队列和条件变量队列解决互斥与同步问题。Java的synchronized关键字借鉴该模型,利用monitorenter/monitorexit指令或ACC_SYNCHRONIZED标志实现对象锁,配合wait/notify实现线程通信。JDK 6后引入偏向锁、轻量级锁、自旋锁等优化,显著提升性能。使用时需注意死锁防范、锁粒度控制及可重入特性,合理应用可保障线程安全并提升并发效率。(238字)

管程(Monitor)是一种用于管理共享资源访问的程序结构,能确保同一时刻只有一个线程访问共享资源,解决并发编程中的互斥和同步问题。MESA模型是管程的经典实现,主要由入口等待队列和条件变量等待队列构成。
1)入口等待队列‌:确保线程互斥,多个线程试图进入管程时,仅一个线程能成功,其余线程在入口等待队列中排队。
2)条件变量等待队列‌:解决线程同步问题,线程在管程内执行时,若条件不满足需等待其他线程操作结果,则进入相应条件变量的等待队列。
当线程被notify或notifyAll唤醒后,不会立即执行,而是先进入入口等待队列竞争管程的锁。只有竞争到锁后,线程才能继续执行。因此,被唤醒的线程需循环检验条件是否满足,即采用while (条件不满足) { wait(); } 的编程范式,以避免条件不一致问题。
image.png

synchronized参考了MESA管程模型,对MESA模型进行了精简。在MESA 模型中,一个管程可以有多个条件变量,而Java中的synchronized机制只对应一个条件变量。

public class Test {
   
    public static void main(String[] args) {
   
        // 同步代码块方式加锁
        synchronized (Test.class) {
   

        }
        // 同步方法方式加锁
        func();
    }

    public static synchronized void func() {
   
    }
}

先使用javac编译,生成Test.class的文件。使用javap -c命令来查看字节码。

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2                  // class com/tencent/trpcprotocol/dayu/identify/Test
         2: dup
         4: monitorenter
         6: monitorexit
        12: monitorexit
        15: invokestatic  #3                  // Method func:()V
        18: return

  public static synchronized void func();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=0, args_size=0
         0: return
      LineNumberTable:
        line 25: 0

从字节码可以看出,同步代码块通过monitorenter和monitorexit指令实现锁的获取与释放,而同步方法通过ACC_SYNCHRONIZED标记隐式管理锁。无论采用哪种方式,其本质是对一个对象(Object)的监视器锁(Monitor locking)进行获取,它与synchronized 所在的对象一一对应。
当一个线程进入一个synchronized方法或代码块时,它会尝试获取该对象的监视器锁。如果锁没有被其他线程占用,该线程会获取到锁,并执行临界区的代码。如果锁已经被其他线程占用,该线程会进入阻塞(BLOCKED)状态,并进入同步队列等待锁的释放。
Java 中的 Object 类提供了wait()、notify() 和 notifyAll()方法,这些方法正是基于MESA 模型中的条件变量实现的。当线程执行wait()方法时,会释放锁,并将线程从运行状态转移到等待队列中;当线程被notify或notifyAll 唤醒后,会重新进入同步队列,参与锁的竞争,竞争成功后才能继续执行。
image.png

synchronized性能优化
synchronized在早期仅支持‌重量级锁‌(Mutex locking),依赖操作系统内核态与用户态的切换,性能较差。JDK 6后引入多级锁优化。
1)偏向锁(Biased Locking):针对同一线程反复获取同一锁的场景,偏向锁会记录首次获取锁的线程ID。后续该线程再次获取锁时,无需同步操作即可直接执行,从而消除不必要的锁竞争开销。
2)轻量级锁(Lightweight Locking):通过‌CAS操作和锁标记位实现。线程尝试以CAS方式将锁标记为轻量级状态,若成功则直接获取锁;若失败,则通过自旋等待锁释放。此机制在竞争不激烈时避免了内核态切换,显著提升锁操作效率。
3)自旋锁(Spin Locking):当锁获取失败时,线程会在有限次数内循环等待(自旋),而非立即进入阻塞状态。适用于锁持有时间极短的场景,通过减少线程挂起与唤醒开销提升性能。
4)锁消除(Lock Elimination):Java虚拟机的即时编译器在运行时分析代码,若检测到某些锁操作(如对局部对象的加锁)无实际意义,会自动移除这些锁,从而优化程序性能。

synchronized使用注意
1)避免死锁‌:如果两个或多个线程互相等待对方释放锁,会导致死锁。由于synchronized 不提供超时机制,可以使用 JUC并发包的ReentrantLock 并设置超时时间来避免死锁。

public class DeadlockAvoidance {
   
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    // 线程1
    public void method1() {
   
        synchronized (lock1) {
   
            System.out.println(Thread.currentThread().getName() + " locked lock1");
            synchronized (lock2) {
   
                System.out.println(Thread.currentThread().getName() + " locked lock2");
            }
        }
    }

    // 线程2
    public void method2() {
   
        synchronized (lock1) {
   
            System.out.println(Thread.currentThread().getName() + " locked lock1");
            synchronized (lock2) {
   
                System.out.println(Thread.currentThread().getName() + " locked lock2");
            }
        }
    }
}

‌2)锁粒度:锁粒度指的是对共享资源加锁的范围。锁的粒度过大,会导致并发性能下降;锁的粒度过小,会增加锁管理的开销。

public class SynchronizedBlockExample {
   
    private int count = 0;

    // 只同步增量操作部分,避免同步无关代码
    public void increment() {
   
        synchronized (this) {
   
            count++;
        }
    }

    public int getCount() {
   
        return count;
    }
}

3)理解可重入性:synchronized 是可重入的,也就是说,同一个线程可以多次获得同一个锁而不会发生死锁

public class ReentrantLockExample {
   
    // 如果一个线程在同步方法内部调用了另一个同步方法,它仍然能获取锁
    public synchronized void methodA() {
   
        System.out.println("Entering method A");
        methodB();  // 调用另一个同步方法
    }

    public synchronized void methodB() {
   
        System.out.println("Entering method B");
    }
}

未完待续

很高兴与你相遇!如果你喜欢本文内容,记得关注哦

目录
相关文章
|
9月前
|
人工智能 运维 Serverless
函数计算 × MSE Nacos : 轻松托管你的 MCP Server
本文将通过一个具体案例,演示如何基于 MCP Python SDK 开发一个标准的 MCP Server,并将其部署至函数计算。在不修改任何业务代码的前提下,通过控制台简单配置,即可实现该服务自动注册至 MSE Nacos 企业版,并支持后续的动态更新与统一管理。
922 90
|
9月前
|
安全 Java
揭秘JUC:volatile与CAS,并发编程的两大基石
JUC并发包由Doug Lea打造,基于volatile与CAS实现线程安全,依托happens-before原则保障内存可见性,通过AQS框架构建锁与同步器,形成高效并发编程基石。
504 8
|
8月前
|
存储 Java Go
Goroutine间的“灵魂管道”:Channel如何实现数据同步与因果传递?
Channel是Go实现CSP并发模型的核心,通过goroutine间安全的数据传递与同步,避免锁和条件变量的复杂性。其底层基于循环队列与等待队列,支持发送接收、阻塞唤醒等机制,并建立happens-before因果关系,确保并发确定性。
1483 0
|
9月前
|
JavaScript 前端开发 数据可视化
[NMP v2] NeteaseMiniPlayer v2 搭建个人网站网易云迷你播放器
NeteaseMiniPlayer v2 [NMP v2]是一款高颜值、无依赖的前端嵌入式网易云音乐迷你播放器,,轻松部署于个人网站,提升音网站体验。
786 6
[NMP v2] NeteaseMiniPlayer v2 搭建个人网站网易云迷你播放器
|
9月前
|
Kubernetes Java 调度
无需接入执行器,0 代码改造实现微服务任务调度
本文提出了一种基于云原生的任务调度新方案,不需要依赖SDK,不依赖语言,实现定时调度和分布式跑批。
487 77
|
9月前
|
机器学习/深度学习 运维 监控
别让运维只会“救火”——用数据点燃业务增长的引擎
别让运维只会“救火”——用数据点燃业务增长的引擎
317 12
|
9月前
|
存储 消息中间件 Kafka
Confluent 首席架构师万字剖析 Apache Fluss(一):核心概念
Apache Fluss是由阿里巴巴与Ververica合作开发的Flink表存储引擎,旨在提供低延迟、高效率的实时数据存储与变更日志支持。其采用TabletServer与CoordinatorServer架构,结合RocksDB和列式存储,实现主键表与日志表的统一管理,并通过客户端抽象整合湖仓历史数据,弥补Paimon在实时场景下的性能短板。
1125 23
Confluent 首席架构师万字剖析 Apache Fluss(一):核心概念
|
9月前
|
SQL 人工智能 运维
一场由AI拯救的数据重构之战
本文以数据研发工程师小D的日常困境为切入点,探讨如何借助AI技术提升数据研发效率。通过构建“数研小助手”智能Agent,覆盖需求评估、模型评审、代码开发、运维排查等全链路环节,结合大模型能力与内部工具(如图治MCP、D2 API),实现影响分析、规范检查、代码优化与问题定位的自动化,系统性解决传统研发中耗时长、协作难、维护成本高等痛点,推动数据研发向智能化跃迁。
545 29
一场由AI拯救的数据重构之战
|
存储 前端开发 安全
前端如何存储数据:Cookie、LocalStorage 与 SessionStorage 全面解析
本文全面解析前端三种数据存储方式:Cookie、LocalStorage与SessionStorage。涵盖其定义、使用方法、生命周期、优缺点及典型应用场景,帮助开发者根据登录状态、用户偏好、会话控制等需求,选择合适的存储方案,提升Web应用的性能与安全性。(238字)
1243 0

热门文章

最新文章