文章目录
1.SemaPhore概述
在之前的文中中提到了并发编程使用到的锁,不管是synchronized
还是ReentrantLock
,其目的都是为了限制线程对资源的访问,从而起到保证线程安全的作用。
信号量SemaPhore
也是在限制多个线程访问共享资源,它与锁(这里指互斥锁)的区别在于,锁限制了只能有一个线程可以访问到共享资源,而信号量可以根据需要来配置多个线程访问共享资源,例如配置的信号量为10,则有10个线程可以在临界区内活跃,如果将信号量配置为1,也可以到达和互斥锁类似的相关。
1.1.共享锁与独占锁
信号量Semaphore
实际上是一个共享锁,它与独占锁的区别也比较容易理解,顾名思义,独占锁就是只有一个线程可以独占的锁,而共享锁可以让多个线程获取。Semaphore
是通过AQS
的共享模式来实现的。
1.2.Semaphore的作用
Semaphore
最主要的作用就是用来做流量控制,也就是限流。
在《Java并发编程的艺术》中写到一个数据库连接的例子,我们需要从数据库中读取几万个文件的数据,对于这种IO密集型的操作,我们可以启动几十个线程做一个并发的操作来加快读取效率,但是数据库的连接只有10个,为了避免出现无法获取数据库连接的异常,我们就可以使用Semaphore
限制访问的线程数量为10个,达到限流的效果。
1.3.Semaphore的使用
Semaphore
的基本使用非常简单,就是使用acquire()
和release()
方法将需要限制线程数量的代码逻辑包裹起来就可以了,平时使用这两个方法就已经足够了,下面写了个简单的Demo来验证对线程数量的限制是否生效:
public class SemaphoreDemo {
public static void test(Semaphore semaphore) {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+":enter");
Thread.sleep(1000L);
System.out.println(Thread.currentThread().getName()+":exit");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2);
for (int i = 0; i < 6; i++) {
new Thread(() -> SemaphoreDemo.test(semaphore), "线程" + i).start();
}
}
}
当前的信号量为2,那预估的结果一定是有两个线程打印出enter后,至少有一个线程打印出exit,才会有新的线程打印出enter。
实际打印结果如下:
线程0:enter
线程1:enter
线程1:exit
线程0:exit
线程2:enter
线程5:enter
线程5:exit
线程2:exit
线程4:enter
线程3:enter
线程4:exit
线程3:exit
可以看到线程访问的顺序虽然是乱序的,但一定是有线程退出了临界区才会有新的线程进入临界区。
2.Semaphore的实现原理
Semaphore
是通过AQS
的共享模式实现的,它使用了state
变量来限制进入临界区的线程数量,每个线程调用了acquire()
方法就表示进入了临界区,此时state
递减1,在state
减为0后,就不会再有新的线程进入临界区了,然后执行完临界区的代码逻辑后,调用release()
方法此时state
递增1。
2.1.Semaphore实例化
和其它使用AQS
实现的JUC
工具类一样,Semaphore
也有一个叫Sync
的内部类继承了AQS
,同时它有两个子类分别对应公平锁和非公平锁,在之前的文章中提到了,公平锁和非公平锁区别只在于新的线程获取锁时是否会做一次抢锁的操作,如果是就是非公平锁。在这里就以非公平锁来分析Semaphore
的实现原理。
Semaphore
通过构造方法进行实例化:
// 非公平锁
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
// 传入true则为公平锁
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
permits
翻译过来是许可的意思,这里创建Semaphore
实例时,就会指定一定数量的“许可证”,供后续使用。然后我们可以继续往里面看,看看permits
到底是什么东西。
abstract static class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) {
setState(permits);
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable{
private volatile int state;
protected final void setState(int newState) {
state = newState;
}
}
从上面的代码可以看出来,permits
就是AQS
中的state
。
2.2.Semaphore申请许可——acquire()
在实例化Semaphore
的时候创建了一定数量的“许可证”,线程只有在获取到“许可证”时才能进入到临界区执行代码,我们看看acquire()
里面做了什么。
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
里面调用了一个AQS
共享模式的通用方法,这里调用的是可以被中断等待的资源获取方法,在Semaphore
中还有另外一个不可中断的方法:
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
但不管是调用哪个方法,我们忽略掉是否可以中断的逻辑,它们都会去调用AQS
的模板方法tryAcquireShared
。
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 这一步是重点
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
这个模板方法是交给子类去实现的,不同的子类可以定义对state
值的不同操作方式,这里我们点进去,选择Semaphore
的非公平锁实现,最终实现如下:
final int nonfairTryAcquireShared(int acquires) {
// 无限循环+CAS ,典型的自旋锁实现
for (;;) {
int available = getState();
int remaining = available - acquires;
// remaning < 0 表示所有的“许可证”都被发完了
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
每个进入这个方法的线程都会使用CAS
将当前的state
减去一定的数量,如果是调用的acquire()
这里就是减1,在替换成功后或计算值减到-1
后就会返回当前计算的状态。
这个返回值会和0做比较if (tryAcquireShared(arg) < 0)
,如果判断为false
,当前线程就成功的进入临界区执行代码逻辑了。如果判断为true
,则会调用AQS
的通用方法,将线程放入到队列中挂起。
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) {
// 被唤醒后去抢锁,这个方法是模板方法,选择Semaphore的实现
int 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);
}
}
看过之前CountdownLatch
的实现原理的同学,对这个代码可能会比较眼熟,这里就是AQS
的“三板斧”。
- 将当前线程包装成一个
Node
节点,加入到CLH
队列尾部。 - 修改前置节点状态为
signal
并将当前线程挂起。 - 被唤醒后,再次通过自己实现的
tryAcquireShared()
判断是否获取锁,获取锁后就退出自旋执行临界区代码,没获取到就再次挂起。
2.3.Semaphore归还许可——release()
与acquire()
一直,release()
也是调用的AQS
的公共方法:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
在doReleaseShared()
中,就是将CLH
中挂起的线程依次唤醒,让他们去抢锁,这个在之前的文中《(十六)栅栏CountDownLatch的使用及实现原理》2.2.线程唤醒的实现中做了分析,由于事先原理一致,这里就不做详细分析了。
调用doReleaseShared()
的条件就是tryReleaseShared(arg)
的返回值,这也是一个模板方法,我们去找到Semaphore
的实现:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
// 大于Integer.MAX_VALUEb
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
上面代码可以看出,在CAS
替换成功,也就是“许可令”成功归还后,就可以去唤醒CLH
中挂起的线程去竞争“许可令”,从而进入到临界区执行代码。
3.总结
Semaphore
是用来限制访问共享资源的线程数量的工具,它底层通过AQS
的特性来实现的。
- 使用
AQS
的state
字段值来限制线程数量。 - 使用
CAS
对state
的递增和递减做原子性操作。 - 对于超出限制数量的线程,将它们加入到
CLH
队列中,防止忙等现象。