java.util.concurrent 包下的 CountdownLatch 和 Semaphore 的区别和联系

1 前言

在开发中,使用到了 java.util.concurrent 包下的 CountdownLatchSemaphore 类,但是还不清楚两者的区别和联系。写下这篇文章把它弄清楚。

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 总结

通过上面的例子,得出 CountDownLatchSemaphore 的关系:

  • 区别:

    • CountdownLatch 用于实现线程间的等待:某个线程A等待若干个其他线程执行完任务之后,它才执行。处理的是线程和线程之间执行顺序的问题。
    • Semaphore 用于控制使用某组资源的线程的数目,处理的是多个线程和有限的资源之间的问题;而 CountdownLatch 却没有这项功能。
  • 联系

    • 当两者用于控制互斥操作时,发挥的作用几乎一样,但是执行顺序控制上,CountdownLatch 比较好。

参考

1.Java 并发专题 : Semaphore 实现 互斥 与 连接池
2.What is a semaphore?
3.Java并发编程:CountDownLatch、CyclicBarrier和Semaphore

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值