认识 Semaphore

Semaphore 是什么

Semaphore 一般译作 信号量,它也是一种线程同步工具,主要用于多个线程对共享资源进行并行操作的一种工具类。它代表了一种​​许可​​的概念,是否允许多线程对同一资源进行操作的许可,使用 Semaphore 可以控制并发访问资源的线程个数。

Semaphore 的使用场景

Semaphore 的使用场景主要用于​​流量控制​​,比如数据库连接,同时使用的数据库连接会有数量限制,数据库连接不能超过一定的数量,当连接到达了限制数量后,后面的线程只能排队等前面的线程释放数据库连接后才能获得数据库连接。

再比如交通公路上的红绿灯,绿灯亮起时只能让 100 辆车通过,红灯亮起不允许车辆通过。

再比如停车场的场景中,一个停车场有有限数量的车位,同时能够容纳多少台车,车位满了之后只有等里面的车离开停车场外面的车才可以进入。

用停车场的示例理解信号量_公平锁

Semaphore 使用

下面我们就来模拟一下停车场的业务场景:在进入停车场之前会有一个提示牌,上面显示着停车位还有多少,当车位为 0 时,不能进入停车场,当车位不为 0 时,才会允许车辆进入停车场。所以停车场有几个关键因素:停车场车位的总容量,当一辆车进入时,停车场车位的总容量 - 1,当一辆车离开时,总容量 + 1,停车场车位不足时,车辆只能在停车场外等待。

public class CarParking {


private static Semaphore semaphore = new Semaphore(10);


public static void main(String[] args){


for(int i = 0;i< 100;i++){


Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("欢迎 " + Thread.currentThread().getName() + " 来到停车场");
// 判断是否允许停车
if(semaphore.availablePermits() == 0) {
System.out.println("车位不足,请耐心等待");
}
try {
// 尝试获取
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 进入停车场");
Thread.sleep(new Random().nextInt(10000));// 模拟车辆在停车场停留的时间
System.out.println(Thread.currentThread().getName() + " 驶出停车场");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, i + "号车");


thread.start();
}


}


}

在上面这段代码中,我们给出了 Semaphore 的初始容量,也就是只有 10 个车位,我们用这 10 个车位来控制 100 辆车的流量,所以结果和我们预想的很相似,即大部分车都在等待状态。但是同时仍允许一些车驶入停车场,驶入停车场的车辆,就会 semaphore.acquire 占用一个车位,驶出停车场时,就会 semaphore.release 让出一个车位,让后面的车再次驶入。

Semaphore 实现原理初探:

Semaphore 是用来保护一个或者多个共享资源的访问,Semaphore 内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于 1,意味着有共享资源可以访问,则使其计数器值减去 1,再访问共享资源。

如果计数器值为 0, 线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加 1,之前进入休眠的线程将被唤醒并再次试图获得信号量。

就好比一个停车场管理员,站在门口,只有停车场有空位,就开门允许与空侧数量等量的人进入。多辆车进入停车场后,相当于 N 辆车来分配使用 N 个空位。为避免多辆车来同时竞争同一个车位,在内部仍然使用锁来控制资源的同步访问。

Java中的信号量和Linux中的思路是一样的

信号量的原理

信号量维护了一个信号量许可集。线程可以通过调用 acquire() 来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。线程可以通过 release() 来释放它所持有的信号量许可。

用停车场的示例理解信号量_公平锁_02


信号量模型

信号量模型比较简单,可以概括为:「一个计数器、一个队列、三个方法」

计数器:记录当前还可以运行多少个资源访问资源。

队列:待访问资源的线程

「三个方法」:

  • 「init()」:初始化计数器的值,可就是允许多少线程同时访问资源。
  • 「up()」:计数器加1,有线程归还资源时,如果计数器的值大于或者等于 0 时,从等待队列中唤醒一个线程
  • 「down()」:计数器减 1,有线程占用资源时,如果此时计数器的值小于 0 ,线程将被阻塞。

这三个方法都是原子性的,由实现方保证原子性。例如在 Java 语言中,JUC 包下的 Semaphore 实现了信号量模型,所以 Semaphore 保证了这三个方法的原子性。

Semaphore 的函数列表

// 创建具有给定的许可数和非公平的公平设置的 Semaphore。
Semaphore(int permits)
// 创建具有给定的许可数和给定的公平设置的 Semaphore。
Semaphore(int permits, boolean fair)


// 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
void acquire()
// 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。
void acquire(int permits)
// 从此信号量中获取许可,在有可用的许可前将其阻塞。
void acquireUninterruptibly()
// 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。
void acquireUninterruptibly(int permits)
// 返回此信号量中当前可用的许可数。
int availablePermits()
// 获取并返回立即可用的所有许可。
int drainPermits()
// 返回一个 collection,包含可能等待获取的线程。
protected Collection<Thread> getQueuedThreads()
// 返回正在等待获取的线程的估计数目。
int getQueueLength()
// 查询是否有线程正在等待获取。
boolean hasQueuedThreads()
// 如果此信号量的公平设置为 true,则返回 true。
boolean isFair()
// 根据指定的缩减量减小可用许可的数目。
protected void reducePermits(int reduction)
// 释放一个许可,将其返回给信号量。
void release()
// 释放给定数目的许可,将其返回到信号量。
void release(int permits)
// 返回标识此信号量的字符串,以及信号量的状态。
String toString()
// 仅在调用时此信号量存在一个可用许可,才从信号量获取许可。
boolean tryAcquire()
// 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。
boolean tryAcquire(int permits)
// 如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。
boolean tryAcquire(int permits, long timeout, TimeUnit unit)
// 如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。
boolean tryAcquire(long timeout, TimeUnit unit)

Semaphore 源码分析 (基于 JDK1.7.0_40)

Semaphore 是通过共享锁实现的。根据共享锁的获取原则,Semaphore 分为” 公平信号量” 和” 非公平信号量”。

“公平信号量” 和” 非公平信号量” 的区别

“公平信号量” 和” 非公平信号量” 的释放信号量的机制是一样的!不同的是它们获取信号量的机制:线程在尝试获取信号量许可时,对于公平信号量而言,如果当前线程不在 CLH 队列的头部,则排队等候;而对于非公平信号量而言,无论当前线程是不是在 CLH 队列的头部,它都会直接获取信号量。该差异具体的体现在,它们的 tryAcquireShared() 函数的实现不同。

用停车场的示例理解信号量_公平锁_03

“公平信号量” 类

static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;


FairSync(int permits) {
super(permits);
}


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;
}
}
}

“非公平信号量” 类

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);
}
}

基于JDK8

简单使用如下。

Semaphore semaphore = new Semaphore(10,true);//初始有10个资源,true 表示公平锁
semaphore.acquire();//获取,暂时获取不到会堵塞
semaphore.release();//释放

如果资源只有一个,类似于 ReentrantLock,但和 ReentrantLock 也有区别。具体在于 ReentrantLock 总是可重入的,可以重入若干次;而 Semaphore 并不区分是当前线程重入获取资源还是其他线程获取资源,所以重入也会减少 state,state 为 1 的话,说明不可重入。

在实现 AQS 的方法方面,共享锁实现需要自旋,而独占锁不需要。

tryAcquireShared

对于公平锁和非公平锁该方法的实现是不同的,公平锁会先看一下队列中是否有等待的线程,是的话则失败,否则不断自旋,只要资源够,就能成功;非公平锁每次不会看前面是否有线程,而是直接抢。

// FairSync
protected int tryAcquireShared(int acquires) {
for (;;) {
//公平锁在队列中不能有前面的节点,否则失败
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
// 有两种情况会返回,一种是资源不够,另一种是 CAS 成功了。
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}




// NonfairSync
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
// Sync
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
// 有两种情况会返回,一种是资源不够,另一种是 CAS 成功了。
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}

tryReleaseShared

在不溢出的情况下,不断自旋执行 CAS,直到某次成功后返回 true。

protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
// 如果 releases 总是正的,出现下面的情况只可能是溢出
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}

构造器和其他

// Semaphore
// 默认非公平锁
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
// 第二个参数表示是否是公平锁。
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}


// Sync
// 自旋清空资源,并返回清空前的值
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
// 和 tryAcquireShared 的区别是 next 可以为负数
final void reducePermits(int reductions) {
for (;;) {
int current = getState();
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
if (compareAndSetState(current, next))
return;
}
}

简单使用

下面这个例子中,资源只有两个,有10个线程,每个线程获取后会暂停5s然后释放,查看结果。

import java.util.concurrent.Semaphore;


public class B {
public static Semaphore semaphore = null;
public static void main(String[] args) throws Exception{
semaphore = new Semaphore(2,true);
Thread[] threads = new Thread[10];
for(int i=0;i<threads.length;i++){
threads[i] = new MyThread(semaphore,"线程"+i);
threads[i].start();
}
}
}


class MyThread extends Thread{
private Semaphore semaphore = null;
public MyThread(Semaphore semaphore,String name){
super(name);
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(getName()+"已获取资源");
Thread.sleep(5000);
} catch(Exception e){
e.printStackTrace();
}finally {
semaphore.release();
System.out.println(getName()+"已释放资源");
}
}
}



关注公众号 soft张三丰 

用停车场的示例理解信号量_数据库连接_04