什么是Semaphore
Semaphore是一个计数信号量,用于控制同时访问特定资源的线程数量,以维护资源的访问控制和确保系统的线程安全。Semaphore可以被视为一个包含若干许可(permit)的集合,线程需要先获取许可才能执行受控操作,执行完毕后归还许可,从而允许其他等待的线程继续执行。
public class Semaphore implements java.io.Serializable {}
主要方法包括:
acquire():尝试获取一个许可,如果没有可用许可,则阻塞直到有其他线程释放许可。
release():释放一个许可,增加信号量的计数,如果其他线程正在等待许可,则唤醒其中一个线程。
tryAcquire():尝试获取一个许可,如果无法立即获取则返回false,不会阻塞。
availablePermits():返回当前可用的许可数。
使用Semaphore
Semaphore 类中的构造函数根据传入的参数初始化 Semaphore 实例。这个构造函数接受两个参数:
- permits:表示信号量初始时允许同时访问共享资源的许可证数量。换句话说,就是并发访问的上限。
- fair:这是一个布尔值,用来决定信号量是否采用公平策略。
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
代码示例:
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2, false);
for (int i = 0; i < 8; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "排队");
Thread.sleep(1000L);
} catch (InterruptedException ignore) {
} finally {
semaphore.release();
}
}, "排队编号:" + i).start();
}
}
排队编号:1排队
排队编号:0排队
排队编号:2排队
排队编号:3排队
排队编号:4排队
排队编号:5排队
排队编号:7排队
排队编号:6排队
acquire函数
此方法从信号量获取一个(多个)许可,在提供一个许可前一直将线程阻塞,或者线程被中断,其源码如下
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
tryAcquireShared尝试在共享模式下获取锁,doAcquireSharedInterruptibly在共享模式下获取锁。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
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);
}
}
该方法首先将节点添加到等待队列中,然后尝试获取共享锁。如果成功获取到共享锁,就将节点设置为头节点并传播,然后将前驱节点的下一个节点设置为null以帮助垃圾回收。如果在尝试获取共享锁时失败,就检查是否需要在失败后挂起线程,并在挂起后检查线程是否被中断。如果线程被中断,就抛出InterruptedException异常。最后,如果获取共享锁失败,就取消获取操作。
release函数
此方法释放一个(多个)许可,将其返回给信号量,源码如下:
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
doReleaseShared:
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
doReleaseShared()方法,用于在共享模式下释放锁。它确保即使有其他正在进行的获取/释放操作,释放操作也能传播。通常的做法是尝试唤醒头节点的后继节点(如果需要信号)。但是,如果无法唤醒后继节点,状态将被设置为PROPAGATE,以确保在释放时继续传播。此外,我们必须循环,以防在我们执行此操作时添加新节点。与其他使用unparkSuccessor的情况不同,我们需要知道CAS重置状态是否失败,如果是,则重新检查。