介绍
semaphore是一个java多线程并发工具包里面的一个并发工具类. 它主要用于协调多线程下的线程之间的通信. 它非常适用于限流.
使用
- semaphore的使用很简单 , 我们模拟有一个线程池,最多只能同时被10个线程同时操作,如果超出10个, 就让后面的线程进行等待.
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;
/**
* 业务场景: 模仿一个数据库连接池, 最多只允许同时有10个线程在同时操作
*/
public class SemaphoneDemo {
/**
* 限定为只能同时10个线程使用
*/
static Semaphore use = new Semaphore(10);
/**
* 假设此队列为存放线程的线程池
*/
private static LinkedBlockingDeque<Object> list = new LinkedBlockingDeque<>();
static {
for (int i = 0; i < 10; i++) {
// 初始化数据库10个连接池
list.add(new Object());
}
}
/**
* 获取连接
*
* @return
*/
public static Object takeConnection() throws InterruptedException {
// 获取许可
use.acquire();
Object o = list.removeFirst();
if (o == null) {
System.out.println("获取线程池出现错误");
}
return o;
}
/**
* 释放连接
*/
public static void releaseConnection(Object object) {
list.addLast(object);
/// 释放许可
use.release();
System.out.println("当前有" + use.getQueueLength() + "个线程等待数据库连接!!"
+ "可用连接数:" + use.availablePermits() + "当前集合数量为:" + list.size());
}
public static void main(String[] args) throws InterruptedException {
// 使用20个线程不断从连接池获取连接
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 20; j++) {
Object o = null;
try {
o = takeConnection();
// 执行业务代码
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
releaseConnection(o);
}
}
}).start();
}
Thread.sleep(3000);
System.out.println("main---------当前有" + use.getQueueLength() + "个线程等待数据库连接!!"
+ "可用连接数:" + use.availablePermits() + "当前集合数量为:" + list.size());
}
结果分析
上述程序非常简单, 在获取连接的时候, 调用了semaphore的acquire()方法, 在归还连接的时候调用了semaphore的release()方法. 就实现了简单的限流. 只能最多有10个线程获取连接.
疑问
- 它是如何实现阻塞的
- 它是如何实现释放资源的
如何实现
-
aqs中有一个Node类,这个类记住了上一个节点,下一个节点,头部和尾部, 这个就是aqs同步队列,如图
-
队列的加入和取出都使用了 cas操作保证了该队列在多线程先的安全问题
-
每次调用acquire方法时, 如果获取到许可, 就直接执行, 如果没有 , 就加入到同步队列的尾部进行, 然后进行阻塞
-
每次调用release方法时, 释放许可,并且唤醒到头部第一个线程,
-
执行流程
源码分析
- 类结构
AbstractQueuedSynchronizer 就是大名鼎鼎的aqs了
Sync继承自AbstractQueuedSynchronizer 实现同步锁功能
FairSync继承自Sync是公平锁的实现
NonfairSync继承自Sync是非公平的实现
- acquire方法分析, 做了什么
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
在类Semaphore中它调用acquireSharedInterruptibly(1)方法
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 线程被阻断, 抛出线程阻断异常
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取一次获取许可证, 获取到了直接通行,执行自己的业务代码
if (tryAcquireShared(arg) < 0)
// 没有获取, 则加入到aqs共享线程队列
doAcquireSharedInterruptibly(arg);
}
- tryAcquireShared 方法在Semaphore里面有两个实现, 一个是NonfairSync里的,一个是FairSync里的
// 调用了父类的初始化方法
NonfairSync(int permits) {
super(permits);
}
// 我们传的数字状态给到了state了 state的初始值就是我们设置的10
Sync(int permits) {
setState(permits);
}
// Semaphore 默认使用了非公平锁
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
// 最终调用了该方法来 尝试获取一次获取许可证
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
// 此if 会返回两种结果, 小于0 , 则是没有获取到许可证, 大于0 ,表示获取到了许可证
// compareAndSetState 如果不懂请百度 google
}
}
- 没有获取许可证的时候进入这个方法
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);
if (r >= 0) {
// 颁发成功, 修改头部,并且通知到其他线程
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 此处进行阻塞 parkAndCheckInterrupt
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed) // 此处代码取消该节点的状态
cancelAcquire(node);
}
}
// 把当前线程加入到aqs同步队列
private Node addWaiter(Node mode) {
// 把当前线程打包成为一个节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 获取队列中的尾部节点
Node pred = tail;
if (pred != null) {
// 把自己的上一个节点设置为队列尾部节点
node.prev = pred;
if (compareAndSetTail(pred, node)) { // 此处保证了线程安全
// 把尾部节点设置为当前节点
pred.next = node;
return node;
}
}
// 循环加入, 此处代码与本方法一致, 使用了死循环, 反正就是肯定可以加入成功
enq(node);
return node;
}
- 走到parkAndCheckInterrupt 这端代码就进行了阻塞, 我们看看release怎么去唤醒的
public void release() {
sync.releaseShared(1);
}
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;
}
}
// 光释放许可还不行, 还需要通知给其他程序, 现在锁已经是空闲状态,可以获取了
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;
}
}
总结
同步的实现关键借用了cas来实现, 而cas直接调用了cpu的指令, 来保证操作的同步性, 使用双向队列这个数据结构来存储线程,并且又使用了 LockSuppert工具类类进行唤醒和阻塞操作.
提出疑问
- 使用其他的数据结构是否可以超越双向队列的性能?