资讯专栏INFORMATION COLUMN

Java多线程进阶(二十)—— J.U.C之synchronizer框架:Semaphore

boredream / 2802人阅读

摘要:当线程使用完共享资源后,可以归还许可,以供其它需要的线程使用。所以,并不会阻塞调用线程。立即减少指定数目的可用许可数。方法用于将可用许可数清零,并返回清零前的许可数六的类接口声明类声明构造器接口声明

本文首发于一世流云的专栏:https://segmentfault.com/blog...
一、Semaphore简介

Semaphore,又名信号量,这个类的作用有点类似于“许可证”。有时,我们因为一些原因需要控制同时访问共享资源的最大线程数量,比如出于系统性能的考虑需要限流,或者共享资源是稀缺资源,我们需要有一种办法能够协调各个线程,以保证合理的使用公共资源。

Semaphore维护了一个许可集,其实就是一定数量的“许可证”。
当有线程想要访问共享资源时,需要先获取(acquire)的许可;如果许可不够了,线程需要一直等待,直到许可可用。当线程使用完共享资源后,可以归还(release)许可,以供其它需要的线程使用。

另外,Semaphore支持公平/非公平策略,这和ReentrantLock类似,后面讲Semaphore原理时会看到,它们的实现本身就是类似的。

二、Semaphore示例

我们来看下Oracle官方给出的示例:

class Pool {
    private static final int MAX_AVAILABLE = 100; // 可同时访问资源的最大线程数
    private final Semaphore available = new Semaphore(MAX_AVAILABLE, true);
    protected Object[] items = new Object[MAX_AVAILABLE];   //共享资源
    protected boolean[] used = new boolean[MAX_AVAILABLE];
    public Object getItem() throws InterruptedException {
        available.acquire();
        return getNextAvailableItem();
    }
    public void putItem(Object x) {
        if (markAsUnused(x))
            available.release();
    }
    private synchronized Object getNextAvailableItem() {
        for (int i = 0; i < MAX_AVAILABLE; ++i) {
            if (!used[i]) {
                used[i] = true;
                return items[i];
            }
        }
        return null;
    }
    private synchronized boolean markAsUnused(Object item) {
        for (int i = 0; i < MAX_AVAILABLE; ++i) {
            if (item == items[i]) {
                if (used[i]) {
                    used[i] = false;
                    return true;
                } else
                    return false;
            }
        }
        return false;
    }
}

items数组可以看成是我们的共享资源,当有线程尝试使用共享资源时,我们要求线程先获得“许可”(调用Semaphoreacquire方法),这样线程就拥有了权限,否则就需要等待。当使用完资源后,线程需要调用Semaphorerelease方法释放许可。

 注意:上述示例中,对于共享资源访问需要由锁来控制,Semaphore仅仅是保证了线程由权限使用共享资源,至于使用过程中是否由并发问题,需要通过锁来保证。

总结一下,许可数 ≤ 0代表共享资源不可用。许可数 > 0,代表共享资源可用,且多个线程可以同时访问共享资源。

这是不是和CountDownLatch有点像?
我们来比较下:

同步器 作用
CountDownLatch 同步状态State > 0表示资源不可用,所有线程需要等待;State == 0表示资源可用,所有线程可以同时访问
Semaphore 剩余许可数 < 0表示资源不可用,所有线程需要等待; 许可剩余数 ≥ 0表示资源可用,所有线程可以同时访问
如果读者阅读过本系列的AQS相关文章,应该立马可以反应过来,这其实就是对同步状态的定义不同。
CountDownLatch内部实现了AQS的共享功能,那么Semaphore是否也一样是利用内部类实现了AQS的共享功能呢?
三、Semaphore原理 Semaphore的内部结构

我们先来看下Semaphore的内部:

可以看到,Semaphore果然是通过内部类实现了AQS框架提供的接口,而且基本结构几乎和ReentrantLock完全一样,通过内部类分别实现了公平/非公平策略。

Semaphore对象的构造

Semaphore sm = new Semaphore (3, true);

Semaphore有两个构造器:

构造器1:

构造器2:

构造时需要指定“许可”的数量——permits,内部结构如下:

四、Semaphore的公平策略分析

我们还是通过示例来分析:

假设现在一共3个线程:ThreadAThreadBThreadC。一个许可数为2的公平策略的Semaphore。线程的调用顺序如下:
Semaphore sm = new Semaphore (2, true);

// ThreadA: sm.acquire()

// ThreadB: sm.acquire(2)

// ThreadC: sm.acquire()

// ThreadA: sm.release()

// ThreadB: sm.release(2)
创建公平策略的Semaphore对象
Semaphore sm = new Semaphore (2, true);

可以看到,内部创建了一个FairSync对象,并传入许可数permits

SyncSemaphore的一个内部抽象类,公平策略的FairSync和非公平策略的NonFairSync都继承该类。
可以看到,构造器传入的permits值就是同步状态的值,这也体现了我们在AQS系列中说过的:
AQS框架的设计思想就是分离构建同步器时的一系列关注点,它的所有操作都围绕着资源——同步状态(synchronization state)来展开,并将资源的定义和访问留给用户解决:

ThreadA调用acqure方法

Semaphoreacquire方法内部调用了AQS的方法,入参"1"表示尝试获取1个许可:

AQS的acquireSharedInterruptibly方式是共享功能的一部分,我们在AQS系列中就已经对它很熟悉了:

关键来看下Semaphore是如何实现tryAcquireShared方法的:

对于Semaphore来说,线程是可以一次性尝试获取多个许可的,此时只要剩余的许可数量够,最终会通过自旋操作更新成功。如果剩余许可数量不够,会返回一个负数,表示获取失败。

显然,ThreadA获取许可成功。此时,同步状态值State == 1,等待队列的结构如下:

ThreadB调用acqure(2)方法

带入参的aquire方法内部和无参的一样,都是调用了AQS的acquireSharedInterruptibly方法:

此时,ThreadB一样进入tryAcquireShared方法。不同的是,此时剩余许可数不足,因为ThreadB一次性获取2个许可,tryAcquireShared方法返回一个负数,表示获取失败:
remaining = available - acquires = 1- 2 = -1;

ThreadB会调用doAcquireSharedInterruptibly方法:

上述方法首先通过addWaiter方法将ThreadB包装成一个共享结点,加入等待队列:

然后会进入自旋操作,先尝试获取一次资源,显然此时是获取失败的,然后判断是否要进入阻塞(shouldParkAfterFailedAcquire):

上述方法会先将前驱结点的状态置为SIGNAL,表示ThreadB需要阻塞,但在阻塞之前需要将前驱置为SIGNAL,以便将来可以唤醒ThreadB。

最终ThreadB会在parkAndCheckInterrupt中进入阻塞:

此时,同步状态值依然是State == 1,等待队列的结构如下:

ThreadC调用acqure()方法

流程和步骤3完全相同,ThreadC被包装成结点加入等待队列后:

同步状态:State == 1

ThreadA调用release()方法

Semaphorerealse方法调用了AQS的releaseShared方法,默认入参为"1",表示归还一个许可:

来看下Semaphore是如何实现tryReleaseShared方法的,tryReleaseShared方法是一个自旋操作,直到更新State成功:

更新完成后,State == 2,ThreadA会进入doReleaseShared方法,先将头结点状态置为0,表示即将唤醒后继结点:

此时,等待队列结构:

然后调用unparkSuccessor方法唤醒后继结点:

此时,ThreadB被唤醒,会从原阻塞处继续向下执行:

此时,同步状态:State == 2

ThreadB从原阻塞处继续执行

ThreadB被唤醒后,从下面开始继续往下执行,进入下一次自旋:

在下一次自旋中,ThreadB调用tryAcquireShared方法成功获取到共享资源(State修改为0),setHeadAndPropagate方法把ThreadB变为头结点,
并根据传播状态判断是否要唤醒并释放后继结点:

同步状态:State == 0

ThreadB会调用doReleaseShared方法,继续尝试唤醒后继的共享结点(也就是ThreadC),这个过程和ThreadB被唤醒完全一样:

同步状态:State == 0

ThreadC从原阻塞处继续执行

由于目前共享资源仍为0,所以ThreadC被唤醒后,在经过尝试获取资源失败后,又进入了阻塞:

ThreadA调用release(2)方法

内部和无参的release方法一样:

更新完成后,State == 2,ThreadA会进入doReleaseShared方法,唤醒后继结点:

此时,等待队列结构:

同步状态:State == 2

ThreadC从原阻塞处继续执行

由于目前共享资源为2,所以ThreadC被唤醒后,获取资源成功:

最终同步队列的结构如下:

同步状态:State == 0

五、总结

Semaphore其实就是实现了AQS共享功能的同步器,对于Semaphore来说,资源就是许可证的数量:

剩余许可证数(State值) - 尝试获取的许可数(acquire方法入参) ≥ 0:资源可用

剩余许可证数(State值) - 尝试获取的许可数(acquire方法入参) < 0:资源不可用

这里共享的含义是多个线程可以同时获取资源,当计算出的剩余资源不足时,线程就会阻塞。

注意:Semaphore不是锁,只能限制同时访问资源的线程数,至于对数据一致性的控制,Semaphore是不关心的。当前,如果是只有一个许可的Semaphore,可以当作锁使用。
Semaphore的非公平策略

另外,上述我们讨论的是Semaphore的公平策略,非公平策略的差异并不大:

可以看到,非公平策略不会去查看等待队列的队首是否有其它线程正在等待,而是直接尝试修改State值。

Semaphore的其它方法

Semaphore还有两个比较特殊的方法,这两个方法的特点是采用自旋操作State变量,直到成功为止。所以,并不会阻塞调用线程。

reducePermits

reducePermits立即减少指定数目的可用许可数。

drainPermits

drainPermits方法用于将可用许可数清零,并返回清零前的许可数

六、Semaphore的类/接口声明 类声明

构造器

接口声明


文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/76652.html

相关文章

  • Java线程进阶(一)—— J.U.C并发包概述

    摘要:整个包,按照功能可以大致划分如下锁框架原子类框架同步器框架集合框架执行器框架本系列将按上述顺序分析,分析所基于的源码为。后,根据一系列常见的多线程设计模式,设计了并发包,其中包下提供了一系列基础的锁工具,用以对等进行补充增强。 showImg(https://segmentfault.com/img/remote/1460000016012623); 本文首发于一世流云专栏:https...

    anonymoussf 评论0 收藏0
  • Java线程进阶(五)—— J.U.Clocks框架:LockSupport

    摘要:初始时,为,当调用方法时,线程的加,当调用方法时,如果为,则调用线程进入阻塞状态。该对象一般供监视诊断工具确定线程受阻塞的原因时使用。 showImg(https://segmentfault.com/img/remote/1460000016012503); 本文首发于一世流云的专栏:https://segmentfault.com/blog... 一、LockSupport类简介...

    jsyzchen 评论0 收藏0
  • Java线程进阶(六)—— J.U.Clocks框架:AQS综述(1)

    摘要:在时,引入了包,该包中的大多数同步器都是基于来构建的。框架提供了一套通用的机制来管理同步状态阻塞唤醒线程管理等待队列。指针用于在结点线程被取消时,让当前结点的前驱直接指向当前结点的后驱完成出队动作。 showImg(https://segmentfault.com/img/remote/1460000016012438); 本文首发于一世流云的专栏:https://segmentfau...

    cocopeak 评论0 收藏0
  • Java线程进阶(十八)—— J.U.Csynchronizer框架:CountDownLatc

    摘要:线程可以调用的方法进入阻塞,当计数值降到时,所有之前调用阻塞的线程都会释放。注意的初始计数值一旦降到,无法重置。 showImg(https://segmentfault.com/img/remote/1460000016012041); 本文首发于一世流云的专栏:https://segmentfault.com/blog... 一、CountDownLatch简介 CountDow...

    Elle 评论0 收藏0
  • Java线程进阶(二)—— J.U.Clocks框架:接口

    摘要:二接口简介可以看做是类的方法的替代品,与配合使用。当线程执行对象的方法时,当前线程会立即释放锁,并进入对象的等待区,等待其它线程唤醒或中断。 showImg(https://segmentfault.com/img/remote/1460000016012601); 本文首发于一世流云的专栏:https://segmentfault.com/blog... 本系列文章中所说的juc-...

    dkzwm 评论0 收藏0

发表评论

0条评论

boredream

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<