JAVA并发编程之Semnphore
1.是什么?
Semaphore 通常我们叫它信号量,通常用于多个共享资源的互斥使用和控制并发线程数
可以理解成抢车位,总共有十个车位,来一辆车占用一个,剩余车位数量减一,走一辆车,剩余车位就加一,如果十个车位全部被占满,后来的车只能等待,其他人把车开走。
2.使用场景
适用于资源复用的场景,比如数据库连接池
3.怎么用
以抢车位为例:
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);// 初始化信号量为3个停车位
for(int i = 1; i <= 6; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
semaphore.acquire(); // 占车位
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"抢到车位");
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
// System.out.println(Thread.currentThread().getName()+"占用车位3s");
semaphore.release(); // 释放车位
System.out.println(Thread.currentThread().getName()+"离开车位");
}
}, String.valueOf(i)).start();
}
}
}
4.底层原理
①new Semaphore(int permits);创建一个sempare(),并传入共享资源的数量
public Semaphore(int permits) {
// 底层new了一个非公平锁,并传入令牌数量(可以通过传入true、false指定是否公平),设置同步状态为permits
sync = new NonfairSync(permits);
}
②NonfairSync作为semaphore的静态内部类存在
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
③调用acquire()占用一个令牌
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 尝试占用共享资源(此方法使用了aqs中模板设计模式,前面分析过),tryAcquireShared(arg)最终会调用非公平锁的nonfairTryAcquireShared方法,去抢占令牌,如果抢占成功,拿着令牌继续执行,
if (tryAcquireShared(arg) < 0)
// 如果抢占失败(没有令牌了),就以共享模式进入阻塞队列,继任的节点尝试再次抢占,失败就阻塞,其他节点阻塞等待被唤醒(aqs框架提供的入队逻辑,前面分析过,不再赘述)
doAcquireSharedInterruptibly(arg);
}
nonfairTryAcquireShared(int acquires)方法的主要逻辑就是只要还有令牌,就尝试去抢占(所以才是非公平的),一旦令牌没有了,才结束循环
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
// 用可使用的令牌数量减去占用的数量,获得占用后剩余令牌数
int remaining = available - acquires;
// 如果剩余令牌数小于0,说明令牌不足没法占用,直接返回剩余令牌数
// 如果剩余令牌数>=0,也就是说令牌可以被占用,采用cas更新可被使用的令牌数量(同步状态),【多线程情况下可能被加塞(其他线程优先抢占了令牌),所以才使用自旋的方式去修改】
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
总结:==重要==
semaphore(共享锁)作为AQS框架的实现,也是只需要编写自己的如何占用和释放资源的业务逻辑就可以了,那么通过 nonfairTryAcquireShared(int acquires)方法看出,它并没有可重入的概念,如果多次调用acquire()方法,就会多次尝试占用令牌
问:semaphore初始化有10个令牌,一个线程重复调用11次acquire方法,会发生什么?
线程阻塞,不会继续往下运行。令牌没有重入的概念。你只要调用一次acquire方法,就需要有一个令牌才能继续运行。
④release()释放令牌
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 如果释放令牌成功,就执行唤醒工作
if (tryReleaseShared(arg)) {
// (之前在已经分析过)唤醒后继节点
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared(arg)
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
// 获取当前可提供的令牌数+要释放令牌数
int next = current + releases;
// 如果next<current,说明传入的释放数releases为负数,抛出异常
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
// 否则尝试修改令牌数为释放后的令牌数,修改成功返回true,结束方法,失败自旋,继续尝试修改
if (compareAndSetState(current, next))
return true;
}
}
总结:==重要==
我们通过tryReleaseShared(int releases)方法,可以看到,只要release一次令牌就会加+1个,即使已经超过了我们最初设置的令牌数
问:semaphore初始化有1个令牌,1个线程调用一次acquire方法,然后调用两次release方法,之后另外一个线程调用acquire(2)方法,此线程能够获取到足够的令牌并继续运行吗?
能,原因是release方法会添加令牌,并不会以初始化的大小为准。
问:semaphore初始化有2个令牌,一个线程调用1次release方法,然后一次性获取3个令牌,会获取到吗?
能,原因是release会添加令牌,并不会以初始化的大小为准
如有问题欢迎指正