Semaphore信号量分析
信号量是JUC包下的一个类,它和ReentrantLock,CountDownLatch和CyclicBarrier不同,锁是为了保证原子性,一般是排他的,只能一个线程获取到锁来执行业务。
信号量则可以初始化多个资源,多线程获取资源,如果资源足够,则都可以获取成功。
信号量也是基于AQS实现的,有公平非公平的概念。
例:连接池就比较适合使用信号量实现,使用信号量初始化连接池的连接数量,支持多线程同时去获取资源,如果资源足够,则顺利获取到连接资源,资源不足,线程进入AQS同步队列,等其他线程释放连接资源时在唤醒同步队列中的线程
例:也可以模拟一个景区,景区的接客流量数是有限的,将允许的流量数初始化到信号量中,购票线程去信号量中获取票数,如果票数不足,可以进入同步队列一直等待,也可以购票失败,等待其他线程释放票后,再重新购票
一,内部结构
1.1,同样内置了sync
/** All mechanics via AbstractQueuedSynchronizer subclass */
private final Sync sync;
// 继承AQS,重写了些逻辑
abstract static class Sync extends AbstractQueuedSynchronizer {...}
// 非公平逻辑
static final class NonfairSync extends Sync {...}
// 公平逻辑实现
static final class FairSync extends Sync
// 构造方法,可以初始化资源数,默认非公平模式
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
// 构造方法,初始化资源数,并且指定公平/非公平模式
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
1.2,基本方法简介
1,acquire():获取资源,也可以获取指定个数的资源,获取不到就进入AQS同步队列等待,如果线程被中断,抛出异常
2,acquireUninterruptibly():与acquire一样,但是线程被设置中断位时也不结束,继续等待
3,tryAcquire():尝试获取资源,获取不到返回false
4,tryAcquire(long timeout, TimeUnit unit):指定时间内获取不到资源,返回false
5,release():释放一次资源,也可以释放指定个数的资源
提示:以下是本篇文章正文内容,下面案例仅供参考
二、信号量的基本使用
public class SemaphoreTest {
// 景区有十张票
static Semaphore semaphore = new Semaphore(10);
private static Thread thread = new Thread(() -> {
System.out.println("一家五口买票");
try {
// 阻塞方法,一次性买5张票,如果剩余票数不足,线程进入AQS同步队列,等待其他线程释放票
semaphore.acquire(5);
// 这时信号量中还剩5张票
System.out.println("一家五口买到了5张票");
// 模拟去景区
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 出景区后释放票
semaphore.release(5);
}
});
public static void main(String[] args) throws InterruptedException {
// 一家五口先去买票
thread.start();
// 留给子线程的启动时间
Thread.sleep(100);
// 其他人去信号量中去购票
for (int i = 0; i < 15; i++) {
int finalI = i;
new Thread(() -> {
try {
// 只买一张票,买不到死等
semaphore.acquire();
System.out.println(finalI + "获取到票");
// 模拟去景区
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 出景区后释放票
semaphore.release();
}
}).start();
}
// 主线程也尝试获取票,可能没票了,此时不等待,直接执行else
if (semaphore.tryAcquire()) {
System.out.println("main获取到资源");
// 释放资源
semaphore.release();
} else {
System.out.println("main未获取到资源");
}
}
}
连接池获取数据库连接是同样的逻辑
三、内部方法分析
1. 分析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)
// 如果返回值小于0,意味着剩余资源不足,将线程放入AQS同步队列
doAcquireSharedInterruptibly(arg);
}
// tryAcquireShared: 非公平模式尝试获取资源
final int nonfairTryAcquireShared(int acquires) {
// 死循环
for (;;) {
// 获取剩余资源数
int available = getState();
// 减去当前所需资源
int remaining = available - acquires;
// 如果资源不足
if (remaining < 0 ||
// 资源足够,使用CAS更新剩余资源数(如果CAS失败,由于是死循环,会重新获取资源)
compareAndSetState(available, remaining))
// 返回剩余资源数
return remaining;
}
}
// 公平模式尝试获取资源
protected int tryAcquireShared(int acquires) {
// 死循环
for (;;) {
// 判断前面是否有排队的节点,因为是公屏模式,排队获取资源
if (hasQueuedPredecessors())
// 还没有轮到当前线程获取资源
return -1;
// 获取当前剩余资源数
int available = getState();
// 减去当前所需资源
int remaining = available - acquires;
// 如果资源不足
if (remaining < 0 ||
// 资源足够,使用CAS更新剩余资源数(如果CAS失败,由于是死循环,会重新获取资源)
compareAndSetState(available, remaining))
// 返回剩余资源数
return remaining;
}
}
// 获取资源失败,进入AQS同步队列
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
// 将当前线程封装成Node,从AQS双向链表(同步队列)的尾部追加当前节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 死循环
for (;;) {
// 获取当前节点的pre
final Node p = node.predecessor();
// 如果上一个节点是head头
if (p == head) {
// 则再次尝试获取资源
int r = tryAcquireShared(arg);
// 如果获取成功
if (r >= 0) {
// 需要结合JDK1.5代码来看,里面的propagate是为了唤醒后续的节点
setHeadAndPropagate(node, r);
// 节点被唤醒后会从队列中移除,此处方便GC回收
p.next = null; // help GC
failed = false;
// 结束死循环
return;
}
}
// 上一个节点不是head或者没拿到资源,当前线程进入AQS同步队列
// 判断线程是否可以被挂起
if (shouldParkAfterFailedAcquire(p, node) &&
// 线程挂起 LockSupport.park,进入WAITING
parkAndCheckInterrupt())
// 抛出中断异常也可以结束死循环
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
非公平模式:
尝试获取资源,如果资源数不足,返回负数,进入doAcquireSharedInterruptibly逻辑,将当前线程挂到AQS同步队列的尾部,然后判断上一节点是否是head,如果是再次获取一次资源,获取成功就会从队列中删除当前节点,结束
获取失败或者不是head,则线程挂起,进入WAITING等待唤醒重新获取资源
公平模式:
与非公平一样的流程,区别在于获取资源时判断前面是否有排队的节点,如果有,说明还没有轮到自己获取
tryAcquire没什么好分析的,上述代码包含tryAcquire逻辑
2.release方法分析
代码如下(示例):
public void release() {
// 默认释放一次,也有释放指定次数的方法
sync.releaseShared(1);
}
// 归还指定个数的资源
public final boolean releaseShared(int arg) {
// 尝试归还,这个逻辑是信号量自己实现的,没有使用AQS的,且不区分公平还是非公平
if (tryReleaseShared(arg)) {
// 归还资源成功,唤醒同步队列中的线程去获取资源
doReleaseShared();
return true;
}
return false;
}
// 尝试归还资源
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归还资源,失败重来
if (compareAndSetState(current, next))
// 归还成功就结束死循环,所以几乎不会归还失败
return true;
}
}
// 唤醒后续节点的线程去获取资源
private void doReleaseShared() {
// 死循环
for (;;) {
// 获取head
Node h = head;
// 判断是否有排队的节点
if (h != null && h != tail) {
// 获取节点状态
int ws = h.waitStatus;
// 如果状态是-1,意味着后续有节点需要被唤醒
if (ws == Node.SIGNAL) {
// 将-1改成0,如果失败,继续死循环
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 修改成功,根据node找到沉睡的线程并唤醒
unparkSuccessor(h);
}
// 状态等于0说明是正常节点
else if (ws == 0 &&
// 将0修改成-3,起到向队列后面传播的特性
// 解决JDK1.5信号量中,有资源,但是队列中WAITING的线程没有被唤醒的问题
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 没有排队的节点,结束
// 唯一出口
if (h == head) // loop if head changed
break;
}
}
总结
其他的源码方法基本和上述代码类似,没什么可聊的,大部分都使用了AQS的默认实现
AQS的的内部属性state,实现了AQS的锁都是使用CAS去更改state值,成功就是获取到了锁。信号量同样使用了这个变量,只是变成了资源数的概念,可以多线程去获取不会有并发问题,线程使用完资源后,就会归还,如果同步队列中有等待的线程,就会被唤醒,参加到资源的获取中
信号量比其他AQS的实现子类都要简单,解决JDK1.5信号量bug的代码,有兴趣可以研究