详细说说线程的同步和互斥

面试资料大全|各种技术资料-2000G

Java 中线程的同步(Synchronization)互斥(Mutual Exclusion) 这两个核心并发概念。它们是确保多线程程序正确性、避免竞态条件(Race Condition)和维持数据一致性的基石。

核心目标: 协调多个线程对共享资源(变量、对象、文件、数据库连接等)的访问,防止它们同时进行可能导致数据损坏或逻辑错误的操作。


1. 互斥(Mutual Exclusion)

  • 是什么? 确保同一时刻,只有一个线程能够访问特定的共享资源或执行特定的代码段(称为临界区 - Critical Section)。
  • 为什么需要? 防止多个线程同时修改共享数据导致数据不一致。例如,两个线程同时对一个银行账户进行存款操作,如果没有互斥,最终的余额可能只反映其中一个操作。
  • 核心思想: “一次只允许一个”。
  • Java 实现机制:
    • synchronized 关键字: 这是 Java 中最基本、最常用的互斥机制。
      • 同步方法: 在方法声明前加 synchronized。锁对象是当前实例对象this)或类对象Class 对象,对于 static 方法)。
        public synchronized void deposit(int amount) {
            balance += amount; // 临界区
        }
        
      • 同步代码块: 指定一个对象作为锁(lockObject),只有持有该锁的线程才能执行代码块内的语句。
        public void withdraw(int amount) {
            synchronized(lockObject) { // 显式指定锁对象
                if (balance >= amount) {
                    balance -= amount; // 临界区
                }
            }
        }
        
      • 原理: 当一个线程进入 synchronized 方法或代码块时,它会尝试获取与指定对象(隐式或显式)关联的内置锁(Intrinsic Lock / Monitor Lock)。如果锁被其他线程持有,当前线程会被阻塞(BLOCKED 状态),直到锁被释放。
    • java.util.concurrent.locks.Lock 接口 (e.g., ReentrantLock): 提供了比 synchronized 更灵活、功能更强大的互斥控制。
      private final Lock lock = new ReentrantLock();
      public void transfer(Account to, int amount) {
          lock.lock(); // 手动获取锁
          try {
              if (this.balance >= amount) {
                  this.balance -= amount;
                  to.balance += amount; // 临界区
              }
          } finally {
              lock.unlock(); // 必须在 finally 块中释放锁!
          }
      }
      
      • 优势:
        • 可中断的锁获取: lockInterruptibly() 允许在等待锁时响应中断。
        • 尝试获取锁: tryLock()tryLock(long time, TimeUnit unit) 允许线程尝试获取锁或在指定时间内获取锁,避免无限期阻塞。
        • 公平锁: ReentrantLock(true) 可以创建公平锁(按等待顺序获取锁),减少线程饥饿(但可能降低吞吐量)。
        • 绑定多个条件: newCondition() 可以创建多个 Condition 对象,实现更精细的线程等待/通知机制。

2. 同步(Synchronization)

  • 是什么? 比互斥更广义的概念。它不仅包括互斥(确保对临界区的互斥访问),还包括线程间的协作,即一个线程的执行需要等待另一个线程完成特定操作后才能继续。它定义了线程之间操作的顺序约束可见性保证
  • 为什么需要? 解决生产者-消费者读写锁等场景,确保线程在正确的时机执行。例如,消费者线程需要等待生产者线程生产出数据后才能消费;读者线程可以并发读,但写者线程写时需要独占。
  • 核心思想: “你做完我再做” 或 “条件满足了我再做”。
  • Java 实现机制(协作部分):
    • wait(), notify(), notifyAll() (与 synchronized 配合使用): 这是 Java 内置的线程协作机制,必须在持有对象锁的同步块或同步方法内调用
      • wait(): 使当前线程释放它持有的对象锁,并进入该对象的等待集(Wait Set),状态变为 WAITINGTIMED_WAITING(如果使用 wait(long timeout))。线程会一直等待,直到被其他线程通过 notify()/notifyAll() 唤醒,或者超时,或者被中断。被唤醒后,线程需要重新竞争获取对象锁才能从 wait() 调用处继续执行。
      • notify(): 随机唤醒在该对象上等待的一个线程(从等待集中移出,状态变为 BLOCKED,等待获取锁)。
      • notifyAll(): 唤醒在该对象上等待的所有线程(都移出等待集,状态变为 BLOCKED,竞争获取锁)。
      • 经典模式 (生产者-消费者):
        public class Buffer {
            private Queue<Integer> queue = new LinkedList<>();
            private int capacity;
        
            public Buffer(int capacity) { this.capacity = capacity; }
        
            public synchronized void produce(int item) throws InterruptedException {
                while (queue.size() == capacity) { // 必须用 while 循环检查条件!
                    wait(); // 缓冲区满,生产者等待
                }
                queue.offer(item);
                notifyAll(); // 生产了一个,通知可能等待的消费者
            }
        
            public synchronized int consume() throws InterruptedException {
                while (queue.isEmpty()) { // 必须用 while 循环检查条件!
                    wait(); // 缓冲区空,消费者等待
                }
                int item = queue.poll();
                notifyAll(); // 消费了一个,通知可能等待的生产者
                return item;
            }
        }
        
      • 重要: 检查条件(如 queue.size() == capacity必须while 循环中进行,而不是 if 语句。因为被唤醒时,条件可能再次不满足(例如,多个消费者被唤醒但只有一个能消费,其他消费者醒来时缓冲区可能又空了)。这就是所谓的 “Mesa 风格” 并发(条件成立时线程不一定立即执行)和 “条件谓词”(Condition Predicate) 的检查。
    • java.util.concurrent.locks.Condition 接口 (与 Lock 配合使用): 提供了比 Objectwait/notify 更清晰、更灵活的线程协作机制。一个 Lock 可以创建多个 Condition 实例,用于不同的等待条件。
      private final Lock lock = new ReentrantLock();
      private final Condition notFull = lock.newCondition(); // 条件:缓冲区不满
      private final Condition notEmpty = lock.newCondition(); // 条件:缓冲区不空
      
      public void produce(int item) throws InterruptedException {
          lock.lock();
          try {
              while (queue.size() == capacity) {
                  notFull.await(); // 在 notFull 条件上等待
              }
              queue.offer(item);
              notEmpty.signal(); // 唤醒一个在 notEmpty 上等待的消费者
          } finally {
              lock.unlock();
          }
      }
      
      public int consume() throws InterruptedException {
          lock.lock();
          try {
              while (queue.isEmpty()) {
                  notEmpty.await(); // 在 notEmpty 条件上等待
              }
              int item = queue.poll();
              notFull.signal(); // 唤醒一个在 notFull 上等待的生产者
              return item;
          } finally {
              lock.unlock();
          }
      }
      
      • 优势:
        • 更清晰的语义: 为不同的等待条件创建独立的 Condition 对象(如 notFull, notEmpty),代码可读性更好。
        • 更精细的控制: signal() 只唤醒在该特定 Condition 上等待的线程,避免了使用 notifyAll() 可能导致的"虚假唤醒"(唤醒不需要的线程)带来的性能开销。
        • 同样支持可中断等待超时等待等。
    • 高级同步工具类 (java.util.concurrent 包): 提供了更高级的抽象,简化常见同步模式的实现。
      • Semaphore (信号量): 控制同时访问特定资源的线程数量(许可证)。
      • CountDownLatch (倒计时门闩): 允许一个或多个线程等待其他线程完成一组操作。
      • CyclicBarrier (循环屏障): 让一组线程相互等待,直到所有线程都到达某个屏障点,然后一起继续执行。
      • Phaser: 更灵活、更强大的可重用屏障。
      • Exchanger (交换器): 两个线程在同步点交换数据。
      • LockSupport: 提供更底层的线程阻塞和唤醒原语(park(), unpark())。

3. 同步与互斥的关系

  1. 互斥是同步的基础和子集: 互斥保证了临界区的排他性访问,这是实现更复杂同步(如线程协作)的前提。wait(), notify() 等协作机制必须在互斥(持有锁)的前提下使用。
  2. 同步包含互斥: 同步是一个更广泛的概念。它不仅要求互斥访问临界区,还要求线程按照某种逻辑顺序或条件来执行。互斥主要解决“同时访问”的问题,同步解决“何时访问”和“如何协作”的问题。
  3. 协作需要互斥: 在生产者-消费者例子中,对共享队列 queue 的操作(offer, poll, 检查 size)需要互斥(synchronizedLock)。wait()notify() 本身也需要在持有锁的上下文中调用,因为它们操作的对象内部状态(等待集)也是共享的、需要互斥访问的。
  4. 互斥不一定需要显式协作: 有时,互斥(例如简单的计数器递增)本身足以保证正确性,不需要额外的 wait/notify 机制。

总结图:

                    Synchronization (同步)
                      /              \
                     /                \
                    /                  \
    Mutual Exclusion (互斥)       Coordination (协作)
    (e.g., synchronized, Lock)     (e.g., wait/notify, Condition, Latch, Barrier)
      确保一次只有一个线程            确保线程按条件/顺序执行
      进入临界区                     (通常建立在互斥基础上)

4. 关键挑战与最佳实践

  • 死锁: 两个或多个线程相互等待对方持有的锁,导致所有线程永久阻塞。预防策略:
    • 固定锁的获取顺序。
    • 使用 tryLock() 超时机制。
    • 减少锁的持有范围和时间。
    • 避免嵌套锁。
  • 活锁: 线程不断重试失败的操作(如谦让式地重试获取锁),导致无法进展。需要改变重试策略(如引入随机退避)。
  • 饥饿: 某个线程长期无法获得所需资源(如锁)。使用公平锁或调整线程优先级可以缓解(但需谨慎)。
  • 性能开销: 锁的获取、释放、线程上下文切换都有开销。优化策略:
    • 减小临界区范围: 只在绝对必要时加锁。
    • 降低锁粒度: 使用多个细粒度锁代替一个大锁。
    • 无锁编程: 使用 java.util.concurrent.atomic 包中的原子类(如 AtomicInteger),或 ConcurrentHashMap, CopyOnWriteArrayList 等并发集合。
    • 读写锁 (ReentrantReadWriteLock / StampedLock): 允许多个读线程并发,写线程独占。
  • 可见性: 一个线程修改了共享变量,其他线程能立即看到最新值。synchronized, Lock, volatile 关键字以及 Atomic 类都提供了可见性保证(通过内存屏障)。
  • volatile 关键字:
    • 保证变量的可见性(读总是看到最新写入)。
    • 禁止指令重排序
    • 不保证原子性(例如 volatile int count; count++ 不是原子的!)。适用于状态标志等简单场景。
  • 优先使用 java.util.concurrent 这个包提供了经过充分测试、高性能的并发工具类,通常比自己用 synchronized/wait/notify 从头实现更安全、更高效。

你想要的技术资料我全都有:https://pan.q删掉汉子uark.cn/s/aa7f2473c65b

你可能感兴趣的:(多线程,面试资料)