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),它提供了更灵活的锁控制能力。

相关推荐
代码扳手37 分钟前
Golang 高效内网文件传输实战:零拷贝、断点续传与 Protobuf 指令解析(含完整源码)
后端·go
银河邮差43 分钟前
python实战-用海外代理IP抓LinkedIn热门岗位数据
后端·python
undsky44 分钟前
【RuoYi-Eggjs】:让 MySQL 更简单
后端·node.js
程序员西西1 小时前
Spring Boot整合MyBatis调用存储过程?
java·后端
whltaoin1 小时前
【 手撕Java源码专栏 】Spirng篇之手撕SpringBean:(包含Bean扫描、注册、实例化、获取)
java·后端·spring·bean生命周期·手撕源码
一枚ABAPer1 小时前
SAP ABAP 如何读取FTP读取CSV文件到内表
后端
苏三的开发日记1 小时前
grafana里面怎么添加Prometheus数据源监控MySQL
后端
找不到对象就NEW一个1 小时前
wechatapi,微信二次开发-连载篇(二)通讯录模块
后端·微信
Y***98512 小时前
【学术会议论文投稿】Spring Boot实战:零基础打造你的Web应用新纪元
前端·spring boot·后端