Semaphore是什么?
Semaphore中文意思是信号量,也是一个线程并发的辅助类,Semaphore实现了线程同步框架AQS,它的本质是一个"共享锁",使用Semaphore可以控制同时访问资源的线程个数,但是不保证线程执行顺序。
Semaphore原理
Semaphore维护了有限数量的许可证,只有得到了许可证的线程才能进行共享资源的访问,如果得不到许可证,说明当前共享资源的访问已经达到最大限制,所以会挂起当前线程,直到前面的线程处理完任务之后,把许可证归还,后面排队的线程才有机会获取,然后处理任务。
Semaphore的构造及常用方法
构造方法:
Semaphore(int permits) //非公平模式指定最大允许访问许可证数量
Semaphore(int permits, boolean fair)//可以通过第二个参数控制是否使用公平模式
一些常用的方法:
acquire() //申请获取一个许可证,如果没有许可证,就阻塞直到能够获取或者被打断
availablePermits() // 返回当前有多少个有用的许可证数量hasQueuedThreads()//查询是否有线程正在等待获取许可证
drainPermits()//获得并返回所有立即可用的许可证数量
getQueuedThreads()//返回一个List包含当前可能正在阻塞队列里面所有线程对象
getQueueLength()//返回当前可能在阻塞获取许可证线程的数量
hasQueuedThreads()//查询是否有线程正在等待获取许可证
isFair()//返回是否为公平模式
reducePermits(int reduction)//减少指定数量的许可证
reducePermits(int reduction)//释放一个许可证
release(int permits)//释放指定数量的许可证
tryAcquire()//非阻塞的获取一个许可证
使用案例
Demo:模拟用户同时访问时,同时只能允许两个线程执行。
public class UseSemaphore {
public static void main(String[] args) {
// 线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 只能2个线程同时访问
final Semaphore semp = new Semaphore(2);
// 模拟12个客户端访问
for (int index = 0; index < 6; index++) {
final int NO = index;
Runnable run = new Runnable() {
public void run() {
try {
System.out.println("用户开始进入"+Thread.currentThread().getName());
// 获取许可
semp.acquire();
System.out.println("拿到进入许可 Accessing: " + Thread.currentThread().getName());
//模拟实际业务逻辑
Thread.sleep(5000);
// 访问完后,释放
System.out.println("我处理完事情了,释放许可:"+Thread.currentThread().getName());
semp.release();
} catch (InterruptedException e) {
}
}
};
exec.execute(run);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println(semp.getQueueLength());
// 退出线程池
exec.shutdown();
}
}
Semaphore底层原理:
Semaphore底层与CountDownLatch类似都是通过AQS的共享锁机制来实现的,指定的数量会设置到AQS里面的state里面,然后对于每一个 调用acquire方法线程,state都会减去一,如果state等于0,那么调用该方法的线程会被添加到同步队列里面,同时使用 LockSupport.park方法挂起等待,知道有线程调用了release方法,会对state加1,然后唤醒共享队列里面的线程,注意这里如果是 公平模式,就直接唤醒下一个等待线程即可,如果是非公平模式就允许新加入的线程与已有的线程进行竞争,谁先得到就是谁的,如果新加入的 竞争失败,就会走公平模式进入队列排队。
源码:
1.acquire函数--获取许可
先从获取一个许可看起,并且先看非公平模式下的实现。首先看acquire方法,acquire方法有几个重载,但主要是下面这个方法
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
从上面可以看到,调用了Sync的acquireSharedInterruptibly方法,该方法在父类AQS中,如下:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//如果线程被中断了,抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//获取许可失败,将线程加入到等待队列中
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
AQS子类如果要使用共享模式的话,需要实现tryAcquireShared方法,下面看NonfairSync的该方法实现:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
该方法调用了父类中的nonfairTyAcquireShared方法,如下:
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//获取剩余许可数量
int available = getState();
//计算给完这次许可数量后的个数
int remaining = available - acquires;
//如果许可不够或者可以将许可数量重置的话,返回
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
这里的释放就是对 state 变量减一(或者更多)的。
返回了剩余的 state 大小。
当返回值小于 0 的时候,说明获取锁失败了,那么就需要进入 AQS 的等待队列了。代码如下:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 添加一个节点 AQS 队列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 死循环
for (;;) {
// 找到新节点的上一个节点
final Node p = node.predecessor();
// 如果这个节点是 head,就尝试获取锁
if (p == head) {
// 继续尝试获取锁,这个方法是子类实现的
int r = tryAcquireShared(arg);
// 如果大于0,说明拿到锁了。
if (r >= 0) {
// 将 node 设置为 head 节点
// 如果大于0,就说明还有机会获取锁,那就唤醒后面的线程,称之为传播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 如果他的上一个节点不是 head,就不能获取锁
// 对节点进行检查和更新状态,如果线程应该阻塞,返回 true。
if (shouldParkAfterFailedAcquire(p, node) &&
// 阻塞 park,并返回是否中断,中断则抛出异常
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
// 取消节点
cancelAcquire(node);
}
}
总的逻辑就是:
创建一个分享类型的 node 节点包装当前线程追加到 AQS 队列的尾部。
如果这个节点的上一个节点是 head ,就是尝试获取锁,获取锁的方法就是子类重写的方法。如果获取成功了,就将刚刚的那个节点设置成 head。
如果没抢到锁,就阻塞等待。
看完了非公平的获取,再看下公平的获取,代码如下:
protected int tryAcquireShared(int acquires) {
for (;;) {
//如果前面有线程再等待,直接返回-1
if (hasQueuedPredecessors())
return -1;
//后面与非公平一样
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
从上面可以看到,FairSync与NonFairSync的区别就在于会首先判断当前队列中有没有线程在等待,如果有,就老老实实进入到等待队列;而不像NonfairSync一样首先试一把,说不定就恰好获得了一个许可,这样就可以插队了。
看完了获取许可后,再看一下release()方法。
2.release()函数--释放许可
释放许可也有几个重载方法,但都会调用下面这个带参数的方法
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
releaseShared方法在AQS中,如下:
public final boolean releaseShared(int arg) {
//如果改变许可数量成功
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
AQS子类实现共享模式的类需要实现tryReleaseShared类来判断是否释放成功,实现如下:
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改变许可数量成功,那么就会调用doReleaseShared()方法释放阻塞的线程
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 设置 head 的等待状态为 0 ,并唤醒 head 上的线程
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 成功设置成 0 之后,将 head 状态设置成传播状态
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
文章参考: