Java中Semaphore(信号量)的使用
Semaphore 的作用:
在 java 中,使用了 synchronized 关键字和 Lock 锁实现了资源的并发访问控制,在同一时间只允许唯一了线程进入临界区访问资源 (读锁除外),这样子控制的主要目的是为了解决多个线程并发同一资源造成的数据不一致的问题。在另外一种场景下,一个资源有多个副本可供同时使用,比如打印机房有多个打印机、厕所有多个坑可供同时使用,这种情况下,Java 提供了另外的并发访问控制 -- 资源的多副本的并发访问控制,今天学习的信号量 Semaphore 即是其中的一种。
Semaphore 实现原理初探:
Semaphore 是用来保护一个或者多个共享资源的访问,Semaphore 内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于 1,意味着有共享资源可以访问,则使其计数器值减去 1,再访问共享资源。
如果计数器值为 0, 线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加 1,之前进入休眠的线程将被唤醒并再次试图获得信号量。
就好比一个厕所管理员,站在门口,只有厕所有空位,就开门允许与空侧数量等量的人进入厕所。多个人进入厕所后,相当于 N 个人来分配使用 N 个空位。为避免多个人来同时竞争同一个侧卫,在内部仍然使用锁来控制资源的同步访问。
Semaphore 的使用:
Semaphore 使用时需要先构建一个参数来指定共享资源的数量,Semaphore 构造完成后即是获取 Semaphore、共享资源使用完毕后释放 Semaphore。
Semaphore semaphore = new Semaphore(10,true);
semaphore.acquire();
//do something here
semaphore.release();
下面的代码就是模拟控制商场厕所的并发使用:
public class ResourceManage {
private final Semaphore semaphore ;
private boolean resourceArray[];
private final ReentrantLock lock;
public ResourceManage() {
this.resourceArray = new boolean[10];//存放厕所状态
this.semaphore = new Semaphore(10,true);//控制10个共享资源的使用,使用先进先出的公平模式进行共享;公平模式的信号量,先来的先获得信号量
this.lock = new ReentrantLock(true);//公平模式的锁,先来的先选
for(int i=0 ;i<10; i++){
resourceArray[i] = true;//初始化为资源可用的情况
}
}
public void useResource(int userId){
semaphore.acquire();
try{
//semaphore.acquire();
int id = getResourceId();//占到一个坑
System.out.print("userId:"+userId+"正在使用资源,资源id:"+id+"\n");
Thread.sleep(100);//do something,相当于于使用资源
resourceArray[id] = true;//退出这个坑
}catch (InterruptedException e){
e.printStackTrace();
}finally {
semaphore.release();//释放信号量,计数器加1
}
}
private int getResourceId(){
int id = -1;
lock.lock();
try {
//lock.lock();//虽然使用了锁控制同步,但由于只是简单的一个数组遍历,效率还是很高的,所以基本不影响性能。
for(int i=0; i<10; i++){
if(resourceArray[i]){
resourceArray[i] = false;
id = i;
break;
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
return id;
}
}
public class ResourceUser implements Runnable{
private ResourceManage resourceManage;
private int userId;
public ResourceUser(ResourceManage resourceManage, int userId) {
this.resourceManage = resourceManage;
this.userId = userId;
}
public void run(){
System.out.print("userId:"+userId+"准备使用资源...\n");
resourceManage.useResource(userId);
System.out.print("userId:"+userId+"使用资源完毕...\n");
}
public static void main(String[] args){
ResourceManage resourceManage = new ResourceManage();
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(new ResourceUser(resourceManage,i));//创建多个资源使用者
threads[i] = thread;
}
for(int i = 0; i < 100; i++){
Thread thread = threads[i];
try {
thread.start();//启动线程
}catch (Exception e){
e.printStackTrace();
}
}
}
}
最后,Semaphore 除了控制资源的多个副本的并发访问控制,也可以使用二进制信号量来实现类似 synchronized 关键字和 Lock 锁的并发访问控制功能(也就是只设置0和1的情况)
Java中的信号量和Linux中的思路是一样的
信号量的原理
信号量维护了一个信号量许可集。线程可以通过调用 acquire() 来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过 release() 来释放它所持有的信号量许可。
Semaphore 的函数列表
// 创建具有给定的许可数和非公平的公平设置的 Semaphore。
Semaphore(int permits)
// 创建具有给定的许可数和给定的公平设置的 Semaphore。
Semaphore(int permits, boolean fair)
// 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
void acquire()
// 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。
void acquire(int permits)
// 从此信号量中获取许可,在有可用的许可前将其阻塞。
void acquireUninterruptibly()
// 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。
void acquireUninterruptibly(int permits)
// 返回此信号量中当前可用的许可数。
int availablePermits()
// 获取并返回立即可用的所有许可。
int drainPermits()
// 返回一个 collection,包含可能等待获取的线程。
protected Collection getQueuedThreads()
// 返回正在等待获取的线程的估计数目。
int getQueueLength()
// 查询是否有线程正在等待获取。
boolean hasQueuedThreads()
// 如果此信号量的公平设置为 true,则返回 true。
boolean isFair()
// 根据指定的缩减量减小可用许可的数目。
protected void reducePermits(int reduction)
// 释放一个许可,将其返回给信号量。
void release()
// 释放给定数目的许可,将其返回到信号量。
void release(int permits)
// 返回标识此信号量的字符串,以及信号量的状态。
String toString()
// 仅在调用时此信号量存在一个可用许可,才从信号量获取许可。
boolean tryAcquire()
// 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。
boolean tryAcquire(int permits)
// 如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。
boolean tryAcquire(int permits, long timeout, TimeUnit unit)
// 如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。
boolean tryAcquire(long timeout, TimeUnit unit)
Semaphore 数据结构
Semaphore 的 UML 类图如下:
从图中可以看出:
(01) 和”ReentrantLock” 一样,Semaphore 也包含了 sync 对象,sync 是 Sync 类型;而且Sync 是一个继承于 AQS 的抽象类。
AQS提供了一种实现阻塞锁和一系列依赖FIFO同步队列的同步器的框架,如下图所示
信号量的底层是使用AQS的共享模式实现的:state的值就是可以同时共享的资源数,每次线程成功获取state都减一
(02) Sync 包括两个子类:” 公平信号量”FairSync 和 “非公平信号量”NonfairSync。sync 是”FairSync 的实例”,或者”NonfairSync 的实例”;默认情况下,sync 是 NonfairSync(即,默认是非公平信号量)。
Semaphore 源码分析 (基于 JDK1.7.0_40)
Semaphore 是通过共享锁实现的。根据共享锁的获取原则,Semaphore 分为” 公平信号量” 和” 非公平信号量”。
“公平信号量” 和” 非公平信号量” 的区别
“公平信号量” 和” 非公平信号量” 的释放信号量的机制是一样的!不同的是它们获取信号量的机制:线程在尝试获取信号量许可时,对于公平信号量而言,如果当前线程不在 CLH 队列的头部,则排队等候;而对于非公平信号量而言,无论当前线程是不是在 CLH 队列的头部,它都会直接获取信号量。该差异具体的体现在,它们的 tryAcquireShared() 函数的实现不同。
“公平信号量” 类
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;
}
}
}
“非公平信号量” 类
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
下面,我们逐步的对它们的源码进行分析。
1. 信号量构造函数
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
从中,我们可以信号量分为 “公平信号量(FairSync)” 和“非公平信号量 (NonfairSync)”。Semaphore(int permits) 函数会默认创建“非公平信号量”。
2. 公平信号量获取和释放
2.1 公平信号量的获取
Semaphore 中的公平信号量是 FairSync。它的获取 API 如下:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
信号量中的 acquire() 获取函数,实际上是调用的 AQS 中的 acquireSharedInterruptibly()。
acquireSharedInterruptibly() 的源码如下:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 如果线程是中断状态,则抛出异常。
if (Thread.interrupted())
throw new InterruptedException();
// 否则,尝试获取“共享锁”;获取成功则直接返回,获取失败,则通过doAcquireSharedInterruptibly()获取。
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
Semaphore 中”公平锁 “对应的 tryAcquireShared() 实现如下:
protected int tryAcquireShared(int acquires) {
for (;;) {
// 判断“当前线程”是不是CLH队列中的第一个线程线程,
// 若是的话,则返回-1。
if (hasQueuedPredecessors())
return -1;
// 设置“可以获得的信号量的许可数”
int available = getState();
// 设置“获得acquires个信号量许可之后,剩余的信号量许可数”
int remaining = available - acquires;
// 如果“剩余的信号量许可数>=0”,则设置“可以获得的信号量许可数”为remaining。
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
说明:tryAcquireShared() 的作用是尝试获取 acquires 个信号量许可数。
对于 Semaphore 而言,state 表示的是 “当前可获得的信号量许可数”。
下面看看 AQS 中 doAcquireSharedInterruptibly() 的实现:
private void doAcquireSharedInterruptibly(long arg)
throws InterruptedException {
// 创建”当前线程“的Node节点,且Node中记录的锁是”共享锁“类型;并将该节点添加到CLH队列末尾。
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
// 获取上一个节点。
// 如果上一节点是CLH队列的表头,则”尝试获取共享锁“。
final Node p = node.predecessor();
if (p == head) {
long 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);
}
}
说明:doAcquireSharedInterruptibly()会使当前线程一直等待,直到当前线程获取到共享锁 (或被中断) 才返回。
(01) addWaiter(Node.SHARED)的作用是,创建”当前线程 “的 Node 节点,且 Node 中记录的锁的类型是” 共享锁 “(Node.SHARED);并将该节点添加到 CLH 队列末尾。关于 Node 和 CLH 在”Java 多线程系列–“JUC 锁”03 之 公平锁(一)” 已经详细介绍过,这里就不再重复说明了。
(02) node.predecessor() 的作用是,获取上一个节点。如果上一节点是 CLH 队列的表头,则” 尝试获取共享锁 “。
(03) shouldParkAfterFailedAcquire() 的作用和它的名称一样,如果在尝试获取锁失败之后,线程应该等待,则返回 true;否则,返回 false。
(04) 当 shouldParkAfterFailedAcquire() 返回 ture 时,则调用 parkAndCheckInterrupt(),当前线程会进入等待状态,直到获取到共享锁才继续运行。
doAcquireSharedInterruptibly()中的 shouldParkAfterFailedAcquire(), parkAndCheckInterrupt 等函数在”Java 多线程系列–“JUC 锁”03 之 公平锁 (一)” 中介绍过,这里也就不再详细说明了。
2.2 公平信号量的释放
Semaphore 中公平信号量 (FairSync) 的释放 API 如下:
public void release() {
sync.releaseShared(1);
}
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
信号量的 releases() 释放函数,实际上是调用的 AQS 中的 releaseShared()。
releaseShared() 在 AQS 中实现,源码如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
说明:releaseShared() 的目的是让当前线程释放它所持有的共享锁。
它首先会通过 tryReleaseShared() 去尝试释放共享锁。尝试成功,则直接返回;尝试失败,则通过 doReleaseShared() 去释放共享锁。
Semaphore 重写了 tryReleaseShared(),它的源码如下:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
// 获取“可以获得的信号量的许可数”
int current = getState();
// 获取“释放releases个信号量许可之后,剩余的信号量许可数”
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
// 设置“可以获得的信号量的许可数”为next。
if (compareAndSetState(current, next))
return true;
}
}
如果 tryReleaseShared() 尝试释放共享锁失败,则会调用 doReleaseShared() 去释放共享锁。doReleaseShared() 的源码如下:
private void doReleaseShared() {
for (;;) {
// 获取CLH队列的头节点
Node h = head;
// 如果头节点不为null,并且头节点不等于tail节点。
if (h != null && h != tail) {
// 获取头节点对应的线程的状态
int ws = h.waitStatus;
// 如果头节点对应的线程是SIGNAL状态,则意味着“头节点的下一个节点所对应的线程”需要被unpark唤醒。
if (ws == Node.SIGNAL) {
// 设置“头节点对应的线程状态”为空状态。失败的话,则继续循环。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒“头节点的下一个节点所对应的线程”。
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()会释放 “共享锁”。它会从前往后的遍历 CLH 队列,依次“唤醒” 然后 “执行” 队列中每个节点对应的线程;最终的目的是让这些线程释放它们所持有的信号量。
3 非公平信号量获取和释放
Semaphore 中的非公平信号量是 NonFairSync。在 Semaphore 中,“非公平信号量许可的释放 (release)” 与“公平信号量许可的释放 (release)” 是一样的。
不同的是它们获取 “信号量许可” 的机制不同,下面是非公平信号量获取信号量许可的代码。
非公平信号量的 tryAcquireShared() 实现如下:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
nonfairTryAcquireShared() 的实现如下:
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 设置“可以获得的信号量的许可数”
int available = getState();
// 设置“获得acquires个信号量许可之后,剩余的信号量许可数”
int remaining = available - acquires;
// 如果“剩余的信号量许可数>=0”,则设置“可以获得的信号量许可数”为remaining。
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
说明:非公平信号量的 tryAcquireShared()调用 AQS 中的 nonfairTryAcquireShared()。而在 nonfairTryAcquireShared()的 for 循环中,它都会直接判断 “当前剩余的信号量许可数” 是否足够;足够的话,则直接“设置可以获得的信号量许可数”,进而再获取信号量。
而公平信号量的 tryAcquireShared() 中,在获取信号量之前会通过 if (hasQueuedPredecessors()) 来判断 “当前线程是不是在 CLH 队列的头部”,是的话,则返回 - 1。