1 前言
在开发中,使用到了 java.util.concurrent
包下的 CountdownLatch
和 Semaphore
类,但是还不清楚两者的区别和联系。写下这篇文章把它弄清楚。
2 互斥操作
2.1 一个小例子
假设公司里只有一台饮水机,早上刚上班的时候。大家都会去接上一杯水。针对这一场景,我们用程序演示出来:
public class WaterMachine {
public void loadWater() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " enter");
System.out.println(Thread.currentThread().getName() + " 正在接水...");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " exit");
}
public static void main(String[] args) {
final WaterMachine waterMachine = new WaterMachine();
/**
* 早上刚上班,5 个人都去接水
*/
for (int i = 0; i < 5; i++) {
new Thread("#Staff" + i){
@Override
public void run() {
super.run();
try {
waterMachine.loadWater();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
}
运行一下程序,查看打印日志:
#Staff0 enter
#Staff0 正在接水...
#Staff4 enter
#Staff4 正在接水...
#Staff3 enter
#Staff3 正在接水...
#Staff2 enter
#Staff2 正在接水...
#Staff1 enter
#Staff1 正在接水...
#Staff3 exit
#Staff4 exit
#Staff0 exit
#Staff2 exit
#Staff1 exit
可以看到,打印出来的结果不符合预期。我们只有一台饮水机,大家接水只能一个一个来。换句话说,我们需要对饮水机的接水操作进行互斥操作。这该怎么办呢?
2.2 引入 Semaphore
Semaphore
是一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire()
,然后再获取该许可。每个 release()
添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore
只对可用许可的号码进行计数,并采取相应的行动。
Semaphore
通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。
在上面的例子中,因为只有一台饮水机,所以我们的许可数是 1。
首先,创建一个许可数为 1 的 Semaphore
对象:
Semaphore semaphore = new Semaphore(1);
其次,在进行接水操作前,从此信号量获取一个许可,在提供一个许可前一直将线程阻塞:
semaphore.acquire();
最后,在接水操作完毕后,释放一个许可,将其返回给信号量:
semaphore.release();
代码如下:
public class WaterMachine {
/**
* 定义许可数为 1 的信号量
*/
Semaphore semaphore = new Semaphore(1);
public void loadWater() throws InterruptedException {
// 请求许可
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " enter");
System.out.println(Thread.currentThread().getName() + " 正在接水...");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " exit");
// 释放许可
semaphore.release();
}
// 省略部分代码
...
}
运行一下程序,查看打印日志:
#Staff0 enter
#Staff0 正在接水...
#Staff0 exit
#Staff4 enter
#Staff4 正在接水...
#Staff4 exit
#Staff3 enter
#Staff3 正在接水...
#Staff3 exit
#Staff2 enter
#Staff2 正在接水...
#Staff2 exit
#Staff1 enter
#Staff1 正在接水...
#Staff1 exit
确实实现了一个一个接水的目的,对饮水机的互斥操作实现了。但是,有点不对头的地方:为什么职员0,职员1,职员2,职员3,职员4依次去接水,但结果顺序却是职员0,职员4,职员3,职员2,职员1?
这是因为初始化信号量时,默认的是非公平的公平设置。需要显式设置为公平的公平设置。修改初始化信号量的代码为:
/**
* 定义许可数为 1 的信号量, 采用公平的公平设置
*/
Semaphore semaphore = new Semaphore(1,true);
实际测试,却发现并不生效。此处先留下一个疑问。
2.3 引入 CountdownLatch
上面的例子如果使用 CountdownLatch
类,可以实现效果吗?
CountdownLatch
是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。用给定的计数 初始化 CountDownLatch
。由于调用了 countDown()
方法,所以在当前计数到达零之前,await
方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。
public class WaterMachine {
public static void main(String[] args) {
final WaterMachine waterMachine = new WaterMachine();
/**
* 早上刚上班,5 个人都去接水
*/
for (int i = 0; i < 5; i++) {
// 初始化一个计数为 1 的 CountdownLatch 对象
final CountDownLatch countDownLatch = new CountDownLatch(1);
Thread thread = new Thread("#Staff" + i) {
@Override
public void run() {
super.run();
try {
waterMachine.loadWater();
// 递减锁存器的计数
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
thread.start();
try {
// 使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void loadWater() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " enter");
System.out.println(Thread.currentThread().getName() + " 正在接水...");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " exit");
}
}
运行一下,查看日志:
#Staff0 enter
#Staff0 正在接水...
#Staff0 exit
#Staff1 enter
#Staff1 正在接水...
#Staff1 exit
#Staff2 enter
#Staff2 正在接水...
#Staff2 exit
#Staff3 enter
#Staff3 正在接水...
#Staff3 exit
#Staff4 enter
#Staff4 正在接水...
#Staff4 exit
可以看到使用 CountdownLatch
类,完美地达到了目标。职员们进入接水的顺序和实际执行的顺序是一致的。
3 访问内容池
3.1 一个小例子
周五下班了,公司几个人一起去一个很有口碑的小餐厅吃饭。但是,餐厅只有容纳 5 桌。早到的人可以直接有桌子,晚到的人只能等前面的人吃完又空桌了,才能有桌子。现在里面已经有 5 桌在用,这时有来了 3 组饭友。用程序来描述一下这个场景:
public class PopularCanteen {
private final List<Table> mTables = new ArrayList<>(5);
public PopularCanteen() {
// 小餐厅只有 5 张餐桌
mTables.add(new Table(1));
mTables.add(new Table(2));
mTables.add(new Table(3));
mTables.add(new Table(4));
mTables.add(new Table(5));
}
public static void main(String[] args) {
// 创建餐厅对象
final PopularCanteen popularCanteen = new PopularCanteen();
// 有 5 桌在用了
final CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread("#EarlyEater" + i) {
@Override
public void run() {
super.run();
countDownLatch.countDown();
// 占用一张桌子
Table table = null;
try {
table = popularCanteen.getTable();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 假定一桌吃饭要 3 秒,只是演示程序的。
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
popularCanteen.release(table);
}
}.start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这时,有来了 3 组饭友,需要 3 张桌子
for (int i = 0; i < 3; i++) {
new Thread("#LateEater" + i) {
@Override
public void run() {
super.run();
try {
Table table = popularCanteen.getTable();
if (table == null) {
return;
}
// 假定一桌吃饭要 3 秒,只是演示程序的。
Thread.sleep(3000);
popularCanteen.release(table);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
public Table getTable() throws InterruptedException {
Table table = null;
synchronized (mTables) {
if (mTables.isEmpty()) {
table = null;
} else {
table = mTables.remove(0);
}
System.out.println(Thread.currentThread().getName() + " get a table: " + table);
}
return table;
}
private void release(Table table) {
mTables.add(table);
System.out.println(Thread.currentThread().getName() + " release a table: " + table);
}
private class Table {
int id;
public Table(int id) {
this.id = id;
}
@Override
public String toString() {
return "Table{" +
"id=" + id +
'}';
}
}
}
运行一下,查看日志:
#EarlyEater0 get a table: Table{id=1}
#EarlyEater1 get a table: Table{id=2}
#EarlyEater2 get a table: Table{id=3}
#EarlyEater3 get a table: Table{id=4}
#EarlyEater4 get a table: Table{id=5}
#LateEater0 get a table: null
#LateEater1 get a table: null
#LateEater2 get a table: null
#EarlyEater3 release a table: Table{id=4}
#EarlyEater2 release a table: Table{id=3}
#EarlyEater1 release a table: Table{id=2}
#EarlyEater0 release a table: Table{id=1}
#EarlyEater4 release a table: Table{id=5}
看到后来的客人,并没有获取到早来的客人空出来的桌子。这没有实现我们的需求。
3.2 使用 Semaphore
直接看代码:
public class PopularCanteen {
private final List<Table> mTables = new ArrayList<>(5);
// 创建一个许可数为 5 的信号量
private final Semaphore mSemaphore = new Semaphore(5);
public PopularCanteen() {
// 小餐厅只有 5 张餐桌
mTables.add(new Table(1));
mTables.add(new Table(2));
mTables.add(new Table(3));
mTables.add(new Table(4));
mTables.add(new Table(5));
}
public static void main(String[] args) {
// 创建餐厅对象
final PopularCanteen popularCanteen = new PopularCanteen();
// 有 5 桌在用了
final CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread("#EarlyEater" + i) {
@Override
public void run() {
super.run();
countDownLatch.countDown();
// 占用一张桌子
Table table = null;
try {
table = popularCanteen.getTable();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 假定一桌吃饭要 3 秒,只是演示程序的。
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
popularCanteen.release(table);
}
}.start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这时,有来了 3 组饭友,需要 3 张桌子
for (int i = 0; i < 3; i++) {
new Thread("#LateEater" + i) {
@Override
public void run() {
super.run();
try {
Table table = popularCanteen.getTable();
if (table == null) {
return;
}
// 假定一桌吃饭要 3 秒,只是演示程序的。
Thread.sleep(3000);
popularCanteen.release(table);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
}
}
public Table getTable() throws InterruptedException {
// 从信号量获取许可,在提供一个许可前一直将线程阻塞,否则线程被中断。
mSemaphore.acquire();
Table table = null;
synchronized (mTables) {
if (mTables.isEmpty()) {
table = null;
} else {
table = mTables.remove(0);
}
System.out.println(Thread.currentThread().getName() + " get a table: " + table);
}
return table;
}
private void release(Table table) {
mTables.add(table);
System.out.println(Thread.currentThread().getName() + " release a table: " + table);
// 释放一个许可,将其返回给信号量。
mSemaphore.release();
}
private class Table {
int id;
public Table(int id) {
this.id = id;
}
@Override
public String toString() {
return "Table{" +
"id=" + id +
'}';
}
}
}
运行程序,查看日志:
#EarlyEater1 get a table: Table{id=1}
#EarlyEater0 get a table: Table{id=2}
#EarlyEater4 get a table: Table{id=3}
#EarlyEater3 get a table: Table{id=4}
#EarlyEater2 get a table: Table{id=5}
#EarlyEater1 release a table: Table{id=1}
#LateEater0 get a table: Table{id=1}
#EarlyEater0 release a table: Table{id=2}
#LateEater2 get a table: Table{id=2}
#EarlyEater4 release a table: Table{id=3}
#EarlyEater3 release a table: Table{id=4}
#LateEater1 get a table: Table{id=3}
#EarlyEater2 release a table: Table{id=5}
#LateEater0 release a table: Table{id=1}
#LateEater2 release a table: Table{id=2}
#LateEater1 release a table: Table{id=3}
可以看到,在 #EarlyEater1 离开 table 1之后,#LateEater0 就获取到了 table 1。后面的客人也同样获取到了空出来的桌子。这样,就实现了需求。
3.3 使用 CountdownLatch
思考一下:如果使用 CountdownLatch
的话,能不能实现上面的需求呢?
自己思考了一下,也在代码中尝试了一番,觉得不能实现。
4 总结
通过上面的例子,得出 CountDownLatch
和 Semaphore
的关系:
区别:
CountdownLatch
用于实现线程间的等待:某个线程A等待若干个其他线程执行完任务之后,它才执行。处理的是线程和线程之间执行顺序的问题。Semaphore
用于控制使用某组资源的线程的数目,处理的是多个线程和有限的资源之间的问题;而CountdownLatch
却没有这项功能。
联系
- 当两者用于控制互斥操作时,发挥的作用几乎一样,但是执行顺序控制上,
CountdownLatch
比较好。
- 当两者用于控制互斥操作时,发挥的作用几乎一样,但是执行顺序控制上,
参考
1.Java 并发专题 : Semaphore 实现 互斥 与 连接池
2.What is a semaphore?
3.Java并发编程:CountDownLatch、CyclicBarrier和Semaphore