典型回答
我们通常所说的并发包也就是java.util.concurrent及其子包,集中了Java并发的各种基础工具类,具体主要包括几个方面:
- 提供了比synchronized更加高级的各种同步结构,包括CountDownLatch、CyclicBarrier、Semaphore等,可以实现更加丰富的多线程操作。比如利用Semaphore作为资源控制器,限制同时进行工作的线程数量。
- 各种线程安全的容器,比如最常见的ConcurrentHashMap、有序的ConcurrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组CopyOnWriteArrayList等。
- 各种并发队列实现,如各种BlockingQueue实现,比较典型的ArrayBlockingQueue、SynchorousQueue或针对特定场景的PriorityBlockingQueue等。
- 强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行等。绝大部分情况下,不再需要自己从头实现线程池和任务调度器。
知识扩展
这部分内容比较多,将分为两讲。这一讲将重点介绍Semaphore,下一讲将介绍CountDownLatch和CyclicBarrier。
1、经典信号量Semaphore
Java提供了经典信号量Semaphore的实现,它通过控制一定数量的许可(permit)的方式,来达到限制通用资源访问的目的。你可以想象一下这个场景:在车站、机场等出租车时,当很多空出租车就位时,为防止过度拥挤,调度员指挥排队等待坐车的队伍一次进来5个人上车,等这5个人坐车出发,再放进去下一批。这和Semaphore的工作原理有些类似。
可以试试使用Semaphore来模拟实现这个调度过程:
import java.util.concurrent.Semaphore; public class UsualSemaphoreSample { public static void main(String[] args) throws InterruptedException { System.out.println("Action...GO!"); Semaphore semaphore = new Semaphore(5); for (int i = 0; i < 10; i++) { Thread t = new Thread(new SemaphoreWorker(semaphore)); t.start(); } } public static class SemaphoreWorker implements Runnable { private String name; private Semaphore semaphore; public SemaphoreWorker(Semaphore semaphore) { this.semaphore = semaphore; } @Override public void run() { try { log("is waiting for a permit!"); semaphore.acquire(); log("acquired a permit!"); log("executed!"); } catch (InterruptedException e) { e.printStackTrace(); } finally { log("released a permit!"); semaphore.release(); } } private void log(String msg) { if (name == null) { name = Thread.currentThread().getName(); } System.out.println(name + " " + msg); } } }
这段代码是比较典型的Semaphore示例,其逻辑是,线程试图获得工作许可,得到许可则进行任务,然后释放许可,这时等待许可的其它线程,就可获得许可进入工作状态,直到全部处理结束。编译运行,我们就能看到Semaphore的许可机制对工作线程的限制。
但是,从具体节奏看来,其实并不符合我们前面场景的需求,因为本例中Semaphore的用法实际是保证,一直有5个人可以试图乘车,如果有一个人出发了,立即就有排队的人获得许可,而这并不完全符合我们前面的需求。
那么,我们再修改一下,演示个非典型的Semaphore用法。
import java.util.concurrent.Semaphore; public class AbnormalSemaphoreSample { public static void main(String[] args) throws InterruptedException { Semaphore semaphore = new Semaphore(0); for (int i = 0; i < 10; i++) { Thread t = new Thread(new MyWorker(semaphore)); t.start(); } System.out.println("Action...GO!"); semaphore.release(5); System.out.println("Wait for permits off"); while (semaphore.availablePermits() != 0) { Thread.sleep(100L); } System.out.println("Action...GO again!"); semaphore.release(5); } static class MyWorker implements Runnable { private Semaphore semaphore; public MyWorker(Semaphore semaphore) { this.semaphore = semaphore; } @Override public void run() { try { semaphore.acquire(); System.out.println("Executed!"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
注意,上面的代码,更侧重的是演示Semaphore的功能以及局限性,其实有很多线程编程中的反实践。比如使用了sleep来协调任务执行,而且使用轮询调用availiablePermits来检测信号量获取情况。这都是很低效并且脆弱的,通常只是在测试或者诊断场景。
总的来说,我们可以看出Semaphore就是个计数器,其基本逻辑基于acquire/release,并没有太复杂的同步逻辑。
如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire进入互斥状态。本质上和互斥锁是非常相似的。但是区别也非常明显,比如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。
【完】