在JUC包下,有三个控制并发的工具类Semaphore信号量、CountDownLatch倒计时器和CyclicBarrier循环栅栏,下面对Semaphore这个工具类的使用示例、源码分析做一个简单的阐述
Semaphore通过维护许可证的数量来控制线程对共享资源的访问。如果许可证数量大于0,则线程可以访问共享资源,否则,线程不能访问共享资源。
Semaphore的一个经典例子就是停车问题,假设共有3个车位(许可证数量),现在有6辆车来停车(线程),只有车位有空闲时车辆才能抢占车位,否则需要等待,直到占用车位的车辆离开,其代码实现如下所示:
public class semphoreTest {
private static int park = 3;
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(park);
for(int i=1;i<7;i++){
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"号车辆停车==============");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
System.out.println(Thread.currentThread().getName()+"号车辆离开************");
}
},String.valueOf(i)).start();
}
}
}
代码运行结果如下所示,可以看到,有剩余的车位就会有车辆抢占停车,车位满了就等待车辆离开车位,而且车辆抢占车位并不是按照顺序抢占的。
看完上面的应用示例后,我们看一下其源码,首先看一下它的构造方法,源码中提供了两个有参构造,代码如下:
//传递一个参数,许可证数量,可以看出默认构造非公平模式
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
//传递两个参数,许可证数量和公平模式,根据fair值判断创建公平模式还是非公平模式
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
可以看出,这两个构造方法都与Sync类有关系,可知Semaphore内部也是定义了一个继承AQS类的Sync内部类,然后分别定义了NonfairSync(非公平模式)和FairSync(公平模式)两个继承Sync类的内部类。new FairSync(permits)
和new NonfairSync(permits)
的代码块定义了相同的方法:
//new NonfairSync
NonfairSync(int permits) {
super(permits);
}
//new FairSync
FairSync(int permits) {
super(permits);
}
//这两个方法都调用super(permits)方法,由于继承的是Sync类,因此,看super(permits)方法的实现
Sync(int permits) {
setState(permits);
}
//最终,调用的是AQS类的setState方法,把许可证数量赋值给共享资源state
protected final void setState(int newState) {
state = newState;
}
acquire
方法既可以无参调用也可以有参调用,这两个的区别在于如果传递参数,那么就需要校验参数是否合法,如果许可证数量参数小于0,会抛出非法数据异常。
//无参调用,默认尝试获取1个许可
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//有参调用
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
可以看出,最终都是通过Sync类调用AQS的acquireSharedInterruptibly
方法实现,代码如下:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//tryAcquireShared(arg)返回值是个int类型数值,只有小于0才能执行doAcquireSharedInterruptibly(arg)方法
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
先看tryAcquireShared(arg)
方法,这个方法是一个模板方法,具体实现在子类中,先看nonFairSync类中的实现,可以看出,其实宏观上的组成类似于ReentrantLock,都是Sync类实现非公平锁的tryAcquireXXX方法
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
//自旋
for (;;) {
//获取当前可提供的许可证数量
int available = getState();
//获取请求成功后剩余的许可证数量
int remaining = available - acquires;
//如果remaining < 0,直接返回remaining,执行AQS的doAcquireSharedInterruptibly方法
//如果remaining >= 0,继续执行cas操作,修改共享资源state值,修改成功,返回remaining,阻塞doAcquireSharedInterruptibly的执行
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
再来看一下公平锁的的tryAcquireShared
方法的实现,代码如下:
protected int tryAcquireShared(int acquires) {
//自旋
for (;;) {
//hasQueuedPredecessors()判断当前线程是否需要排队,之前ReentrantLock的公平锁tryAcquire方法讲过,只有同步队列的头节点的下个节点才能获取资源,否则都需要排队
if (hasQueuedPredecessors())
//返回负值,执行doAcquireSharedInterruptibly
return -1;
//获取当前许可证的数量
int available = getState();
//新的剩余许可证的数量,下面的代码同非公平模式相同
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
从上面的代码分析可知,公平模式和非公平模式的主要区别在于tryAcquireShared
方法中,公平模式需要判断线程需要排队,以同步队列的节点顺序获取共享资源。总结一下acquire
方法,如果不传入许可证数量,默认获取1个,否则,按照传入数量获取,不过需要先判断传参是否合法,对于公平模式,还需要判断当前线程是否需要排队,做完这些之后,获取剩余的许可数量(当前数量减去获取数量),如果剩余数量小于0,说明资源不足提供,当前线程执行doAcquireSharedInterruptibly方法,否则,会自旋直到更新剩余容量成功。
前面说过,如果资源不足以提供当前线程获取,会执行doAcquireSharedInterruptibly
方法,该方法类似于独占锁的acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
,也是将线程包装成节点进行入队出队操作,代码如下:
/**
* Acquires in shared interruptible mode.
* @param arg the acquire argument
*/
//可以看一下它的注释,以共享可中断模式获取,当然还有以不可中断XXX,方法为doAcquireShared,这两个方法大体上没有区别,只不过在处理中断时一个抛异常,一个修改标志位
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//将当前线程封装成共享模式的节点加入同步队列队尾,返回当前线程节点
final Node node = addWaiter(Node.SHARED);
//失败标志
boolean failed = true;
try {
//自旋
for (;;) {
//获取该节点的前一个节点p
final Node p = node.predecessor();
//如果p是同步队列的头节点,当前线程节点可以获取共享资源
if (p == head) {
//尝试获取资源,返回值是剩余资源数量
int r = tryAcquireShared(arg);
//如果有剩余资源或者资源刚好够获取
if (r >= 0) {
//将当前线程节点设置为头节点
setHeadAndPropagate(node, r);
//释放原来的头节点
p.next = null; // help GC
failed = false;
return;
}
}
//检查线程节点是否需要挂起,在前面AQS的独占锁模式已经讲过
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
//如果失败
if (failed)
//取消获取资源
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
//同步队列头节点
Node h = head; // Record old head for check below
//将当前节点设为头节点,清除当前节点的线程属性和前驱指针属性
setHead(node);
//如果共享资源有剩余
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
//获取新的头节点的下一个节点
Node s = node.next;
//判断释放需要释放共享资源
if (s == null || s.isShared())
doReleaseShared();
}
}
private void doReleaseShared() {
//自旋
for (;;) {
//获取头节点
Node h = head;
//头节点不是null且头节点不是尾节点
if (h != null && h != tail) {
//获取等待状态
int ws = h.waitStatus;
//如果等待状态是通知下个节点
if (ws == Node.SIGNAL) {
//cas设置状态等待状态,如果失败,continue继续执行
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;
}
}
release
方法的实现分为有参和无参两种,对于有参的release
方法,需要进行参数的验证,后续的方法都一样,都是调用的AQS的releaseShared
方法,代码如下:
public final boolean releaseShared(int arg) {
//需要通过tryReleaseShared方法的结果去判断
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
先看一下在Semaphore中tryReleaseShared
方法的实现,代码如下:
protected final boolean tryReleaseShared(int releases) {
//自旋
for (;;) {tryR
//当前的共享资源数量
int current = getState();
//释放后的资源数量
int next = current + releases;
//超出限制
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//cas更改state的值
if (compareAndSetState(current, next))
return true;
}
tryReleaseShared
方法执行成功后,才能执行释放资源的方法doReleaseShared
,代码如下:
private void doReleaseShared() {
//自旋
for (;;) {
//同步队列的头节点
Node h = head;
//同步队列不为空
if (h != null && h != tail) {
//头节点的等待状态
int ws = h.waitStatus;
//如果是通知下一个节点的状态
if (ws == Node.SIGNAL) {
//cas修改头节点的等待状态为初始状态
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//唤醒一个节点
unparkSuccessor(h);
}
//如果当前节点
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//检查到head节点没有变化时退出循环,head节点的变化是由线程唤醒引起的,说明当前没有线程再被唤醒
if (h == head) // loop if head changed
break;
}
}
该方法会尝试唤醒head的后继节点的线程,如果线程已经被唤醒,则仅设置PROPAGATE状态,上述操作都是cas完成的,cas失败后还会continue尝试,当检测到head节点不再发生变化,退出自旋。