跳转至

Java Semaphore 深入解析

简介

在 Java 并发编程中,Semaphore(信号量)是一个强大的工具,用于控制对有限资源的访问。它可以用来限制同时访问特定资源的线程数量,从而避免资源过度使用或竞争。本文将详细介绍 Java Semaphore 的基础概念、使用方法、常见实践以及最佳实践,帮助读者深入理解并高效使用这一并发工具。

目录

  1. 基础概念
  2. 使用方法
  3. 常见实践
  4. 最佳实践
  5. 小结
  6. 参考资料

基础概念

Semaphore 是一个计数信号量,它维护了一个许可(permit)的计数。线程在访问共享资源之前,必须先从 Semaphore 中获取一个许可,如果许可数量为 0,则线程会被阻塞,直到有其他线程释放许可。当线程使用完资源后,需要将许可归还给 Semaphore,以便其他线程可以继续使用。

Semaphore 有两种模式:公平模式和非公平模式。公平模式下,线程会按照请求许可的顺序依次获取许可;非公平模式下,线程获取许可的顺序是不确定的,可能会有线程插队获取许可。

使用方法

构造函数

Semaphore 有两个主要的构造函数: - Semaphore(int permits):创建一个具有指定许可数量的非公平 Semaphore。 - Semaphore(int permits, boolean fair):创建一个具有指定许可数量的 Semaphore,并指定是否使用公平模式。

主要方法

  • acquire():获取一个许可,如果没有可用许可,则线程会被阻塞。
  • acquire(int permits):获取指定数量的许可,如果没有足够的许可,则线程会被阻塞。
  • release():释放一个许可。
  • release(int permits):释放指定数量的许可。
  • tryAcquire():尝试获取一个许可,如果有可用许可,则立即返回 true,否则返回 false
  • tryAcquire(int permits):尝试获取指定数量的许可,如果有足够的许可,则立即返回 true,否则返回 false
  • tryAcquire(long timeout, TimeUnit unit):尝试在指定的时间内获取一个许可,如果在时间内有可用许可,则返回 true,否则返回 false
  • tryAcquire(int permits, long timeout, TimeUnit unit):尝试在指定的时间内获取指定数量的许可,如果在时间内有足够的许可,则返回 true,否则返回 false

代码示例

import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    private static final int MAX_THREADS = 3;
    private static Semaphore semaphore = new Semaphore(MAX_THREADS);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Worker());
            thread.start();
        }
    }

    static class Worker implements Runnable {
        @Override
        public void run() {
            try {
                // 获取许可
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName() + " 获得许可,开始工作");
                // 模拟工作
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName() + " 工作完成,释放许可");
                // 释放许可
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在上述代码中,我们创建了一个 Semaphore,初始许可数量为 3。然后创建了 10 个线程,每个线程在执行工作前需要先获取许可,工作完成后释放许可。由于许可数量有限,最多只有 3 个线程可以同时工作。

常见实践

限制资源访问

Semaphore 最常见的用途是限制对有限资源的访问。例如,在一个数据库连接池中,我们可以使用 Semaphore 来限制同时使用的数据库连接数量,避免过多的连接导致数据库性能下降。

import java.util.concurrent.Semaphore;

public class DatabaseConnectionPool {
    private static final int MAX_CONNECTIONS = 5;
    private Semaphore semaphore = new Semaphore(MAX_CONNECTIONS);

    public void getConnection() {
        try {
            // 获取许可
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + " 获得数据库连接");
            // 模拟使用连接
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName() + " 使用完数据库连接,释放连接");
            // 释放许可
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        DatabaseConnectionPool pool = new DatabaseConnectionPool();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> pool.getConnection());
            thread.start();
        }
    }
}

实现生产者 - 消费者模型

Semaphore 也可以用于实现生产者 - 消费者模型。我们可以使用两个 Semaphore 来控制生产者和消费者的行为,一个 Semaphore 用于控制缓冲区的空闲位置,另一个 Semaphore 用于控制缓冲区中的可用数据。

import java.util.concurrent.Semaphore;

public class ProducerConsumerExample {
    private static final int BUFFER_SIZE = 5;
    private static int[] buffer = new int[BUFFER_SIZE];
    private static int in = 0;
    private static int out = 0;
    private static Semaphore empty = new Semaphore(BUFFER_SIZE);
    private static Semaphore full = new Semaphore(0);

    static class Producer implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    // 等待缓冲区有空位
                    empty.acquire();
                    // 生产数据
                    buffer[in] = (int) (Math.random() * 100);
                    System.out.println(Thread.currentThread().getName() + " 生产数据: " + buffer[in]);
                    in = (in + 1) % BUFFER_SIZE;
                    // 通知消费者有新数据
                    full.release();
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    // 等待缓冲区有数据
                    full.acquire();
                    // 消费数据
                    int data = buffer[out];
                    System.out.println(Thread.currentThread().getName() + " 消费数据: " + data);
                    out = (out + 1) % BUFFER_SIZE;
                    // 通知生产者有空闲位置
                    empty.release();
                    Thread.sleep(1500);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread producerThread = new Thread(new Producer());
        Thread consumerThread = new Thread(new Consumer());
        producerThread.start();
        consumerThread.start();
    }
}

最佳实践

  • 合理设置许可数量:根据实际需求合理设置 Semaphore 的初始许可数量,避免许可数量过多或过少导致资源浪费或性能下降。
  • 确保许可的获取和释放:在使用 Semaphore 时,一定要确保每个线程在获取许可后都能正确释放许可,避免出现死锁或资源泄漏的问题。可以使用 try-finally 块来确保许可的释放。
try {
    semaphore.acquire();
    // 执行操作
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    semaphore.release();
}
  • 选择合适的模式:根据实际情况选择公平模式或非公平模式。公平模式可以保证线程按照请求顺序获取许可,但可能会影响性能;非公平模式可以提高性能,但可能会导致某些线程长时间得不到许可。

小结

Java Semaphore 是一个强大的并发工具,用于控制对有限资源的访问。通过本文的介绍,我们了解了 Semaphore 的基础概念、使用方法、常见实践以及最佳实践。在实际开发中,合理使用 Semaphore 可以帮助我们更好地管理资源,提高程序的性能和稳定性。

参考资料

  • 《Effective Java》