Semaphore 使用&核心原理 图解


JUC 高并发工具类 3文章:

1 Semaphore是什么?

Semaphore是计数信号量。Semaphore管理一系列许可。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可这个对象,Semaphore只是维持了一个可获得许可证的数量。

比如:停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。

比如:在学生时代都去餐厅打过饭,假如有3个窗口可以打饭,同一时刻也只能有3名同学打饭。第四个人来了之后就必须在外面等着,只要有打饭的同学好了,就可以去相应的窗口了 。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fhj3NWT2-1604139314540)(https://pics5.baidu.com/feed/e1fe9925bc315c6004ec19b8f2c86e16485477ee.jpeg?token=4e4314eeaf79a48cc2cc9003bdf22d27&s=8D26F4170993F3E902C5DD4C03007073)]

2 怎么使用 Semaphore

2.1 构造方法

//创建具有给定的许可数和非公平的公平设置的 Semaphore。  
Semaphore(int permits)   

//创建具有给定的许可数和给定的公平设置的 Semaphore。  
Semaphore(int permits, boolean fair)   
       

2.2 重要方法

在上面我们使用最基本的acquire方法和release方法就可以实现Semaphore最常见的功能,不过其他方法还是需要我们去了解一下的。



1、acquire(int permits)

从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。就好比是一个学生占两个窗口。这同时也对应了相应的release方法。

2、release(int permits)

释放给定数目的许可,将其返回到信号量。这个是对应于上面的方法,一个学生占几个窗口完事之后还要释放多少

3、availablePermits()

返回此信号量中当前可用的许可数。也就是返回当前还有多少个窗口可用。

4、reducePermits(int reduction)

根据指定的缩减量减小可用许可的数目。

5、hasQueuedThreads()

查询是否有线程正在等待获取资源。

6、getQueueLength()

返回正在等待获取的线程的估计数目。该值仅是估计的数字。

7、tryAcquire(int permits, long timeout, TimeUnit unit)

如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。

8、acquireUninterruptibly(int permits)

从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。

3 使用案例

这个案例使用的就是我们之前的小例子,也就是去餐厅打饭的案例。

我们先看Test类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FS6locSZ-1604139314543)(https://pics3.baidu.com/feed/472309f790529822787d1d77a9b3dece0b46d4b2.jpeg?token=4e2d7f2dfdf9be3f1a3f16fda56b967b&s=BA81A14C47F09868165999130000E081)]

在这个代码中我们看到,主要是new了一个Semaphore,然后赋给每一位同学Student,接下来我们就来好好看看Student线程是如何实现的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZiBxQx9P-1604139314544)(https://pics4.baidu.com/feed/2f738bd4b31c870154037c6659063b2a0608ff7d.jpeg?token=a01971796d2bdc9e96b2e1d0563fb96a&s=3281B14CD2B4966F56D9B50F000070C1)]

在这个Student类中我们最主要看run方法的实现,首先我们通过acquire获取了当前窗口的许可,然后休眠3秒代表打饭,最后在finally使用release方法释放这个窗口许可证。代码很简单,原理很清楚,我们测试一波:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5jnNutK9-1604139314547)(https://pics0.baidu.com/feed/58ee3d6d55fbb2fbfe163bf6313385a14723dc90.jpeg?token=4dc06098df214c105b05dfe933528c9e&s=479C2D2A114F554F1C613CDA000050B4)]

这个结果你也看到了,基本上同一时刻只能有三个学生在窗口旁边。

在这里你可能有一个疑问了,Semaphore好像和synchronized关键字没什么区别,都可以实现同步,如果是这样那说明我们还没有真正理解jdk的注释,他只是限制了访问某些资源的线程数,其实并没有实现同步,我们可以看一下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O2oWAgxb-1604139314549)(https://pics7.baidu.com/feed/c995d143ad4bd113e91ab29724d6010a4afb053a.jpeg?token=195052e5fe3b9252fcc25d2ac11fe879&s=3A81A14C52F4AC69045D180B0000F0C0)]

现在我们在获取许可前增加了一条输出语句,也就是能打印出有哪个线程进入了,再去测试一波

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mdrSNy4s-1604139314551)(https://pics4.baidu.com/feed/962bd40735fae6cd2ebff58a71caaa2143a70f54.jpeg?token=50bb6e1dd1815e4e7f1453196faf0cc6&s=F79CAD2B0FC24C4102CC75DE00008034)]

结果很清晰,所以对于Semaphore来说,我们需要记住的其实是资源的互斥而不是资源的同步,在同一时刻是无法保证同步的,但是却可以保证资源的互斥。

4 Semaphore使用场景

用于那些资源有明确访问数量限制的场景,常用于限流 。

  • 比如:数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制数量后,后面的线程只能排队等前面的线程释放了数据库连接才能获得数据库连接。

  • 比如:停车场场景,车位数量有限,同时只能容纳多少台车,车位满了之后只有等里面的车离开停车场外面的车才可以进入。

5 Semaphore原理

(1)、Semaphore初始化。

Semaphore semaphore=new Semaphore(2);

1、当调用new Semaphore(2) 方法时,默认会创建一个非公平的锁的同步阻塞队列。

2、把初始许可数量赋值给同步队列的state状态,state的值就代表当前所剩余的许可数量。

初始化完成后同步队列信息如下图:

img

(2)获取许可

semaphore.acquire();

1、当前线程会尝试去同步队列获取一个许可,获取许可的过程也就是使用原子的操作去修改同步队列的state ,获取一个许可则修改为state=state-1。

2、 当计算出来的state<0,则代表许可数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程。

3、当计算出来的state>=0,则代表获取许可成功。

源码:

/**
     *  获取1个许可
     */
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
/**
     * 共享模式下获取许可,获取成功则返回,失败则加入阻塞队列,挂起线程
     * @param arg
     * @throws InterruptedException
     */
    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        //尝试获取许可,arg为获取许可个数,当可用许可数减当前许可数结果小于0,则创建一个节点加入阻塞队列,挂起当前线程。
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
/**
     * 1、创建节点,加入阻塞队列,
     * 2、重双向链表的head,tail节点关系,清空无效节点
     * 3、挂起当前节点线程
     * @param arg
     * @throws InterruptedException
     */
    private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //创建节点加入阻塞队列
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            for (;;) {
                //获得当前节点pre节点
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);//返回锁的state
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //重组双向链表,清空无效节点,挂起当前线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

线程1、线程2、线程3、分别调用semaphore.acquire(),整个过程队列信息变化如下图:

img

(3)、释放许可

 semaphore.release();

当调用semaphore.release() 方法时

1、线程会尝试释放一个许可,释放许可的过程也就是把同步队列的state修改为state=state+1的过程

2、释放许可成功之后,同时会唤醒同步队列的所有阻塞节共享节点线程

3、被唤醒的节点会重新尝试去修改state=state-1 的操作,如果state>=0则获取许可成功,否则重新进入阻塞队列,挂起线程。

源码:

 /**
     * 释放许可
     */
    public void release() {
        sync.releaseShared(1);
    }
/**
     *释放共享锁,同时唤醒所有阻塞队列共享节点线程
     * @param arg
     * @return
     */
    public final boolean releaseShared(int arg) {
        //释放共享锁
        if (tryReleaseShared(arg)) {
            //唤醒所有共享节点线程
            doReleaseShared();
            return true;
        }
        return false;
    }
 /**
     * 唤醒所有共享节点线程
     */
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {//是否需要唤醒后继节点
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//修改状态为初始0
                        continue;
                    unparkSuccessor(h);//唤醒h.nex节点线程
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE));
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

继上面的图,当我们线程1调用semaphore.release(); 时候整个流程如下图:

img


回到◀疯狂创客圈

疯狂创客圈 - Java高并发研习社群,为大家开启大厂之门

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页