什么是Java 中的 Semaphore?

在多线程编程中,如何有效地控制多个线程对共享资源的访问是一个重要的问题。Semaphore(信号量)是 Java 中一个重要的同步工具,用于控制同时访问特定资源的线程数量。本文将深入探讨 Semaphore 的产生背景、基本使用、工作原理及其广泛的应用场景。

Semaphore 产生的背景

Semaphore 的概念由荷兰计算机科学家 Edsger W. Dijkstra 在 1960 年代提出,旨在解决多线程编程中的同步问题,即如何在多个线程或进程之间协调对共享资源的访问。

多线程同步问题

在多线程环境中,多个线程可能会同时访问共享资源,这会导致数据不一致或竞态条件。常见的同步问题包括:

  1. 互斥访问:确保同一时刻只有一个线程可以访问共享资源。
  2. 读写锁:允许多个线程同时读取资源,但写入资源时需要独占访问。
  3. 信号量控制:限制并发访问的线程数,避免资源过载。

早期解决方案

在 Semaphore 被提出之前,早期的解决方案主要依赖于锁(Lock)和条件变量(Condition Variable)。这些同步原语可以解决基本的互斥访问问题,但在控制并发线程数量方面存在一定的局限性。

信号量的引入

为了解决上述问题,Dijkstra 引入了信号量的概念。信号量是一种计数器,用于控制对共享资源的访问。信号量可以是二进制信号量(类似于互斥锁)或计数信号量(允许多个线程同时访问资源)。

信号量的基本操作包括:

  • P 操作(Proberen,试图减少):尝试减少信号量,如果信号量为 0,则线程进入等待状态。
  • V 操作(Verhogen,增加):增加信号量,并唤醒等待队列中的一个线程。

信号量的实现

在 Java 中,信号量通过 Semaphore 类实现。Semaphore 提供了获取和释放许可的方法,用于控制线程对共享资源的并发访问。

现代应用

在现代计算机系统中,Semaphore 的应用场景非常广泛,包括但不限于:

  1. 限流控制:限制同时访问某个服务或资源的线程数。
  2. 资源池管理:控制对有限资源(如数据库连接、线程池)的并发访问。
  3. 生产者-消费者模型:协调生产者和消费者线程之间的数据传递。
  4. 并发任务执行:限制同时执行的并发任务数量,避免系统过载。

Semaphore 的基本使用

假设有 N (N>5) 个线程需要访问一个共享资源,但同一时刻只能有 5 个线程能够访问这个资源。以下是一个简单的示例:

java

import java.util.concurrent.Semaphore;

public class SemaphoreExample {

    public static void main(String[] args) {
        // 初始共享资源数量
        final Semaphore semaphore = new Semaphore(5);

        // 模拟 N 个线程
        for (int i = 0; i < 10; i++) {
            new Thread(new Task(semaphore)).start();
        }
    }

    static class Task implements Runnable {
        private Semaphore semaphore;

        Task(Semaphore semaphore) {
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            try {
                // 获取1个许可
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName() + " 获取了许可");
                // 模拟任务执行
                Thread.sleep(2000);
                // 释放1个许可
                semaphore.release();
                System.out.println(Thread.currentThread().getName() + " 释放了许可");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

在这个示例中,同一时刻最多只有 5 个线程能够获取到许可并执行任务,其他线程会被阻塞,直到有线程释放许可。

Semaphore 的构造方法

Semaphore 有两个构造方法:

java

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
  • permits:表示许可的数量。
  • fair:表示是否是公平模式。

公平模式下,调用 acquire() 方法的顺序就是获取许可的顺序,遵循 FIFO;非公平模式下则是抢占式的。

Semaphore 的工作原理

Semaphore 是共享锁的一种实现,其核心在于内部的 state 值,这个值表示当前剩余的许可数量。以下是 acquire 和 release 方法的源码解析:

acquire 方法

java

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
  • acquire() 方法尝试获取一个许可(arg=1)。
  • tryAcquireShared(arg) 方法尝试减少 state 的值,如果 state >= arg 则表示获取成功,否则线程会被阻塞。

tryAcquireShared 方法

java

protected int tryAcquireShared(int arg) {
    for (;;) {
        int available = getState();
        int remaining = available - arg;
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;
    }
}
  • getState() 获取当前剩余的许可数量。
  • compareAndSetState(available, remaining) 使用 CAS 操作尝试修改 state 的值。

release 方法

java

public void release() {
    sync.releaseShared(1);
}

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
  • release() 方法尝试释放一个许可(arg=1)。
  • tryReleaseShared(arg) 方法增加 state 的值,并唤醒同步队列中的一个线程。

tryReleaseShared 方法

java

protected boolean tryReleaseShared(int arg) {
    for (;;) {
        int current = getState();
        int next = current + arg;
        if (compareAndSetState(current, next))
            return true;
    }
}
  • getState() 获取当前剩余的许可数量。
  • compareAndSetState(current, next) 使用 CAS 操作尝试修改 state 的值。

Semaphore 的应用场景

1. 限流控制

场景描述

在高并发系统中,限流控制是非常重要的一环。限流可以保护系统避免因瞬时高并发请求导致的过载,从而提高系统的稳定性。

示例代码

假设我们有一个 Web 服务,最多允许 10 个并发请求。使用 Semaphore 来实现限流控制:

java

import java.util.concurrent.Semaphore;

public class WebService {
    // 定义一个信号量,初始许可数量为10
    private final Semaphore semaphore = new Semaphore(10);

    public void handleRequest(String request) {
        try {
            // 获取一个许可,如果没有许可则会阻塞等待
            semaphore.acquire();
            System.out.println("Handling request: " + request);
            // 模拟处理请求
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放一个许可
            semaphore.release();
        }
    }

    public static void main(String[] args) {
        WebService service = new WebService();

        // 模拟20个请求
        for (int i = 0; i < 20; i++) {
            final int requestId = i;
            new Thread(() -> service.handleRequest("Request " + requestId)).start();
        }
    }
}

在这个示例中,我们定义了一个 Semaphore,初始许可数量为 10。每次处理请求时,先获取一个许可,如果没有许可则会阻塞等待。处理完请求后,释放一个许可。通过这种方式,我们可以有效地控制并发请求的数量,避免系统过载。

2. 资源池管理

场景描述

在资源有限的情况下,比如数据库连接池、线程池等,需要控制对这些有限资源的并发访问。Semaphore 可以很好地管理这些资源的分配。

示例代码

假设我们有一个数据库连接池,最多允许 5 个并发连接:

java

import java.util.concurrent.Semaphore;

public class ConnectionPool {
    private final Semaphore semaphore = new Semaphore(5);

    public void getConnection() {
        try {
            // 获取一个许可,如果没有许可则会阻塞等待
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + " 获取了数据库连接");
            // 模拟使用数据库连接
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放一个许可
            semaphore.release();
            System.out.println(Thread.currentThread().getName() + " 释放了数据库连接");
        }
    }

    public static void main(String[] args) {
        ConnectionPool pool = new ConnectionPool();

        // 模拟10个线程获取数据库连接
        for (int i = 0; i < 10; i++) {
            new Thread(pool::getConnection).start();
        }
    }
}

在这个示例中,我们定义了一个 Semaphore,初始许可数量为 5。每次获取数据库连接时,先获取一个许可,如果没有许可则会阻塞等待。使用完数据库连接后,释放一个许可。通过这种方式,我们可以有效地管理数据库连接的分配。

3. 生产者-消费者模型

场景描述

生产者-消费者模型是一种经典的多线程协作模型。生产者负责生产数据,消费者负责消费数据。使用 Semaphore 可以很好地协调生产者和消费者之间的数据传递。

示例代码

假设我们有一个生产者-消费者模型,生产者生产数据,消费者消费数据:

java

import java.util.concurrent.Semaphore;
import java.util.Queue;
import java.util.LinkedList;

public class ProducerConsumer {
    private final Semaphore items = new Semaphore(0); // 表示可消费的项目数量
    private final Semaphore spaces = new Semaphore(10); // 表示可生产的空间数量
    private final Queue<Integer> queue = new LinkedList<>();

    public void produce(int item) throws InterruptedException {
        spaces.acquire(); // 获取一个生产许可
        synchronized (this) {
            queue.add(item);
            System.out.println("Produced: " + item);
        }
        items.release(); // 释放一个消费许可
    }

    public int consume() throws InterruptedException {
        items.acquire(); // 获取一个消费许可
        int item;
        synchronized (this) {
            item = queue.poll();
            System.out.println("Consumed: " + item);
        }
        spaces.release(); // 释放一个生产许可
        return item;
    }

    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();

        // 创建生产者线程
        new Thread(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    pc.produce(i);
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        // 创建消费者线程
        new Thread(() -> {
            try {
                for (int i = 0; i < 20; i++) {
                    pc.consume();
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

在这个示例中,我们定义了两个 Semaphore

  • items:表示可消费的项目数量,初始值为 0。
  • spaces:表示可生产的空间数量,初始值为 10。

生产者线程在生产数据时,先获取一个生产许可,生产完数据后,释放一个消费许可。消费者线程在消费数据时,先获取一个消费许可,消费完数据后,释放一个生产许可。通过这种方式,我们可以有效地协调生产者和消费者之间的数据传递。

4. 并发任务执行

场景描述

在一些任务调度系统中,可能需要限制同时执行的并发任务数量,以避免系统过载。Semaphore 可以很好地控制并发任务的执行数量。

示例代码

假设我们有一个任务调度系统,最多允许 3 个并发任务:

java

import java.util.concurrent.Semaphore;

public class TaskScheduler {
    private final Semaphore semaphore = new Semaphore(3);

    public void executeTask(String task) {
        try {
            semaphore.acquire(); // 获取一个许可,如果没有许可则会阻塞等待
            System.out.println("Executing task: " + task);
            // 模拟执行任务
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // 释放一个许可
        }
    }

    public static void main(String[] args) {
        TaskScheduler scheduler = new TaskScheduler();

        // 模拟10个任务
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            new Thread(() -> scheduler.executeTask("Task " + taskId)).start();
        }
    }
}

在这个示例中,我们定义了一个 Semaphore,初始许可数量为 3。每次执行任务时,先获取一个许可,如果没有许可则会阻塞等待。任务执行完毕后,释放一个许可。通过这种方式,我们可以有效地控制并发任务的执行数量。

结论

通过本文的讲解,我们深入了解了 Semaphore 的产生背景、基本使用、构造方法、工作原理以及其典型的应用场景。Semaphore 是一个非常强大的工具,可以很好地控制资源的并发访问,适用于多种实际应用场景,如限流控制、资源池管理、生产者-消费者模型和并发任务执行等。在实际开发中,合理使用 Semaphore 可以极大地提高系统的稳定性和并发处理能力,是多线程编程中不可或缺的工具之一。

  • 19
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值