Semaphone

Semaphore,现在普遍翻译为“信号量”,以前也曾被翻译成“信号灯”,

信号量模型

信号量模型还是很简单的,可以简单概括为:一个计数器,一个等待队列,三个方法。在信 号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来 访问它们,这三个方法分别是:init()、down() 和 up()。

这三个方法详细的语义具体如下所示。

init():设置计数器的初始值。

down():计数器的值减 1;如果此时计数器的值小于 0,则当前线程将被阻塞,否则当前线程可以继续执行。

up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个 线程,并将其从等待队列中移除。

这里提到的 init()、down() 和 up() 三个方法都是原子性的,并且这个原子性是由信号量模 型的实现方保证的。在 Java SDK 里面,信号量模型是由 java.util.concurrent.Semaphore 实现的,Semaphore 这个类能够保证这三个方法都是原子操作。

如何使用

想想红绿灯。十字路口的红绿灯可以控制交通,得益于它的一个关键规则:车辆在通过路 口前必须先检查是否是绿灯,只有绿灯才能通行。这个规则和锁规则很类似。

其实,信号量的使用也是类似的。这里我们还是用累加器的例子来说明信号量的使用吧。在 累加器的例子里面,count+=1 操作是个临界区,只允许一个线程执行,也就是说要保证 互斥。那这种情况用信号量怎么控制呢?,就像我们用互斥锁一样,只需要在进入临界区之前执行一下 down() 操作,退出临界区之前执行一下 up() 操作就可以了。下面是 Java 代码的示例,acquire() 就是信号 量里的 down() 操作,release() 就是信号量里的 up() 操作。

1
2
3
4
5
6
7
8
9
10
11
12
static int count;
// 初始化信号量
static final Semaphore s =new Semaphore(1);
// 用信号量保证互斥
static void addOne(){
s.acquire();
try{
count+=1;
}finally{
s.release();
}
}

实现限流器

上面用信号量实现了简单的互斥锁,而实现互斥锁仅仅是Semaphore的部分功能,Semaphore 还可以允许多个线程访问一个临界区。

信号量的计数器,在上面的例子中,我们设置成了 1,这个 1 表示只允许一个线程进入临 界区,但如果我们把计数器的值设置成对象池里对象的个数 N,就能完美解决对象池的限 流问题了。

我们用一个 List来保存对象实例,用 Semaphore 实现限流器。关键的代码是 ObjPool 里 面的 exec() 方法,这个方法里面实现了限流的功能。在这个方法里面,我们首先调用 acquire() 方法(与之匹配的是在 finally 里面调用 release() 方法),假设对象池的大小是 10,信号量的计数器初始化为 10,那么前 10 个线程调用 acquire() 方法,都能继续执 行,相当于通过了信号灯,而其他线程则会阻塞在 acquire() 方法上。对于通过信号灯的线 程,我们为每个线程分配了一个对象 t(这个分配工作是通过 pool.remove(0) 实现的), 分配完之后会执行一个回调函数 func,而函数的参数正是前面分配的对象 t ;执行完回调 函数之后,它们就会释放对象(这个释放工作是通过 pool.add(t) 实现的),同时调用 release() 方法来更新信号量的计数器。如果此时信号量里计数器的值小于等于 0,那么说 明有线程在等待,此时会自动唤醒等待的线程。

简言之,使用信号量,我们可以轻松地实现一个限流器,使用起来还是非常简单的。