Java并发系列之八Semaphore

简介: Java并发系列之八Semaphore

前面我已经讲解过了CountDownLatch和CyclicBarrier。本篇我们来讲解下Semaphore。


Semaphore是指信号量,在计算机的世界里信号量可以使用在数据竞争的场景中。在生活中交通信号灯可以比作现实世界中的Semaphore。Semaphore的作用就是允许或者禁止。比如说红灯禁止通行,绿灯允许通行。计算机世界里的Semaphore会持有多张许可证,举个例子有10张许可证,假设有20个线程同时请求信用量的许可证,那么只能是其中十个线程能够拿到许可证,执行代码。另外10个线程只能等拿到许可证的线程释放许可证。举个生活中的例子。比如在银行办理业务,假设大厅有3个窗口。办理业务的群众有10个。那么每次只能有3个群众能获得叫号的资格(对应计算机拿到许可证),如果有一个群众办理完业务了,那么窗口才能空余出来(对应计算机线程释放许可证),等待的群众才能获得叫号的资格。


我们用代码来模拟下该场景。假设只有3个窗口,有10个群众来办理业务,每个业务办理1~5秒钟不等

public class Bank {
    int[] windows = new int[3];//3个办事窗口
    final boolean[] busy = new boolean[3];//办事窗口是否busy
    String[] persons = new String[10];
    Semaphore semaphore = new Semaphore(3);//最多只有3张许可证
    {
        for (int i = 0; i < 10; i++) {
            persons[i] = "person " + i;
        }
    }
    ExecutorService pool = Executors.newCachedThreadPool();
    public void deal() {
        for (final String person : persons) {
            pool.execute(new Runnable() {
                public void run() {
                    try {
                        semaphore.acquire();
                        int index = 0;
                        synchronized (busy) {
                            for (int i = 0; i < 3; i++) {
                                if (!busy[i]) {//如果该窗口空闲
                                    index = i;
                                    busy[i] = true;
                                    System.out.println("请 " + person + " 到 " + index + " 号窗口办理业务");
                                    break;
                                }
                            }
                        }
                        Random random = new Random();
                        int second = 1 + random.nextInt(5);
                        TimeUnit.SECONDS.sleep(second);
                        System.out.println(person + "在"+index+" 窗口办理完成 费时" + second + "秒");
                        synchronized (busy) {
                            busy[index] = false;
                        }
                    } catch (
                            InterruptedException e
                            )
                    {
                        e.printStackTrace();
                    } finally
                    {
                        semaphore.release();
                    }
                }
            });
        }
    }
    public static void main(String[] args) {
        Bank bank = new Bank();
        bank.deal();
    }
}

输出结果如下

请 person 0 到 0 号窗口办理业务
请 person 1 到 1 号窗口办理业务
请 person 2 到 2 号窗口办理业务
person 1在1 窗口办理完成 费时1秒
请 person 3 到 1 号窗口办理业务
person 0在0 窗口办理完成 费时3秒
请 person 4 到 0 号窗口办理业务
person 2在2 窗口办理完成 费时3秒
请 person 5 到 2 号窗口办理业务
person 3在1 窗口办理完成 费时2秒
请 person 6 到 1 号窗口办理业务
person 4在0 窗口办理完成 费时3秒
请 person 7 到 0 号窗口办理业务
person 5在2 窗口办理完成 费时4秒
请 person 8 到 2 号窗口办理业务
person 6在1 窗口办理完成 费时5秒
请 person 9 到 1 号窗口办理业务
person 8在2 窗口办理完成 费时2秒
person 7在0 窗口办理完成 费时4秒
person 9在1 窗口办理完成 费时3秒

从打印结果我们可以看出,每次最多允许3个人办理业务,每当窗口空闲出来就会叫号。

接下来我们看看Semaphore源码

public class Semaphore implements java.io.Serializable {
    private static final long serialVersionUID = -3222578661600680210L;
    /** All mechanics via AbstractQueuedSynchronizer subclass */
    private final Sync sync;
    abstract static class Sync extends AbstractQueuedSynchronizer {
    }
}

Semaphore持有一个Sync对象,该对象是一个AQS。之前的并发系列我们讲过AQS。它是同步的核心。而Lock类也是持有Sync对象。我们可以把Semaphore看成是一个Lock。区别是Semaphore没有lock和unlock方法,取而代之的是acquire 和release方法


public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
}
public boolean tryAcquire(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    return sync.nonfairTryAcquireShared(permits) >= 0;
}
final int nonfairTryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }

看到share字样,首先这把锁是共享锁。每次获取到了锁,许可证数量-1,如果许可证数量小于0,则获取锁失败,该线程等待

public void release() {
        sync.releaseShared(1);
}
protected final boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                if (next < current) // overflow
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next))
                    return true;
            }
        }

release方法许可证+1,同时通知等待线程,尝试获取许可证


总结下CountDownLatch CyclicBarrier Semaphore的区别


CountDownLatch 适合做汇总相关的工作,比如开启多个线程爬取数据,在所有线程完成任务后,将数据做汇总。


CyclicBarrier适合于竞赛相关的场景,让多个线程在同一时间点执行任务。


Semaphore适用于资源有限的情况下的流量控制。

相关文章
|
2月前
|
Java
JAVA并发编程系列(7)Semaphore信号量剖析
腾讯T2面试,要求在3分钟内用不超过20行代码模拟地铁安检进站过程。题目设定10个安检口,100人排队,每人安检需5秒。实际中,这种题目主要考察并发编程能力,特别是多个线程如何共享有限资源。今天我们使用信号量(Semaphore)实现,限制同时进站的人数,并通过信号量控制排队和进站流程。并详细剖析信号量核心原理和源码。
|
3月前
|
安全 Java 编译器
揭秘JAVA深渊:那些让你头大的最晦涩知识点,从泛型迷思到并发陷阱,你敢挑战吗?
【8月更文挑战第22天】Java中的难点常隐藏在其高级特性中,如泛型与类型擦除、并发编程中的内存可见性及指令重排,以及反射与动态代理等。这些特性虽强大却也晦涩,要求开发者深入理解JVM运作机制及计算机底层细节。例如,泛型在编译时检查类型以增强安全性,但在运行时因类型擦除而丢失类型信息,可能导致类型安全问题。并发编程中,内存可见性和指令重排对同步机制提出更高要求,不当处理会导致数据不一致。反射与动态代理虽提供运行时行为定制能力,但也增加了复杂度和性能开销。掌握这些知识需深厚的技术底蕴和实践经验。
74 2
|
3月前
|
安全 Java 调度
解锁Java并发编程高阶技能:深入剖析无锁CAS机制、揭秘魔法类Unsafe、精通原子包Atomic,打造高效并发应用
【8月更文挑战第4天】在Java并发编程中,无锁编程以高性能和低延迟应对高并发挑战。核心在于无锁CAS(Compare-And-Swap)机制,它基于硬件支持,确保原子性更新;Unsafe类提供底层内存操作,实现CAS;原子包java.util.concurrent.atomic封装了CAS操作,简化并发编程。通过`AtomicInteger`示例,展现了线程安全的自增操作,突显了这些技术在构建高效并发程序中的关键作用。
67 1
|
4天前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
15 2
|
21天前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
25 1
|
2月前
|
Java API 容器
JAVA并发编程系列(10)Condition条件队列-并发协作者
本文通过一线大厂面试真题,模拟消费者-生产者的场景,通过简洁的代码演示,帮助读者快速理解并复用。文章还详细解释了Condition与Object.wait()、notify()的区别,并探讨了Condition的核心原理及其实现机制。
|
3月前
|
存储 Java
Java 中 ConcurrentHashMap 的并发级别
【8月更文挑战第22天】
50 5
|
3月前
|
存储 算法 Java
Java 中的同步集合和并发集合
【8月更文挑战第22天】
39 5
|
3月前
|
缓存 Java 调度
【Java 并发秘籍】线程池大作战:揭秘 JDK 中的线程池家族!
【8月更文挑战第24天】Java的并发库提供多种线程池以应对不同的多线程编程需求。本文通过实例介绍了四种主要线程池:固定大小线程池、可缓存线程池、单一线程线程池及定时任务线程池。固定大小线程池通过预设线程数管理任务队列;可缓存线程池能根据需要动态调整线程数量;单一线程线程池确保任务顺序执行;定时任务线程池支持周期性或延时任务调度。了解并正确选用这些线程池有助于提高程序效率和资源利用率。
51 2
|
3月前
|
Java 开发者
【编程高手必备】Java多线程编程实战揭秘:解锁高效并发的秘密武器!
【8月更文挑战第22天】Java多线程编程是提升软件性能的关键技术,可通过继承`Thread`类或实现`Runnable`接口创建线程。为确保数据一致性,可采用`synchronized`关键字或`ReentrantLock`进行线程同步。此外,利用`wait()`和`notify()`方法实现线程间通信。预防死锁策略包括避免嵌套锁定、固定锁顺序及设置获取锁的超时。掌握这些技巧能有效增强程序的并发处理能力。
26 2