多线程安全

117 阅读3分钟

一、线程安全的本质问题

多线程安全问题源于共享资源的竞态条件(Race Condition),即多个线程同时读写同一资源时,因执行顺序不确定导致的数据不一致或异常。
典型场景

  • 计数器递增(count++ 非原子操作)
  • 银行账户转账(存款与取款并发)
  • 缓存读写(读取时可能被其他线程修改)

二、核心解决方案

1. 互斥同步(阻塞同步)

通过锁机制确保同一时刻只有一个线程访问共享资源。

  • 语言原生锁
    • Java:synchronized 关键字、ReentrantLock
    • Python:threading.LockRLock
    • C++:std::mutexstd::lock_guard
  • 示例(Java)
    public class Counter {
      private int count = 0;
      private final Lock lock = new ReentrantLock();
      
      public void increment() {
        lock.lock();
        try {
          count++; // 临界区
        } finally {
          lock.unlock();
        }
      }
    }
    
2. 无锁编程(非阻塞同步)

通过原子操作(Atomic Operation)避免锁的开销。

  • 原子类
    • Java:AtomicIntegerAtomicReference
    • Python:threading.Lock(低层级可用 ctypes 调用原子操作)
    • C++:std::atomic
  • 示例(Java)
    public class Counter {
      private AtomicInteger count = new AtomicInteger(0);
      
      public void increment() {
        count.incrementAndGet(); // 原子操作,无需锁
      }
    }
    
3. 线程封闭(Thread Confinement)

将数据限制在单个线程内,避免共享。

  • 栈封闭:局部变量仅在方法内可见(如 Java 的 ThreadLocal)。
  • 示例(Java)
    public class ConnectionManager {
      private static final ThreadLocal<Connection> connection = new ThreadLocal<>();
      
      public static Connection getConnection() {
        if (connection.get() == null) {
          connection.set(DriverManager.getConnection(URL)); // 每个线程独立创建连接
        }
        return connection.get();
      }
    }
    
4. 不可变对象(Immutable Objects)

使用不可变类(如 Java 的 StringInteger),确保数据创建后无法修改。

  • 示例(Java)
    public final class ImmutableUser { // 类用 final 修饰
      private final String name;
      private final int age;
      
      public ImmutableUser(String name, int age) {
        this.name = name;
        this.age = age;
      }
      
      // 无 setter 方法,仅提供 getter
      public String getName() { return name; }
      public int getAge() { return age; }
    }
    

三、问题

1. 问:synchronizedReentrantLock 的区别?
    • 语法层面synchronized 是关键字,ReentrantLock 是类;
    • 灵活性ReentrantLock 支持公平锁、可中断锁、尝试锁;
    • 性能:高竞争场景下 ReentrantLock 更优(JDK 1.6 后 synchronized 优化显著);
    • 释放方式synchronized 自动释放,ReentrantLock 需手动调用 unlock()
2. 问:如何实现线程安全的单例模式?
    • 静态内部类(推荐)
      public class Singleton {
        private static class Holder {
          private static final Singleton INSTANCE = new Singleton();
        }
        private Singleton() {}
        public static Singleton getInstance() {
          return Holder.INSTANCE; // 类加载时初始化,线程安全
        }
      }
      
    • 双重检查锁定(DCL)
      public class Singleton {
        private static volatile Singleton instance; // 防止指令重排
        private Singleton() {}
        public static Singleton getInstance() {
          if (instance == null) {
            synchronized (Singleton.class) {
              if (instance == null) {
                instance = new Singleton();
              }
            }
          }
          return instance;
        }
      }
      
3. 问:CAS(Compare-and-Swap)的原理与优缺点?
    • 原理:原子操作,比较内存值与预期值,相等则更新,否则重试;
    • 优点:无锁,避免线程切换开销;
    • 缺点
      • 循环时间长可能导致 CPU 开销大;
      • 只能保证单个变量的原子性;
      • 存在 ABA 问题(需通过 AtomicStampedReference 解决)。
4. 问:什么是死锁?如何避免?
    • 死锁条件:互斥、占有并等待、不可抢占、循环等待;
    • 避免策略
      • 按顺序获取锁(如统一先锁 A 再锁 B);
      • 设置锁超时(tryLock);
      • 减少锁粒度(如使用分段锁)。

四、性能优化与最佳实践

  1. 锁优化策略

    • 减小锁粒度:如 ConcurrentHashMap 的分段锁(JDK 1.8 前);
    • 锁粗化:避免频繁加锁解锁(如循环内的同步操作);
    • 偏向锁/轻量级锁:JVM 对 synchronized 的优化(根据竞争程度动态调整)。
  2. 线程安全容器选择

    • 读多写少CopyOnWriteArrayListConcurrentHashMap
    • 阻塞队列LinkedBlockingQueueArrayBlockingQueue
    • 并发工具类CountDownLatchCyclicBarrierSemaphore
  3. 实战建议

    • 优先使用成熟框架:如 Java 的 java.util.concurrent 包;
    • 测试线程安全:使用压力测试工具(如 JMH)检测竞态条件;
    • 文档化线程安全:明确标注类/方法是否线程安全(如 @ThreadSafe)。