这节我们来看一下信号量的实现方式,下面是我们本次的入口代码,我们先看一下非公平的方式是怎么做的:
Semaphore semaphore = new Semaphore(1);
semaphore.acquire(1);
semaphore.release(1);
首先来看一下它的构造方法:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
它是基于非公平框架实现的:
Sync(int permits) {
setState(permits);
}
最后调用父类的构造方法保存了我们传入的资源的个数,下面就开始看只存在一个线程获取资源的时候流程是怎样的:
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
在共享模式下获得资源,如果获取资源的线程被中断了,那么就终止资源获取:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
先尝试在共享模式下获取资源,会返回获取资源后还剩下的资源的数量,如果小于0,说明有一部分资源不能获取到,导致资源获取失败,需要等待其他线程释放资源后再获取
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
非公平模式下尝试获取资源:
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//获取还剩下的可获取的资源的数量
int available = getState();
//计算如果当前线程获取到了所需要的资源后还剩下多少资源
int remaining = available - acquires;
//如果计算后剩下的资源小于0,说明当前线程不能获取资源,否则通过CAS方式获取资源
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
尝试获取资源主要做了三件事:
1、获取还剩下的可获取的资源的数量
2、计算如果当前线程获取到了所需要的资源后还剩下多少资源
3、如果计算后剩下的资源小于0,说明当前线程不能获取资源,否则通过CAS方式获取资源
返回后就可用通过返回值是否小于0来判断是否成功获取到了资源,如果获取成功什么也不做,如果没获取到放到同步队列里面:
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);
//如果返回值大于等于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);
}
}
其实这个方法和可重入锁做的事情差不多,首先创建一个共享结点添加到同步队列中,进入死循环不断尝试获取资源,中间过程会改变自己的状态来选择是否应该挂起:
private Node addWaiter(Node mode) {
//创建一个结点
Node node = new Node(Thread.currentThread(), mode);
//添加到同步队列中
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
首先创建一个结点,如果这是第一次创建同步队列的结点需要通过enq方法入队,否则通过CAS方式直接添加到队列中:
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
构造方法中与当前线程进行了绑定。
我们假设当前创建的结点能获取到资源会进入这个方法:
private void setHeadAndPropagate(Node node, int propagate) {
//记录旧的头节点
Node h = head;
//将当前结点设为头节点
setHead(node);
//如果剩余的资源大于0
if (propagate > 0 ||
//如果旧的头结点为空
h == null ||
//如果旧的头节点状态小于0
h.waitStatus < 0 ||
//如果新的头节点为空
(h = head) == null ||
//如果新的头节点状态小于0
h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
//唤醒下一个结点
doReleaseShared();
}
}
在这个方法中修改头节点并且尝试唤醒下一个头节点:
private void doReleaseShared() {
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;
}
}
保证释放动作(向同步等待队列尾部)传递,即使没有其他正在进行的请求或释放动作。如果头节点的后继节点需要唤醒,那么执行唤醒动作;如果不需要,将头结点的等待状态设置为PROPAGATE保证唤醒传递。另外,为了防止过程中有新节点进入(队列),这里必需做循环,所以,和其他unparkSuccessor方法使用方式不一样的是,如果(头结点)等待状态设置失败,重新检测。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果前驱结点已经是唤醒模式直接返回
if (ws == Node.SIGNAL)
return true;
//如果前驱结点是取消状态,需要重构同步队列
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//否则将前驱结点设置为唤醒模式
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
如果前驱结点已经是唤醒模式了,那么当前结点可以放心的挂起,如果前驱结点被取消了,那么就要修改队列的链接关系,否则通过CAS将前驱结点设置为唤醒模式。
接下来我们看一下是如何释放资源的:
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
调用同步框架是释放资源的方法:
public final boolean releaseShared(int arg) {
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");
if (compareAndSetState(current, next))
return true;
}
}
通过CAS的方式将资源还回去。
信号量的公平方式和非公平方式的区别:
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;
}
}
公平方式在尝试获取资源的时候之后同步队列中没有结点才会尝试去获取,而非公平方式会直接去尝试获取。