简单使用
基本使用:信号量,用来限制能同时访问共享资源的线程上限,达到限流的效果。
示例:
public static void main(String[] args) {
// 1. 创建 semaphore 对象
Semaphore semaphore = new Semaphore(3);
// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 3. 获取许可
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("running...");
sleep(1);
log.debug("end...");
} finally {
// 4. 释放许可
semaphore.release();
}
}).start();
}
}
输出
07:35:15.485 c.TestSemaphore [Thread-2] - running...
07:35:15.485 c.TestSemaphore [Thread-1] - running...
07:35:15.485 c.TestSemaphore [Thread-0] - running...
07:35:16.490 c.TestSemaphore [Thread-2] - end...
07:35:16.490 c.TestSemaphore [Thread-0] - end...
07:35:16.490 c.TestSemaphore [Thread-1] - end...
07:35:16.490 c.TestSemaphore [Thread-3] - running...
07:35:16.490 c.TestSemaphore [Thread-5] - running...
07:35:16.490 c.TestSemaphore [Thread-4] - running...
07:35:17.490 c.TestSemaphore [Thread-5] - end...
07:35:17.490 c.TestSemaphore [Thread-4] - end...
07:35:17.490 c.TestSemaphore [Thread-3] - end...
07:35:17.490 c.TestSemaphore [Thread-6] - running...
07:35:17.490 c.TestSemaphore [Thread-7] - running...
07:35:17.490 c.TestSemaphore [Thread-9] - running...
07:35:18.491 c.TestSemaphore [Thread-6] - end...
07:35:18.491 c.TestSemaphore [Thread-7] - end...
07:35:18.491 c.TestSemaphore [Thread-9] - end...
07:35:18.491 c.TestSemaphore [Thread-8] - running...
07:35:19.492 c.TestSemaphore [Thread-8] - end...
Semaphore还有一个构造函数Semaphore(int permits, boolean fair),fair表示是否创建公平或非公平的信号量
Semaphore应用
-
使用Semaphore限流,在访问高峰期,让请求线程阻塞,高峰期过去再释放许可。它只适合单机线程数量,而不能限制资源数(例如连接数,请对比Tomcat LimitLatch的实现)
-
用Semaphore实现简单连接池,对比享元模式下的实现(用wait notify),性能和可读性显然更好,注意下面的代码,线程数和数据库连接数是相同的
@Slf4j(topic = "c.Pool")
class Pool {
// 1. 连接池大小
private final int poolSize;
// 2. 连接对象数组
private Connection[] connections;
// 3. 连接状态数组 0 表示空闲, 1 表示繁忙
private AtomicIntegerArray states;
private Semaphore semaphore;
// 4. 构造方法初始化
public Pool(int poolSize) {
this.poolSize = poolSize;
// 让许可数与资源数一致
this.semaphore = new Semaphore(poolSize);
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("连接" + (i+1));
}
}
// 5. 借连接
public Connection borrow() {// t1, t2, t3
// 获取许可
try {
semaphore.acquire(); // 没有许可的线程,在此等待
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < poolSize; i++) {
// 获取空闲连接
if(states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
// 不会执行到这里
return null;
}
// 6. 归还连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
states.set(i, 0);
log.debug("free {}", conn);
semaphore.release();
break;
}
}
}
}
Semaphore原理
核心源码分析:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
非公平
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
代码逻辑:
- 线程进来nonfairTryAcquireShared后,会进行自旋,获取当前的state(本例是3)
- state减1,如果减去后的值小于0,意味加锁失败,如果大于0,就执行CAS操作试图修改state为之前减去后的值,因为修改state是并发的,所以这里用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;
}
}
代码逻辑:整体上和非公平的一样,主要区别是在加锁前判队列中是否有阻塞的线程
如果加锁失败就执行下面方法
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//创建SHARED类型的节点,并加入同步队列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//获取当前线程节点的前驱节点
final Node p = node.predecessor();
if (p == head) {
// 如果前驱节点是头节点就尝试获取锁
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//判断是否应该阻塞线程,第一次轮询的时候会把前去节点的waitStatus改为-1
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())//park住线程
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
线程索取所失败会执行上述自旋过程,被加入到同步队列中,节点类型为SHARED,同时修改前驱节点的waitStatus为-1,并在第二次轮询的时候park再parkAndCheckInterrupt方法等待其他线程释放锁是unpark唤醒
核心代码分析:
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;
}
}
释放锁的流程加锁的逆过程,state+1
释放锁成功后就执行下面方法
private void doReleaseShared() {
for (;;) {
Node h = head;//获取同步队列中的头节点
if (h != null && h != tail) {
int ws = h.waitStatus; //获取头节点的waitStatus
if (ws == Node.SIGNAL) { //如果是-1,就执行下面的操作(有加锁失败可知,头节点已经被修改为-1)
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//先把头节点的waitStatus改为0,防止当前线程释放锁的时候其他线程干扰
continue; // loop to recheck cases
unparkSuccessor(h); //唤醒头节点的后继结点,那之前阻塞在parkAndCheckInterrupt的后继节点对应的线程就会继续执行,尝试获取锁
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
同步队列中被唤醒的线程,如果获取锁失败,会继续阻塞