1. 概述

本文将探讨在Java中实现互斥锁(mutex)的多种方式。互斥锁是并发编程中的基础概念,掌握不同实现方式能帮助我们在实际开发中灵活应对各种场景。

2. 互斥锁

在多线程应用中,当两个或多个线程同时访问共享资源时,会导致不可预期的行为。典型的共享资源包括:

  • 数据结构
  • I/O设备
  • 文件
  • 网络连接

这种场景被称为竞态条件(race condition),而访问共享资源的代码段则称为临界区(critical section)。要避免竞态条件,必须同步对临界区的访问

互斥锁(mutex)是最简单的同步器——它确保同一时间只有一个线程能执行临界区代码。线程执行流程如下:

  1. 获取互斥锁
  2. 执行临界区代码
  3. 释放互斥锁

在此期间,其他所有线程都会阻塞,直到互斥锁被释放。当线程退出临界区后,其他线程才能进入。

3. 为什么需要互斥锁?

先看一个SequenceGenerator类的例子,它通过递增currentValue生成序列:

public class SequenceGenerator {
    private int currentValue = 0;
    
    public int getNextSequence() {
        currentValue = currentValue + 1;
        return currentValue;
    }
}

现在创建测试用例,模拟多线程并发访问:

@Test
public void testRaceCondition() throws InterruptedException {
    int threadCount = 1000;
    ExecutorService executor = Executors.newFixedThreadPool(threadCount);
    SequenceGenerator generator = new SequenceGenerator();
    Set<Integer> uniqueSequences = Collections.synchronizedSet(new HashSet<>());

    for (int i = 0; i < threadCount; i++) {
        executor.execute(() -> {
            uniqueSequences.add(generator.getNextSequence());
        });
    }

    executor.shutdown();
    executor.awaitTermination(1, TimeUnit.MINUTES);

    assertEquals(threadCount, uniqueSequences.size());
}

执行测试会发现它经常失败,错误类似:

Expected :1000
Actual   :999

uniqueSequences的大小本应等于方法调用次数,但由于竞态条件导致结果不一致。为避免这种问题,必须确保同一时间只有一个线程执行getNextSequence方法。这时互斥锁就派上用场了。

接下来我们将用不同方式为SequenceGenerator实现互斥锁。

4. 使用synchronized关键字

synchronized关键字是Java中最简单的互斥实现方式。每个Java对象都有一个内置锁(intrinsic lock),**synchronized方法和代码块都利用这个锁**来保证临界区的线程安全。

当线程调用synchronized方法或进入synchronized块时,会自动获取锁。方法/块执行结束或抛出异常时锁自动释放。

同步方法版

只需给getNextSequence添加synchronized关键字:

public synchronized int getNextSequence() {
    currentValue = currentValue + 1;
    return currentValue;
}

同步代码块版

同步代码块更灵活,能精确控制临界区范围和锁对象:

private final Object mutex = new Object();

public int getNextSequence() {
    synchronized (mutex) {
        currentValue = currentValue + 1;
        return currentValue;
    }
}

5. 使用ReentrantLock

Java 1.5引入的ReentrantLock类比synchronized提供更强的灵活性和控制力。使用示例:

import java.util.concurrent.locks.ReentrantLock;

public class SequenceGeneratorWithLock extends SequenceGenerator {
    private final ReentrantLock lock = new ReentrantLock();

    @Override
    public int getNextSequence() {
        lock.lock();
        try {
            currentValue = currentValue + 1;
            return currentValue;
        } finally {
            lock.unlock();
        }
    }
}

关键点:

  • 必须手动调用lock()unlock()
  • 最佳实践是将unlock()放在finally块中
  • 支持公平锁、超时获取等高级特性

6. 使用Semaphore

Semaphore(信号量)同样在Java 1.5引入。与互斥锁(只允许一个线程访问临界区)不同,信号量允许固定数量的线程访问临界区。因此将信号量的许可数设为1就能实现互斥锁

实现示例:

import java.util.concurrent.Semaphore;

public class SequenceGeneratorWithSemaphore extends SequenceGenerator {
    private final Semaphore semaphore = new Semaphore(1);

    @Override
    public int getNextSequence() {
        try {
            semaphore.acquire();
            currentValue = currentValue + 1;
            return currentValue;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Interrupted", e);
        } finally {
            semaphore.release();
        }
    }
}

注意:

  • acquire()可能抛出InterruptedException,需妥善处理
  • 必须确保release()finally中执行
  • 适合需要控制并发访问数量的场景

7. 使用Guava的Monitor

前几种都是Java原生方案,Google Guava库提供的Monitor类是ReentrantLock的更优替代品。根据官方文档使用Monitor的代码比ReentrantLock更易读且不易出错

首先添加Maven依赖:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-jre</version>
</dependency>

实现示例:

import com.google.common.util.concurrent.Monitor;

public class SequenceGeneratorWithMonitor extends SequenceGenerator {
    private final Monitor monitor = new Monitor();

    @Override
    public int getNextSequence() {
        monitor.enter();
        try {
            currentValue = currentValue + 1;
            return currentValue;
        } finally {
            monitor.leave();
        }
    }
}

优势:

  • 语法更简洁(无需显式锁操作)
  • 内置超时和中断检测
  • 支持条件变量(Condition)
  • 推荐在复杂同步场景中使用

8. 结论

本文深入探讨了互斥锁的概念,并展示了在Java中的四种实现方式:

  1. synchronized关键字:最简单直接,适合基础场景
  2. **ReentrantLock**:功能强大,需手动管理锁
  3. **Semaphore**:灵活控制并发访问数
  4. **Guava的Monitor**:易读性强,推荐用于复杂逻辑

每种方案都有适用场景,实际开发中需根据需求权衡:

  • 简单场景用synchronized足矣
  • 需要公平锁/超时机制时选ReentrantLock
  • 需要控制并发数量时用Semaphore
  • 复杂同步逻辑优先考虑Monitor

完整示例代码见 GitHub仓库


原始标题:Using a Mutex Object in Java | Baeldung

« 上一篇: Java Weekly, 第296期
» 下一篇: Java 中的 XOR 操作符