信号量模型

信号量模型可以简单概括为:一个计数器,一个等待队列,三个方法

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

image-20230506215117555
  • init():设置计数器的初始值。

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

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

这里的 init()、down() 和 up() 三个方法都是原子性的,并且这个原子性是由信号量模型的实现方保证的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Semaphore {
// 计数器
int count;
// 等待队列
Queue queue;
// 初始化操作
Semaphore(int c) {
this.count = c;
}
void down() {
this.count--;
if (this.count < 0) {
// 将当前线程插入等待队列
// 阻塞当前线程
}
}
void up() {
this.count++;
if (this.count <= 0) {
// 移除等待队列中的某个线程T
// 唤醒线程T
}
}
}

如何使用信号量

信号量其实就是一个标记,可以理解为红绿灯。每次通过路口时,要先检测红绿灯,是绿灯才可以同行,否则不能通过。而上面的信号量可以理解为,每次进入这里时,要先检测以下count是否大于0,如果大于0,才可以执行,否则不能执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int count;
//初始化信号量
static final Semaphore s = new Semaphore(1);
//用信号量保证互斥
static void addOne() {
// 这里就相当于调用上面的down,来将count--,如果count < 0,线程就会被阻塞,无法执行。
s.acquire();
try {
count += 1;
} finally {
// 这里相当于调用up,相当于释放资源,将count++。
s.release();
}
}

注意,这里的acquire和release是原子性的。

快速实现一个限流器

这里的限流器的场景是:需要一个对象池,里面有N个对象可以被重复使用。当一个线程使用了其中的对象式时,该线程释放该对象之前,其他线程不允许使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ObjPool<T, R> {
final List<T> pool;
// 用信号量实现限流器
final Semaphore sem;
// 构造函数
ObjPool(int size, T t){
pool = new Vector<T>(){};
for (int i = 0; i < size; i++) {
pool.add(t);
}
sem = new Semaphore(size);
}

// 利用对象池的对象,调用func
R exec(Function<T,R> func) {
T t = null;
// 获取信号量,相当于进入临界资源的访问
sem.acquire();
try {
// 使用对象池中的对象
t = pool.remove(0);
return func.apply(t);
} finally {
// 使用完之后将对象归还
pool.add(t);
sem.release();
}
}
}
// 创建对象池
ObjPool<Long, String> pool = new ObjPool<Long, String>(10, 2);
// 通过对象池获取t,之后执行
pool.exec(t -> {
System.out.println(t);
return t.toString();
});

信号量可以允许多个线程同时访问临界资源,它的并发度相对于使用Lock来说要好一点。但是它无法同时唤醒多个线程去争抢锁,只能唤醒阻塞线程中的一个。

因为信号量模型是没有Condition的概念的,即阻塞线程被醒了直接就运行了而不会去检查此时临界条件是否已经不满足了,所以一次只能唤醒一个。即满足进去临界区域之后,就从所有阻塞的线程中选一个进去,如果选多个,这几个线程都会认为此时已经满足,并开始执行。事实是,唤醒多个之后,有一个线程开始执行后,其他的线程就不满足条件了,但由于不检查条件,他们还是会执行。

参考

《Java 并发编程实战》