Semaphore信号量

381 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情

一、Semaphore介绍

Semaphore,俗称信号量,它是操作系统中PV操作的原语在java的实现,它也是基于AbstractQueuedSynchronizer实现的。

Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取信号量实现。大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量。

image.png

PV操作是操作系统一种实现进程互斥与同步的有效方法。PV操作与信号量(S)的处理相关,P表示通过的意思,V表示释放的意思。用PV操作来管理共享资源时,首先要确保PV操作自身执行的正确性。

P操作的主要动作是:

①S减1;

②若S减1后仍大于或等于0,则进程继续执行;

③若S减1后小于0,则该进程被阻塞后放入等待该信号量的等待队列中,然后转进程调度。

V操作的主要动作是:

①S加1; 

②若相加后结果大于0,则进程继续执行;

③若相加后结果小于或等于0,则从该信号的等待队列中释放一个等待进程,然后再返回原进程继续执行或转进程调度。

二、Semaphore 常用方法

2-1、构造器

image.png

  • permits 表示许可证的数量(资源数)
  • fair 表示公平性,如果这个设为 true 的话,下次执行的线程会是等待最久的线程

2-2、常用方法

public void acquire() throws InterruptedException
public boolean tryAcquire()
public void release()
public int availablePermits()
public final int getQueueLength()
public final boolean hasQueuedThreads()
protected void reducePermits(int reduction)

各方法作用如下,标红为常用方法

  • acquire() 表示阻塞并获取许可
  • tryAcquire() 方法在没有许可的情况下会立即返回 false,要获取许可的线程不会阻塞
  • release() 表示释放许可
  • int availablePermits():返回此信号量中当前可用的许可证数。
  • int getQueueLength():返回正在等待获取许可证的线程数。
  • boolean hasQueuedThreads():是否有线程正在等待获取许可证。
  • void reducePermit(int reduction):减少 reduction 个许可证
  • Collection getQueuedThreads():返回所有等待获取许可证的线程集合

三、应用场景

可以用于做流量控制,特别是公用资源有限的应用场景

3-1、模拟买票场景

在互联网还没席卷的时候,我们购买火车票都需要去火车站排队购买票,买票的人很多,可是售票窗口是有限的,下面用Semaphore来模拟实现一个买票的场景

如下使用Semaphore来模拟售票窗口,设置的资源数为3,使用for循环来模拟排队购买的人员,当for执行,进入run方法,使用windows.acquire()加锁的时候,实际上就是占用了一个资源,第二第三再次进入的时候,也同时占用一个资源,当第四个再进入的时候,因为已经没有资源,就会加入的排队的状态。当前面三个其中一个人释放锁的时候,后面排队的才会加锁成功。

需要注意,使用的时候一定要在finally中使用release解锁,防止程序异常无法释放锁

public class SemaphoreTest {
    public static void main(String[] args) {
        // 声明3个窗口  state:  资源数
        Semaphore windows = new Semaphore(3);

        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        // 占用窗口    加锁
                        windows.acquire();
                        System.out.println(Thread.currentThread().getName() + ": 开始买票");
                        //模拟买票流程
                        Thread.sleep(5000);
                        System.out.println(System.currentTimeMillis());
                        System.out.println(Thread.currentThread().getName() + ": 购票成功");

                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        // 释放窗口
                        windows.release();
                    }
                }
            }).start();

        }
    }
}

执行结果如下,可以看到线程1/2/4抢占到了锁,后面的线程就会进入排队等待的状态。

image.png

3-2、限流

如下示例,定义了一个5个请求的限流器,然后创建一个线程池,在main方法中使用for实现了一个无线循环,利用线程池执行方法,由于方法中使用了限流器,这样每次就只允许5个线程执行,其他线程进行排队,当前5个线程执行完成释放锁之后,后面的线程再抢占锁,这样虽然需要等待的线程有很多,但是通过使用Semaphone实现了一个限流器,就每次只允许5个任务同时执行

public class SemaphoneTest2 {
    /**
     * 实现一个同时只能处理5个请求的限流器
     */
    private static Semaphore semaphore = new Semaphore(5);

    /**
     * 定义一个线程池
     */
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor
            (10, 50, 60,
                    TimeUnit.SECONDS, new LinkedBlockingDeque<>(200));

    /**
     * 模拟执行方法
     */
    public static void exec() {
        try {
            //占用1个资源
            semaphore.acquire(1);
            //TODO  模拟业务执行
            System.out.println("执行exec方法");
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放一个资源
            semaphore.release(1);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        {
            for (; ; ) {
                Thread.sleep(100);
                // 模拟请求以10个/s的速度
                executor.execute(() -> exec());
            }
        }
    }
}

执行结果:

image.png