java并发之Semaphore信号量
简介:
Semaphore是计数信号量。Semaphore管理一系列许可证。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。
通俗来说:
Semaphore是一个可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来空出位置才能进入。对于N=1的情况,称为binary semaphore。一般的用法是,用于限制对于某一资源的同时访问。
构造方法
//创建具有给定的许可证数量和非公平的公平设置的Semaphore。
Semaphore(int permits)
//创建具有给定的许可证数量和给定的公平设置的Semaphore,true即为公平锁
Semaphore(int permits, boolean fair)
//注:所谓的公平信号量是获得锁的顺序与调用semaphore.acquire()的顺序有关,但不代表100%地获得信号量,仅仅是在概率上能得到保证。而非公平信号量就是无关的了。
上厕所Demo(非公平)
public class SemaphoreTest {
private static final Semaphore semaphore = new Semaphore(3);
private static final ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
public static void main(String[] args) {
String[] name = {"斌斌", "马昊", "小康", "郑祥", "浩哥", "蔡哥"};
for (int i = 0; i < 6; i++) {
Thread t1 = new InformationThread(name[i]);
threadPool.execute(t1);
}
threadPool.shutdown();
}
private static class InformationThread extends Thread {
private final String name;
public InformationThread(String name) {
this.name = name;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ":此时" + name + "进入了厕所(获取到了许可证),当前时间为:" + System.currentTimeMillis());
//模拟上厕所
Thread.sleep(1000);
System.out.println(name + "要准备离开厕所(准备释放许可证)了,当前时间为:" + System.currentTimeMillis());
semaphore.release();
System.out.println(name + "走出了厕所,当前可使用的厕所(许可证)数为:" + semaphore.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
上厕所Demo(公平)
public class SemaphoreTest {
private static final Semaphore semaphore = new Semaphore(3,true);
private static final ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
public static void main(String[] args) {
String[] name = {"斌斌", "马昊", "小康", "郑祥", "浩哥", "蔡哥"};
for (int i = 0; i < 6; i++) {
Thread t1 = new InformationThread(name[i]);
threadPool.execute(t1);
}
threadPool.shutdown();
}
private static class InformationThread extends Thread {
private final String name;
public InformationThread(String name) {
this.name = name;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ":此时" + name + "进入了厕所(获取到了许可证),当前时间为:" + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(name + "要准备离开厕所(准备释放许可证)了,当前时间为:" + System.currentTimeMillis());
semaphore.release();
System.out.println(name + "走出了厕所,当前可使用的厕所(许可证)数为:" + semaphore.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
常用方法
//获取许可证,许可证减一
semaphore.acquire();
//释放许可证,许可证加一
semaphore.release();
//返回此信号量中可用的当前许可数。
semaphore.availablePermits()
其他方法
//从此信号量中获取许可,不可中断
void acquireUninterruptibly()
//返回此信号量中当前可用的许可数。
int availablePermits()
//获取并返回立即可用的所有许可。
int drainPermits()
//返回一个 collection,包含可能等待获取的线程。
protected Collection<Thread> getQueuedThreads();
//返回正在等待获取的线程的估计数目。
int getQueueLength()
//查询是否有线程正在等待获取。
boolean hasQueuedThreads()
//如果此信号量的公平设置为 true,则返回 true。
boolean isFair()
//仅在调用时此信号量存在一个可用许可,才从信号量获取许可。
//如果没有许可可用,则立即返回false。 .
// tryAcquire() 方法不会顾及公平设置,即使为公平模式。
//如果想要顾及公平设置,使用tryAcquire(long, TimeUnit)方法
boolean tryAcquire()
//如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,
//则从此信号量获取一个许可。如果当前没有许可可用,则线程不可用进入休眠状态直到发生有如下事情发生:
//1.别的线程释放了资源,当前线程是下一个被分配资源的;
//2.别的线程中断了当前线程;
//3.等待时间过去
boolean tryAcquire(long timeout, TimeUnit unit)
源码解析
//公平构造方法
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
//非公平构造方法
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
/**
* Fair version
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//获取当前可用的线程数量
int available = getState();
//计算给完这次许可数量后的剩余的许可证数量
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
//小于0表示许可证数量不够
return remaining;
}
}
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);
}
执行过程是这样的:当一个线程请求到来时,如果state值代表的许可数足够使用,那么请求线程将会获得同步状态即对共享资源的访问权,并更新state的值(一般是对state值减1),但如果state值代表的许可数已为0,则请求线程将无法获取同步状态,线程将被加入到同步队列并阻塞,直到其他线程释放同步状态(一般是对state值加1)才可能获取对共享资源的访问权。
/**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//把当前线程标记为共享模式并加入队尾
final Node node = addWaiter(Node.SHARED);
//标识获取资源是否成功
boolean failed = true;
try {
for (;;) {
//拿到节点的前驱节点
final Node p = node.predecessor();
//判断前驱结点是否为head
if (p == head) {
int r = tryAcquireShared(arg);
//如果r>0 说明获取同步状态成功(即获取到许可证)
if (r >= 0) {
//将当前线程结点设置为头结点并传播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//调整同步队列中node结点的状态并判断是否应该被挂起
//并判断是否需要被中断,如果中断直接抛出异常,当前结点请求也就结束
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
//结束该结点线程的请求
cancelAcquire(node);
}
}
小总结:
在AQS中存在一个变量state,当我们创建Semaphore对象传入许可数值时,最终会赋值给state。state的数值代表同一个时刻可同时操作共享数据的线程数量,每当一个线程请求(如调用Semaphored的acquire()方法)获取同步状态成功,state的值将会减少1,直到state为0时,表示已没有可用的许可数,也就是对共享数据进行操作的线程数已达到最大值,其他后来线程将被阻塞。此时AQS内部会将线程封装成共享模式的Node结点,加入同步队列中等待并开启自旋操作。只有当持有对共享数据访问权限的线程执行完成任务并释放同步状态后,同步队列中的结点线程才有可能获取同步状态并被唤醒执行同步操作。注意在同步队列中获取到同步状态的结点将被设置成head并清空相关线程数据(毕竟线程已在执行也就没有必要保存信息了),AQS通过这种方式便实现共享锁
Release
//许可证释放是否成功
protected final boolean tryReleaseShared(int releases) {
for (;;) {
//获取当前许可数量
int current = getState();
//计算回收后的数量
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//CAS改变许可数量成功,返回true
if (compareAndSetState(current, next))
return true;
}
}
什么是CAS:
如果当前内存位置的值等于预期原值A的话,就将B赋值。否则,处理器 不做任何操作。整个比较并替换的操作是一个原子操作。这样做就不用害怕其它线程同时修改变量。
private void doReleaseShared() {
/*
* 保证释放动作(向同步等待队列尾部)传递,即使没有其他正在进行的
* 请求或释放动作。如果头节点的后继节点需要唤醒,那么执行唤醒
* 动作;如果不需要,将头结点的等待状态设置为PROPAGATE保证
* 唤醒传递。另外,为了防止过程中有新节点进入(队列),这里必
* 需做循环,所以,和其他unparkSuccessor方法使用方式不一样
* 的是,如果(头结点)等待状态设置失败,重新检测。
*/
for (;;) {
Node h = head;
//判断队列是否至少有两个node,如果队列从来没有初始化过(head为null),或者head就是tail,那么中间逻辑直接不走,直接判断head是否变化了。
if (h != null && h != tail) {
// 获取头节点对应的线程的状态
int ws = h.waitStatus;
// 如果头节点对应的线程是SIGNAL状态,则意味着头
//结点的后继结点所对应的线程需要被唤醒。
if (ws == Node.SIGNAL) {
// 修改头结点对应的线程状态设置为0。失败的话,则继续循环。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒头结点h的后继结点所对应的线程
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
/ 如果头结点发生变化,则继续循环。否则,退出循环。
if (h == head) // loop if head changed
break;
}
}
//唤醒传入结点的后继结点对应的线程
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
//拿到后继结点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//唤醒该线程
if (s != null)
LockSupport.unpark(s.thread);
}