python在使用synchronized关键字时,需要注意哪些细节问题?

synchronized 是 Java 中最基础的线程同步关键字,虽然使用简单,但在实际开发中若忽略细节,容易导致 线程安全失效、性能瓶颈、死锁 等问题。以下是使用 synchronized 时必须注意的核心细节:

一、锁的对象选择:明确 "锁什么"(最易出错)

synchronized 的核心是 锁定 "对象" ,而非代码块或方法本身。锁的对象决定了同步范围,选不对会直接导致线程安全失效。

1. 实例方法锁(synchronized void method()

  • 锁对象 :当前实例(this)。

  • 细节 1:不同实例的锁相互独立,多线程操作不同实例时不会同步。

    java

    运行

    java 复制代码
    class Counter {
        private int count = 0;
        public synchronized void increment() { count++; } // 锁 this
    }
    
    // 错误场景:多线程操作不同实例,count 会出错
    Counter c1 = new Counter();
    Counter c2 = new Counter();
    // 线程1操作 c1,线程2操作 c2,两者无锁竞争,count 可能统计不准确
  • 细节 2:若实例被回收,锁会失效(需避免锁对象被频繁创建 / 销毁)。

2. 静态方法锁(synchronized static void method()

  • 锁对象 :当前类的 Class 对象(全局唯一)。

  • 细节:所有实例共享同一把锁,即使操作不同实例,也会同步。适合需要 "全局唯一" 同步的场景(如单例模式、全局计数器)。

    java

    运行

    java 复制代码
    class Counter {
        private static int globalCount = 0;
        public synchronized static void increment() { globalCount++; } // 锁 Counter.class
    }
    
    // 正确场景:多线程操作不同实例,也会同步 globalCount
    Counter c1 = new Counter();
    Counter c2 = new Counter();
    // 线程1调用 c1.increment(),线程2调用 c2.increment(),会竞争同一把锁

3. 代码块锁(synchronized(lockObj) {}

  • 锁对象:自定义的锁对象(需保证多线程共享同一实例)。

  • 核心细节

    • 锁对象必须是 引用类型 (不能是基本类型,如 intlong,会自动装箱为不同对象)。

      java

      运行

      javascript 复制代码
      // 错误:锁对象是基本类型,每次自动装箱为新 Integer 对象,锁失效
      int lock = 0;
      synchronized(lock) { ... } 
      
      // 正确:使用自定义引用类型,确保多线程共享同一实例
      Object lock = new Object(); // 全局唯一锁对象
      synchronized(lock) { ... }
    • 避免使用 StringInteger 等常量 / 缓存对象作为锁(可能因常量池复用导致锁冲突)。

      java

      运行

      javascript 复制代码
      // 错误:"lock" 是常量池对象,其他地方若也用 "lock" 作为锁,会导致无关代码同步
      synchronized("lock") { ... }
    • 锁对象不能为 null(会抛出 NullPointerException)。

二、锁的粒度:避免 "过度同步" 或 "同步不足"

锁的粒度直接影响性能,需在 "线程安全" 和 "性能" 之间平衡。

1. 避免 "锁粒度太大"(过度同步)

  • 问题:将无关的操作都放在同一个 synchronized 块中,导致线程长时间阻塞,性能下降。

  • 优化:仅对 "共享变量的修改 / 访问" 进行同步,缩小同步块范围。

    java

    运行

    csharp 复制代码
    // 错误:同步块包含无关操作(日志打印、本地变量计算),线程阻塞时间长
    public synchronized void update() {
        log.info("开始更新"); // 无关操作,无需同步
        int temp = 1 + 2;    // 本地变量,无需同步
        sharedVar = temp;    // 仅这一步需要同步
    }
    
    // 正确:缩小同步块,仅包裹临界区
    public void update() {
        log.info("开始更新");
        int temp = 1 + 2;
        synchronized(lock) {
            sharedVar = temp; // 仅临界区同步
        }
    }

2. 避免 "锁粒度太小"(同步不足)

  • 问题:若多个操作需要原子性(如 "检查 - 修改 - 提交"),仅同步单个步骤会导致线程安全问题。

  • 示例:

    java

    运行

    typescript 复制代码
    // 错误:check 和 set 未原子化,多线程下可能重复初始化
    private volatile Object instance;
    public Object getInstance() {
        if (instance == null) { // 检查(未同步)
            synchronized(lock) {
                instance = new Object(); // 修改(同步)
            }
        }
        return instance;
    }
    
    // 正确:双重检查锁(需配合 volatile 禁止重排序)
    public Object getInstance() {
        if (instance == null) {
            synchronized(lock) {
                if (instance == null) { // 二次检查(同步内)
                    instance = new Object();
                }
            }
        }
        return instance;
    }

三、线程安全的边界:synchronized 不能跨方法 / 跨线程传递

synchronized 仅保证 同一锁对象 下的同步,跨方法、跨线程时无法保证线程安全。

1. 跨方法同步失效

  • 问题:若一个方法同步,另一个方法修改同一共享变量但不同步,会导致线程安全问题。

    java

    运行

    csharp 复制代码
    class Counter {
        private int count = 0;
        public synchronized void increment() { count++; } // 同步
        public void decrement() { count--; } // 未同步,跨方法修改共享变量
    }
    // 线程1调用 increment(),线程2调用 decrement(),count 会出错

2. 跨线程同步失效

  • 问题:synchronized 仅对 "竞争同一锁" 的线程有效,若线程间通过异步回调、消息队列等方式交互,同步无效。
  • 示例:线程 A 将数据存入共享变量,线程 B 通过消息队列通知后读取,但未同步,可能读取到旧值。

四、死锁风险:避免循环等待

synchronized 可能导致死锁,需满足 四个必要条件:互斥、请求与保持、不可剥夺、循环等待。核心是避免 "循环等待"。

1. 死锁示例

java

运行

scss 复制代码
// 线程1:先锁 a,再锁 b
synchronized(a) {
    Thread.sleep(100); // 给线程2争取时间
    synchronized(b) { ... }
}

// 线程2:先锁 b,再锁 a
synchronized(b) {
    Thread.sleep(100);
    synchronized(a) { ... }
}
  • 结果:线程 1 持有 a 等待 b,线程 2 持有 b 等待 a,互相阻塞,死锁。

2. 避免死锁的细节

  • 固定顺序 申请锁(如按对象哈希值排序)。
  • 避免嵌套锁(尽量减少锁的嵌套层级)。
  • 给锁添加 超时时间 (可通过 Lock 接口的 tryLock(timeout) 实现,synchronized 本身不支持,需手动规避)。
  • 减少锁的持有时间(尽快释放锁,避免线程长时间占用)。

五、可见性与有序性:synchronized 的隐含保证

synchronized 不仅保证原子性,还隐含可见性和有序性,但需注意细节:

1. 可见性保证

  • 线程释放锁时,会将工作内存中的修改写回主内存;线程获取锁时,会从主内存加载最新变量值。
  • 细节:仅对 "同一锁保护的共享变量" 有效,若变量未被锁保护,仍可能出现可见性问题。

2. 有序性保证

  • 同步块内的指令禁止重排序,且释放锁的操作 happens-before 后续获取同一锁的操作。
  • 细节:同步块外的指令仍可能重排序,仅同步块内的指令有序。

六、性能相关细节

1. synchronized 的优化(JDK 1.6+)

JDK 1.6 对 synchronized 进行了优化,引入了 偏向锁、轻量级锁、重量级锁 的升级机制,避免直接使用重量级锁(操作系统互斥量)导致的性能开销。

  • 细节:无需手动干预,JVM 自动优化,但需避免锁的频繁竞争(否则会升级为重量级锁,性能下降)。

2. 避免锁竞争激烈

  • 若多个线程频繁竞争同一把锁,会导致线程阻塞、上下文切换,性能下降。

  • 优化方案:

    • 拆分锁(如 ConcurrentHashMap 的分段锁思想,将大锁拆分为多个小锁)。
    • 使用无锁并发工具(如 Atomic 原子类、ConcurrentLinkedQueue)。
    • 减少锁的持有时间(如前面提到的缩小同步块)。

3. 不要用 synchronized 修饰频繁调用的方法

  • 若方法被高频调用(如每秒上万次),synchronized 的同步开销会被放大,建议改用无锁方案或优化锁粒度。

七、其他细节

1. synchronized 不能中断线程

  • synchronized 是不可中断的,若线程在等待锁时被阻塞,只能通过以下方式结束:

    • 持有锁的线程释放锁(执行完同步块或抛出异常)。
    • 程序终止。
  • 若需要中断等待锁的线程,需使用 Lock 接口的 lockInterruptibly() 方法。

2. synchronized 不能超时等待

  • synchronized 没有超时机制,线程会一直等待锁,直到获取到为止。
  • 若需要超时等待,需改用 Lock 接口的 tryLock(timeout, unit) 方法。

3. 静态锁与实例锁互不干扰

  • 静态方法锁(锁 Class 对象)和实例方法锁(锁 this)是不同的锁,多线程同时调用时不会竞争。

    java

    运行

    arduino 复制代码
    class Test {
        public synchronized static void staticMethod() {} // 锁 Test.class
        public synchronized void instanceMethod() {} // 锁 this
    }
    // 线程1调用 staticMethod(),线程2调用 instanceMethod(),无锁竞争

4. 异常会释放锁

  • 若同步块内抛出异常,JVM 会自动释放锁,避免锁泄露(线程持有锁但崩溃,导致其他线程无法获取)。
  • 细节:若需要在异常后执行特定逻辑(如资源释放),需在 finally 块中处理,但锁的释放无需手动操作。

总结

使用 synchronized 时,核心是抓住 "锁对象正确、锁粒度合适、避免死锁、利用隐含的可见性 / 有序性保证" 这四大关键点:

  1. 锁对象必须是多线程共享的引用类型,避免用常量、基本类型、null
  2. 同步块仅包裹临界区,避免过度同步或同步不足。
  3. 规避死锁,尤其是循环等待和嵌套锁。
  4. 了解 synchronized 的优化机制,避免不必要的性能损耗。

在高并发场景下,若 synchronized 无法满足需求(如超时等待、中断、公平锁),可考虑使用 java.util.concurrent.locks.Lock 接口(如 ReentrantLock),它提供了更灵活的锁控制能力。

相关推荐
BingoGo3 小时前
PHP clone 之后,为什么改副本会影响原对象?
后端·php
JaguarJack3 小时前
PHP clone 之后,为什么改副本会影响原对象?
后端·php·服务端
小灰灰搞电子4 小时前
Rust+Slint 实现动态消息提示框源码分享
开发语言·后端·rust
小奏技术4 小时前
10 MB 的 Postman 替代品,启动不到 1 秒
后端
东风破_4 小时前
Text2SQL :用自然语言操作 SQLite 数据库
人工智能·后端
IT_陈寒8 小时前
Python的多线程就是个假把式,我算是体验到了
前端·人工智能·后端
码事漫谈8 小时前
如果 AI 要圈养人类,它可能不需要笼子
后端
newerp9 小时前
Golang 调度循环:Go runtime 如何永不停歇地找人干活
后端·程序员·go
明月_清风9 小时前
AI 时代,为什么架构师又开始谈"本体论"?
人工智能·后端·agent