并发编程原理与实战(四)经典并发协同方式synchronized与wait+notify详解
并发编程原理与实战(五)经典并发协同方式伪唤醒与加锁失效原理揭秘
并发编程原理与实战(六)详解并发协同利器CountDownLatch
并发编程原理与实战(七)详解并发协同利器CyclicBarrier
回顾下CyclicBarrier在多个线程同时调用接口的场景中的使用,如果我们要对接口做并发访问控制,限制同一时间的请求数量,也就是控制调用接口的并发线程数量,防止并发过大造成服务崩溃,“又要并发访问,又不能太猛”,那么要怎么做?Semaphore可以帮助我们实现,本文就来讲解另一个并发协同利器Semaphore。
计数信号量Semaphore
Semaphore的字面意思是信号标,信号灯的意思,java中的专业俗语通常叫信号量。Semaphore 的主要应用场景是通过许可证机制控制多线程对有限资源的并发访问,我们来看看官方说明。
/**
* A counting semaphore. Conceptually, a semaphore maintains a set of
* permits. Each {@link #acquire} blocks if necessary until a permit is
* available, and then takes it. Each {@link #release} adds a permit,
* potentially releasing a blocking acquirer.
* However, no actual permit objects are used; the {@code Semaphore} just
* keeps a count of the number available and acts accordingly.
*
* <p>Semaphores are often used to restrict the number of threads than can
* access some (physical or logical) resource.
*
...
*/
一个计数的信号量,从概念上讲,信号量维护了一组许可证。每次调用信号量对象的acquire()方法时线程都会进入阻塞状态直到获得许可证,每次调用信号量对象的release()方法时都会添加一个许可证,然后释放一个处于阻塞状态的获得者。Semaphore 通常用在限制访问某些物理资源或者逻辑资源的线程数量。
/** <p>Before obtaining an item each thread must acquire a permit from
* the semaphore, guaranteeing that an item is available for use. When
* the thread has finished with the item it is returned back to the
* pool and a permit is returned to the semaphore, allowing another
* thread to acquire that item. Note that no synchronization lock is
* held when {@link #acquire} is called as that would prevent an item
* from being returned to the pool. The semaphore encapsulates the
* synchronization needed to restrict access to the pool, separately
* from any synchronization needed to maintain the consistency of the
* pool itself.
* */
在获得一项物品之前,每个线程都必须先从信号量中获得一个许可证,以保证那项物品可以使用。当线程使用完物品之后,物品将被返回池中,并且将一个许可证返回给信号量,允许其他线程获得物品。需要注意的是,当线程调用acquire()方法时,并不会持有防止物品被返回给池的锁对象,信号量内部封装了需要限制访问池的同步控制,单独的同步控制以保证池的一致性。
二进制信号量Semaphore
二进制信号量是一种特殊的信号量实现,只有0或1个许可证。
/** <p>A semaphore initialized to one, and which is used such that it
* only has at most one permit available, can serve as a mutual
* exclusion lock. This is more commonly known as a <em>binary
* semaphore</em>, because it only has two states: one permit
* available, or zero permits available. When used in this way, the
* binary semaphore has the property (unlike many {@link java.util.concurrent.locks.Lock}
* implementations), that the "lock" can be released by a
* thread other than the owner (as semaphores have no notion of
* ownership). This can be useful in some specialized contexts, such
* as deadlock recovery.
* */
如果一个信号量初始化为1,且最多只有一个许可证可以使用,那么可以将这个信号量当成一个互斥的排他锁。这个通常称为二进制信号量,因为它只有两个状态:只有一个许可证可以使用或者零个许可证可以使用。当将信号量当成这种方式使用时,二进制信号量就有了锁可以被其他线程释放的属性(因为信号量没有所有权的概念)。这在某些特殊的场景下非常有用,比如死锁的恢复。
通过上面我们可以看出,计数信号量和二进制信号量还是有区别的。二进制信号量只有严格的0、1两种状态,0表示没有许可证可用,1表示只有1个许可证可用;而计数信号量是可以有多个许可证的。
获取信号量的公平性
公平性是指线程获取信号量的时候需不需要排队。
/** <p>The constructor for this class optionally accepts a
* <em>fairness</em> parameter. When set false, this class makes no
* guarantees about the order in which threads acquire permits. In
* particular, <em>barging</em> is permitted, that is, a thread
* invoking {@link #acquire} can be allocated a permit ahead of a
* thread that has been waiting - logically the new thread places itself at
* the head of the queue of waiting threads. When fairness is set true, the
* semaphore guarantees that threads invoking any of the {@link
* #acquire() acquire} methods are selected to obtain permits in the order in
* which their invocation of those methods was processed
* (first-in-first-out; FIFO). Note that FIFO ordering necessarily
* applies to specific internal points of execution within these
* methods. So, it is possible for one thread to invoke
* {@code acquire} before another, but reach the ordering point after
* the other, and similarly upon return from the method.
* Also note that the untimed {@link #tryAcquire() tryAcquire} methods do not
* honor the fairness setting, but will take any permits that are
* available.
*
* <p>Generally, semaphores used to control resource access should be
* initialized as fair, to ensure that no thread is starved out from
* accessing a resource. When using semaphores for other kinds of
* synchronization control, the throughput advantages of non-fair
* ordering often outweigh fairness considerations.
* */
通过构造函数创建信号量对象时,可以指定一个是否可以公平的获取信号量的参数。当设置为false时,无法保证线程获取许可证的顺序,即允许线程以插队的形式在已经排队等待获取信号量的线程前面获取信号量。当设置为true的时候,可以保证线程按调用acquire()方法的顺序来获取信号量,即先进先出的顺序。需要注意的是,调用不计时的尝试获取许可证的方法tryAcquire()将不会有公平性,将会获取任务可以使用的许可证。通常,信号量用于控制资源的访问时应该初始化为公平性,以保证没有线程出现访问不到资源而出现饿死的现象。
了解了计数信号量、二进制信号量、公平性的概念后,下面我们来分析下Semaphore的主要方法。
Semaphore的主要方法
1、构造函数
(1)Semaphore(int permits):指定许可证的个数创建信号量对象,默认是非公平模式的。需要注意的是,许可证的个数可能是负数,在这种情况下,释放许可证必须在任何获取许可证之前。
/**
* Creates a {@code Semaphore} with the given number of
* permits and nonfair fairness setting.
*
* @param permits the initial number of permits available.
* This value may be negative, in which case releases
* must occur before any acquires will be granted.
*/
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
(2)Semaphore(int permits, boolean fair):创建指定许可数量和公平模式的信号量,线程将会按照先进先出的顺序获取许可证。
/**
...
* @param fair {@code true} if this semaphore will guarantee
* first-in first-out granting of permits under contention,
* else {@code false}
*/
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
2、获取和释放信号量方法
(1)void acquire():获取一个许可证,线程进入阻塞状态直到有一个可以使用的许可证(其他线程调用release()方法释放许可证)或者线程被中断。如果有一个可用的许可证则立即返回,可用的许可证的数量减1。
/**
* Acquires a permit from this semaphore, blocking until one is
* available, or the thread is {@linkplain Thread#interrupt interrupted}.
*
* <p>Acquires a permit, if one is available and returns immediately,
* reducing the number of available permits by one.
*
* <p>If no permit is available then the current thread becomes
* disabled for thread scheduling purposes and lies dormant until
* one of two things happens:
* <ul>
* <li>Some other thread invokes the {@link #release} method for this
* semaphore and the current thread is next to be assigned a permit; or
* <li>Some other thread {@linkplain Thread#interrupt interrupts}
* the current thread.
* </ul>
*
* <p>If the current thread:
* <ul>
* <li>has its interrupted status set on entry to this method; or
* <li>is {@linkplain Thread#interrupt interrupted} while waiting
* for a permit,
* </ul>
* then {@link InterruptedException} is thrown and the current thread's
* interrupted status is cleared.
*
* @throws InterruptedException if the current thread is interrupted
*/
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
(2)void acquire(int permits):获取指定数量的许可证,线程进入阻塞状态,直到指定数量的许可证全部满足或者被中断才会立即返回。此方法和循环调用acquire()方法获取许可证的效果是一样的,除了它是一次性获取所有的许可证。如果传入的是负数将会抛出非法数据异常。
/**
...
* <p>Acquires the given number of permits, if they are available,
* and returns immediately, reducing the number of available permits
* by the given amount. This method has the same effect as the
* loop {@code for (int i = 0; i < permits; ++i) acquire();} except
* that it atomically acquires the permits all at once:
...
* @throws IllegalArgumentException if {@code permits} is negative
*/
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
(3)void release():释放一个许可证。如果有线程正在尝试获取许可证,那么刚释放的许可证将会分配给其中的一个线程,线程进入调度可运行状态。没有要求释放许可证的线程一定要通过调用acquire()方法来获取得刚释放的许可证。
/**
* Releases a permit, returning it to the semaphore.
*
* <p>Releases a permit, increasing the number of available permits by
* one. If any threads are trying to acquire a permit, then one is
* selected and given the permit that was just released. That thread
* is (re)enabled for thread scheduling purposes.
*
* <p>There is no requirement that a thread that releases a permit must
* have acquired that permit by calling {@link #acquire}.
* Correct usage of a semaphore is established by programming convention
* in the application.
*/
public void release() {
sync.releaseShared(1);
}
(4)void release(int permits):释放指定数量的许可证,将会增加指定数量的许可证。如果释放的许可证数量能满足线程的请求,那么线程将进入可调度运行状态,如果还有剩余的许可证,那么将会分配给其他请求获取许可证的线程。
/**
* Releases the given number of permits, returning them to the semaphore.
*
* <p>Releases the given number of permits, increasing the number of
* available permits by that amount.
* If any threads are trying to acquire permits, then one thread
* is selected and given the permits that were just released.
* If the number of available permits satisfies that thread's request
* then that thread is (re)enabled for thread scheduling purposes;
* otherwise the thread will wait until sufficient permits are available.
* If there are still permits available
* after this thread's request has been satisfied, then those permits
* are assigned in turn to other threads trying to acquire permits.
*
...
*/
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
3、尝试获取许可证方法
(1)boolean tryAcquire():尝试获取一个许可证,立即返回结果。如果有可用的许可证则返回true,否则返回false。即使信号量设置了公平模式,如果有可用的许可证那么调用该方法会立即获得,无论是否有其他线程正在等待。调用该方法并不会尊重公平性,如果要尊重公平性,可以使用带超时时间的tryAcquire(long, TimeUnit) ,tryAcquire(0, TimeUnit.SECONDS)方法。
/**
* Acquires a permit from this semaphore, only if one is available at the
* time of invocation.
*
* <p>Acquires a permit, if one is available and returns immediately,
* with the value {@code true},
* reducing the number of available permits by one.
*
* <p>If no permit is available then this method will return
* immediately with the value {@code false}.
*
* <p>Even when this semaphore has been set to use a
* fair ordering policy, a call to {@code tryAcquire()} <em>will</em>
* immediately acquire a permit if one is available, whether or not
* other threads are currently waiting.
* This "barging" behavior can be useful in certain
* circumstances, even though it breaks fairness. If you want to honor
* the fairness setting, then use
* {@link #tryAcquire(long, TimeUnit) tryAcquire(0, TimeUnit.SECONDS)}
* which is almost equivalent (it also detects interruption).
*
* @return {@code true} if a permit was acquired and {@code false}
* otherwise
*/
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
(2)boolean tryAcquire(int permits):尝试获取多个许可证,如果能满足要求则立即返回true,否则返回fasle,该方法同样不尊重公平性。
(3)boolean tryAcquire(long timeout, TimeUnit unit):尝试在指定的时间内获取一个许可证。如果有可用的许可证则立即返回,否则线程将进入阻塞状态直到有可用的许可证或者等待时间用完了。该方法尊重公平性。
(4)boolean tryAcquire(int permits, long timeout, TimeUnit unit):尝试在指定的时间内获取指定个数的许可证。如果能满足则立即返回,否则线程将进入阻塞状态直到有满足条件的许可证或者等待时间用完了。该方法尊重公平性。
4、查询类方法
(1)int availablePermits():返回当前可用的许可证数量,通常用于调试或者测试。
(2)boolean isFair():判断是否为公平模式。
(3)int getQueueLength():获取等待许可证的线程数量。
(4)Collection getQueuedThreads() :返回一个等待获取许可证的线程的集合。
总结
本文讲解了计数信号量和二进制信号量的概念,并对创建信号量、获取和释放信号量的主要方法,以及公平性的概念进行了讲解,希望通过解读这些方法能加深对Semaphore的理解,关于Semaphore的应用举例放到下一篇中。Semaphore的方法还是比较多的,下面用一个表格总结下:
类别 | 核心方法 | 应用场景 |
---|---|---|
构造函数 | Semaphore(int permits),Semaphore(int permits, boolean fair) | 创建信号量对象 |
成员方法 | acquire(),acquire(int permits),tryAcquire(),tryAcquire(int permits),tryAcquire(long timeout, TimeUnit unit),tryAcquire(int permits, long timeout, TimeUnit unit) | 获取信号量 |
成员方法 | release(),release(int permits) | 释放信号量 |
成员方法 | availablePermits(),isFair(),getQueueLength(),getQueuedThreads() | 状态查询 |
如果文章对您有帮助,不妨“帧栈”一下,关注“帧栈”公众号,第一时间获取推送文章,您的肯定将是我写作的动力!