[ 本文主要从整体上介绍Java中的多线程技术,对于一些重要的基础概念会进行相对详细的介绍,若有叙述不清晰以及不合理的地方,希望大家指出,谢谢大家:) ]
一、为什么使用多线程
1. 并发与并行
2. 阻塞与非阻塞
typedef ssize_t int; typedef size_t unsigned; ssize_t read(int fd, void *buf, size_t n);
3. 多进程 vs 多线程
二、如何使用多线程
1. 线程执行模型
2. 创建一个新线程
(1)通过实现Runnable接口
class MyRunnable implements Runnable { ... public void run() { //这里是新线程需要执行的任务 } } Runnable r = new MyRunnable(); Thread t = new Thread(r);
(2)通过继承Thread类
class MyThread extends Thread { public void run() { //这里是线程要执行的任务 } }
t.start();
(3)两种方式的比较
既然有两种方式可以创建线程,那么我们该使用哪一种呢?首先,直接继承Thread类的方法看起来更加方便,但它存在一个局限性:由于Java中不允许多继承,我们自定义的类继承了Thread后便不能再继承其他类,这在有些场景下会很不方便;实现Runnable接口的那个方法虽然稍微繁琐些,但是它的优点在于自定义的类可以继承其他的类。
3. 线程的属性
(1)线程的状态
- New(新生):线程对象刚刚被创建出来;
- Runnable(可运行):在线程对象上调用start方法后,相应线程便会进入Runnable状态,若被线程调度程序调度,这个线程便会成为当前运行(Running)的线程;
- Blocked(被阻塞):若一段代码被线程A”上锁“,此时线程B尝试执行这段代码,线程B就会进入Blocked状态;
- Waiting(等待):当线程等待另一个线程通知线程调度器一个条件时,它本身就会进入Waiting状态;
- Time Waiting(计时等待):计时等待与等待的区别是,线程只等待一定的时间,若超时则不再等待;
- Terminated(被终止):线程的run方法执行完毕或者由于一个未捕获的异常导致run方法意外终止会进入Terminated状态。
后文中若不加特殊说明的话,我们会用阻塞状态统一指代Blocked、Waiting、Time Waiting。
(2)线程的优先级
void setPriority(int newPriority) //设置线程的优先级,可以使用系统提供的三个优先级常量 static void yield() //使当前线程处于让步状态,这样当存在其他优先级大于等于本线程的线程时,线程调度程序会调用那个线程
4. Thread类
Thread实现了Runnable接口,关于这个类的以下实例域需要我们了解:
private volatile char name[]; //当前线程的名字,可在构造器中指定 private int priority; //当前线程优先级 private Runnable target; //当前要执行的任务 private long tid; //当前线程的ID
Thread类的常用方法除了我们之前提到的用于启动线程的start外还有:
- sleep方法,这是一个静态方法,作用是让当前线程进入休眠状态(但线程不会释放已获取的锁),这个休眠状态其实就是我们上面提到过的Time Waiting状态,从休眠状态“苏醒”后,线程会进入到Runnable状态。sleep方法有两个重载版本,声明分别如下:
public static native void sleep(long millis) throws InterruptedException; //让当前线程休眠millis指定的毫秒数 public static native void sleep(long millis, int nanos) throws InterruptedException; //在毫秒数的基础上还指定了纳秒数,控制粒度更加精细
- join方法,这是一个实例方法,在当前线程中对一个线程对象调用join方法会导致当前线程停止运行,等那个线程运行完毕后再接着运行当前线程。也就是说,把当前线程还没执行的部分“接到”另一个线程后面去,另一个线程运行完毕后,当前线程再接着运行。join方法有以下重载版本:
public final synchronized void join() throws InterruptedException public final synchronized void join(long millis) throws InterruptedException; public final synchronized void join(long millis, int nanos) throws InterruptedException;
无参数的join表示当前线程一直等到另一个线程运行完毕,这种情况下当前线程会处于Wating状态;带参数的表示当前线程只等待指定的时间,这种情况下当前线程会处于Time Waiting状态。当前线程通过调用join方法进入Time Waiting或Waiting状态后,会释放已经获取的锁。实际上,join方法内部调用了Object类的实例方法wait,关于这个方法我们下面会具体介绍。
- yield方法,这是一个静态方法,作用是让当前线程“让步”,目的是为了让优先级不低于当前线程的线程有机会运行,这个方法不会释放锁。
- interrupt方法,这是一个实例方法。每个线程都有一个中断状态标识,这个方法的作用就是将相应线程的中断状态标记为true,这样相应的线程调用isInterrupted方法就会返回true。通过使用这个方法,能够终止那些通过调用可中断方法进入阻塞状态的线程。常见的可中断方法有sleep、wait、join,这些方法的内部实现会时不时的检查当前线程的中断状态,若为true会立刻抛出一个InterruptedException异常,从而终止当前线程。
以下这幅图很好的诠释了随着各种方法的调用,线程在不同的状态之间的切换(图片来源:http://www.cnblogs.com/dolphin0520/p/3920357.html):
5. wait方法与notify/notifyAll方法
(1)wait方法
wait方法是Object类中定义的实例方法。在指定对象上调用wait方法能够让当前线程进入阻塞状态(前提时当前线程持有该对象的内部锁(monitor)),此时当前线程会释放已经获取的那个对象的内部锁,这样一来其他线程就可以获取这个对象的内部锁了。当其他线程获取了这个对象的内部锁,进行了一些操作后可以调用notify方法来唤醒正在等待该对象的线程。
(2)notify/notifyAll方法
notify/notifyAll方法也是Object类中定义的实例方法。它俩的作用是唤醒正在等待相应对象的线程,区别在于前者唤醒一个等待该对象的线程,而后者唤醒所有等待该对象的线程。这么说比较抽象,下面我们来举一个具体的例子来说明以下wait和notify/notifyAll的用法。请看以下代码(转自Java并发编程:线程间协作的两种方式):
1 public class Test { 2 private int queueSize = 10; 3 private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize); 4 5 public static void main(String[] args) { 6 Test test = new Test(); 7 Producer producer = test.new Producer(); 8 Consumer consumer = test.new Consumer(); 9 10 producer.start(); 11 consumer.start(); 12 } 13 14 class Consumer extends Thread{ 15 16 @Override 17 public void run() { 18 consume(); 19 } 20 21 private void consume() { 22 while(true){ 23 synchronized (queue) { 24 while(queue.size() == 0){ 25 try { 26 System.out.println("队列空,等待数据"); 27 queue.wait(); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 queue.notify(); 31 } 32 } 33 queue.poll(); //每次移走队首元素 34 queue.notify(); 35 System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素"); 36 } 37 } 38 } 39 } 40 41 class Producer extends Thread{ 42 43 @Override 44 public void run() { 45 produce(); 46 } 47 48 private void produce() { 49 while(true){ 50 synchronized (queue) { 51 while(queue.size() == queueSize){ 52 try { 53 System.out.println("队列满,等待有空余空间"); 54 queue.wait(); 55 } catch (InterruptedException e) { 56 e.printStackTrace(); 57 queue.notify(); 58 } 59 } 60 queue.offer(1); //每次插入一个元素 61 queue.notify(); 62 System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size())); 63 } 64 } 65 } 66 } 67 }
以上代码描述的是经典的“生产者-消费者”问题。Consumer类代表消费者,Producer类代表生产者。在生产者进行生产之前(对应第48行的produce方法),会获取queue的内部锁(monitor)。然后判断队列是否已满,若满了则无法再生产,所以在第54行调用queue.wait方法,从而等待在queue对象上。(释放了queue的内部锁)此时生产者能够能够获取queue的monitor从而进入第21行的consume方法,这样一来它就会通过第33行的queue.poll方法进行消费,于是队列不再满了,接着它在第34行调用queue.notify方法来通知正在等待的生产者,生产者就会从刚才阻塞的wait方法(第54行)中返回。
同理,当队列空时,消费者也会等待(第27行)生产者来唤醒(第61行)。
await方法和signal/signalAll方法是wait方法和notify/notifyAll方法的升级版,在后文中会具体介绍它们与wait、notify/notifyAll之间的关系。
6. 如何保证线程安全
所谓线程安全,指的是当多个线程并发访问数据对象时,不会造成对数据对象的“破坏”。保证线程安全的一个基本思路就是让访问同一个数据对象的多个线程进行“排队”,一个接一个的来,这样就不会对数据造成破坏,但带来的代价是降低了并发性。
(1)race condition(竟争条件)
public class Counter { private long count = 0; public void add(long value) { this.count = this.count + value; } }
- 第一步,把count的值加载到寄存器中;
- 第二步,把相应寄存器的值加上value的值;
- 第三步,把寄存器的值写回count变量。
(2)锁对象
public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
我们来分别介绍下Lock接口中发方法:
- lock方法用来获取锁,在锁被占用时它会一直阻塞,并且这个方法不能被中断;
- lockInterruptibly方法在获取不到锁时也会阻塞,它与lock方法的区别在于阻塞在该方法时可以被中断;
- tryLock方法也是用来获取锁的,它的无参版本在获取不到锁时会立刻返回false,它的计时等待版本会在等待指定时间还获取不到锁时返回false,计时等待的tryLock在阻塞期间也能够被中断。使用tryLock方法的典型代码如下:
if (myLock.tryLock()) { try { … } finally { myLock.unlock(); } } else { //做其他的工作 }
- unlock方法用来释放锁;
- newCondition方法用来获取当前锁对象相关的条件对象,这个在下文我们会具体介绍。
Lock myLock = new ReentrantLock(); public void add(long value) { myLock.lock(); try { this.count = this.count + value; } finally { myLock.unlock(); } }
从以上代码可以看到,使用ReentrantLock对象来上锁时只需要先获取一个它的实例。然后通过lock方法进行上锁,通过unlock方法进行解锁。注意,我们使用了一个try-finally块,以确保即使发生异常也总是会解锁,不然其他线程会一直无法执行add方法。当一个线程执行完“myLock.lock()”时,它就获得了一个锁对象,这就相当于它给临界区上了锁,其他线程都无法进来,只有这个线程执行完“myLock.unlock()"时,释放了锁对象,其他线程才能再通过“myLock.lock()"获得锁对象,从而进入临界区。也就是说,当一个线程获取了锁对象后,其他尝试获取锁对象的线程都会被阻塞,进入Blocked状态,直至获取锁对象的线程释放了锁对象。
(3)条件对象
public void add(int value) { if (this.count > 5) { this.count = this.count + value; } }
public void add(int value) { myLock.lock(); try { while (counter.getCount() <= 5) { //等待直到大于5 } this.count = this.count + value; } finally { myLock.unlock(); } }
Condition enoughCount = myLock.newCondition();
(4)synchronized关键字
public synchronized void add(int value) { ... }
public void add(int value) { this.innerLock.lock(); try { ... } finally { this.innerLock.unlock(); } }
public synchronized void add(int value) { while (this.count <= 5) { wait(); } this.count += value; notifyAll(); }
- 不能中断一个正在试图获取锁的线程;
- 试图获取锁时不能设定超时;
- 每个锁仅有一个相关条件;
- 若我们需要多个线程进行读操作,应该使用实现了Lock接口的ReentrantReadWriteLock类,这个类允许多个线程同时读一个数据对象(这个类的使用后面会介绍);
- 当我们需要Lock/Condition的特性时,应该考虑使用它(比如多个条件还有计时等待版本的await函数);
- 一般场景我们可以考虑使用synchronized关键字,因为它的简洁性一定程度上能够减少出错的可能。关于synchronized关键字需要注意的一点是:synchronized方法或者synchronized代码块出现异常时,Java虚拟机会自动释放当前线程已获取的锁。
(5)同步阻塞
synchronized (obj) { //临界区 }
public class Counter { private Object lock = new Object(); synchronized (lock) { //临界区 } ... }
那么这种使用这种锁有什么好处呢?我们知道Counter对象只有一个内部锁,这个内部锁在同一时刻只能被一个对象持有,那么设想Counter对象中定义了两个synchronized方法。在某一时刻,线程A进入了其中一个synchronized方法并获取了内部锁,此时线程B尝试进去另一个synchronized方法时由于对象内部锁还没有被线程A释放,因此线程B只能被阻塞。然而我们的两个synchronized方法是两个不同的临界区,它们不会相互影响,所以它们可以在同一时刻被不同的线程所执行。这时我们就可以使用如上面所示的显式的锁对象,它允许不同的方法同步在不同的锁上。
(6)volatile域
(7)死锁
假设现在进程中只有线程A和线程B这两个线程,考虑下面这样一种情形:
线程A获取了counterA对象的内部锁,线程B获取了counterB对象的内部锁。而线程A只有在获取counterB的内部锁后才能继续执行,线程B只有在获取线程A的内部锁后才能继续执行。这样一来,两个线程在互相等待对方释放锁从而谁也没法继续执行,这种现象就叫做死锁(deadlock)。
除了以上情况,还有一种类似的死锁情况是两个线程获取锁后都不满足条件从而进入条件的等待集中,相互等待对方唤醒自己。
Java没有为解决死锁提供内在机制,因此我们只有在开发时格外小心,以避免死锁的发生。关于分析定位程序中的死锁,大家可以参考这篇文章:Java Deadlock Example and How to analyze deadlock situation
(8)读/写锁
public interface ReadWriteLock { /** * Returns the lock used for reading. * * @return the lock used for reading */ Lock readLock(); /** * Returns the lock used for writing. * * @return the lock used for writing */ Lock writeLock(); }
我们可以看到这个接口就定义了两个方法,其中readLock方法用来获取一个“读锁”,writeLock方法用来获取一个“写锁”。
ReentrantReadWriteLock类的使用步骤通常如下所示://构造一个ReentrantReadWriteLock对象 private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); //分别从中“提取”读锁和写锁 private Lock readLock = rwl.readLock(); private Lock writeLock = rwl.writeLock(); //对所有的Reader线程加读锁 readLock.lock(); try { //读操作可并发,但写操作会互斥 } finally { readLock.unlock(); } //对所有的Writer线程加写锁 writeLock.lock(); try { //排斥所有其他线程的读和写操作 } finally { writeLock.unlock(); }
在使用ReentrantReadWriteLock类时,我们需要注意以下两点:
- 若当前已经有线程占用了读锁,其他要申请写锁的线程需要占用读锁的线程释放了读锁才能申请成功;
- 若当前已经有线程占用了写锁,其他要申请读锁或写锁的线程都需要等待占用写锁的线程释放了写锁才能申请成功。
7. 阻塞队列
- add方法:添加一个元素。若队列已满,会抛出IllegalStateException异常。
- element方法:返回队列的头元素。若队列为空,会抛出NoSuchElementException异常。
- offer方法:添加一个元素,若成功则返回true。若队列已满,则返回false。
- peek方法:返回队列的头元素。若队列为空,则返回null。
- poll方法:删除并返回队列的头元素。若队列为空,则返回null。
- put方法:添加一个元素。若队列已满,则阻塞。
- remove方法:移除并返回头元素。若队列为空,会抛出NoSuchElementException。
- take方法:移除并返回头元素。若队列为空,则阻塞。
- LinkedBlockingQueue是一个基于链表实现的阻塞队列。默认容量没有上限,但也有可以指定最大容量的构造方法。它有的“双端队列版本”为LinkedBlockingDeque。
- ArrayBlockingQueue是一个基于数组实现的阻塞队列,它在构造时需要指定容量。它还有一个构造方法可以指定一个公平性参数,若这个参数为true,那么等待了最长时间的线程会得到优先处理(指定公平性参数会降低性能)。
- PriorityBlockingQueue是一个基于堆实现的带优先级的阻塞队列。元素会按照它们的优先级被移除队列。
public class BlockingQueueTest { private int size = 20; private ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(size); public static void main(String[] args) { BlockingQueueTest test = new BlockingQueueTest(); Producer producer = test.new Producer(); Consumer consumer = test.new Consumer(); producer.start(); consumer.start(); } class Consumer extends Thread{ @Override public void run() { while(true){ try { //从阻塞队列中取出一个元素 queue.take(); System.out.println("队列剩余" + queue.size() + "个元素"); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread{ @Override public void run() { while (true) { try { //向阻塞队列中插入一个元素 queue.put(1); System.out.println("队列剩余空间:" + (size - queue.size())); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
8. 执行器
ExecutorService newCachedThreadPool() //返回一个带缓存的线程池,该池在必要的时候创建线程,在线程空闲60s后终止线程 ExecutorService newFixedThreadPool(int threads) //返回一个线程池,线程数目由threads参数指明 ExecutorService newSingleThreadExecutor() //返回只含一个线程的线程池,它在一个单一的线程中依次执行各个任务
- 对于newCachedThreadPool方法返回的线程池:对每个任务,若有空闲线程可用,则立即让它执行任务;若没有可用的空闲线程,它就会创建一个新线程并加入线程池中;
- newFixedThreadPool方法返回的线程池里的线程数目由创建时指定,并一直保持不变。若提交给它的任务多于线程池中的空闲线程数目,那么就会把任务放到队列中,当其他任务执行完毕后再来执行它们;
- newSingleThreadExecutor会返回一个大小为1的线程池,由一个线程执行提交的任务。
Future<T> submit(Callable<T> task) Future<T> submit(Runnable task, T result) Future<?> submit(Runnable task)
- 调用Executors中相关方法构建一个线程池;
- 调用submit方法提交一个Runnable对象或Callable对象到线程池中;
- 若想要取消一个任务,需要保存submit返回的Future对象;
- 当不再提交任何任务时,调用shutdown方法。
关于线程池更加深入及详细的分析,大家可以参考这篇博文:http://www.cnblogs.com/dolphin0520/p/3932921.html
ScheduledFuture<V> schedule(Callable<V> task, long time, TimeUnit unit) ScheduledFuture<?> schedule(Runnable task, long time, TimeUnit unit) //以上两个方法预定在指定时间过后执行任务 SchedukedFuture<?> scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) //在指定的延迟(initialDelay)过后,周期性地执行给定任务 ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit) //在指定延迟(initialDelay)过后周期性的执行任务,每两个任务间的间隔为delay指定的时间
T invokeAny(Collection<Callable<T>> tasks) T invokeAny(Collection<Callable<T>> tasks, long timeout, TimeUnit unit)
List<Future<T>> invokeAll(Collection<Callable<T>> tasks) List<Future<T>> invokeAll(Collection<Callable<T>> tasks, long timeout, TimeUnit unit)
9. Callable与Future
public interface Callable<V> { V call() throws Exception; }
public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
- cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false(即如果取消已经完成的任务会返回false);如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
- isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
- isDone方法表示任务是否已经完成,若任务完成,则返回true;
- get()方法用来获取执行结果,这个方法会阻塞,一直等到任务执行完才返回;
- get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
也就是说Future提供了三种功能:
- 判断任务是否完成;
- 能够中断任务;
- 能够获取任务执行结果。
Future接口的实现类是FutureTask:
public class FutureTask<V> implements RunnableFuture<V>
FutureTask类实现了RunnableFuture接口,这个接口的定义如下:
public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); }
FutureTask类有如下两个构造器:
public FutureTask(Callable<V> callable) public FutureTask(Runnable runnable, V result)
FutureTask通常与线程池配合使用,通常会创建一个包装了Callable对象的FutureTask实例,并用submit方法将它提交到一个线程池去执行,我们可以通过FutureTask的get方法获取返回结果。
10. 同步容器与并发容器
(1)同步容器
Java中的同步容器指的是线程安全的集合类,同步容器主要包含以下两类:
- 通过Collections类中的相应方法把普通容器类包装成线程安全的版本;
- Vector、HashTable等系统为我们封装好的线程安全的集合类。
相比与并发容器(下面会介绍),同步容器存在以下缺点:
- 对于并发读访问的支持不够好;
- 由于内部多采用synchronized关键字实现,所以性能上不如并发容器;
- 对同步容器进行迭代的同时修改它的内容,会报ConcurrentModificationException异常。
关于同步容器更加详细的介绍请参考这里:http://www.cnblogs.com/dolphin0520/p/3933404.html
(2)并发容器
并发容器相比于同步容器,具有更强的并发访问支持,主要体现在以下方面:
- 在迭代并发容器时修改其内容并不会抛出ConcurrentModificationException异常;
- 在并发容器的内部实现中尽量避免了使用synchronized关键字,从而增强了并发性。
Java在java.util.concurrent包中提供了主要以下并发容器类:
- ConcurrentHashMap,这个并发容器是为了取代同步的HashMap;
- CopyOnWriteArrayList,使用这个类在迭代时进行修改不抛异常;
- ConcurrentLinkedQuerue是一个非阻塞队列;
- ConcurrentSkipListMap用于在并发环境下替代SortedMap;
- ConcurrentSkipSetMap用于在并发环境下替代SortedSet。
关于这些类的具体使用,大家可以参考官方文档及相关博文。通常来说,并发容器的内部实现做到了并发读取不用加锁,并发写时加锁的粒度尽可能小。
11. 同步器(Synchronizer)
java.util.concurrent包提供了几个帮助我们管理相互合作的线程集的类,这些类的主要功能和适用场景如下:
- CyclicBarrier:它允许线程集等待直至其中预定数目的线程到达某个状态(这个状态叫公共障栅(barrier)),然后可以选择执行一个处理障栅的动作。适用场景:当多个线程都完成某操作,这些线程才能继续执行时,或都完成了某操作后才能执行指定任务时。对CyclicBarrier对象调用await方法即可让相应线程进入barrier状态,等到预定数目的线程都进入了barrier状态后,这些线程就可以继续往下执行了
- CountDownLatch:允许线程集等待直到计数器减为0。适用场景:当一个或多个线程需要等待直到指定数目的事件发生。举例来说,假如主线程需要等待N个子线程执行完毕才继续执行,就可以使用CountDownLatch来实现,需要用到CountDownLatch的以下方法:
1 public void await() throws InterruptedException { }; //调用该方法的线程会进入阻塞状态,直到count值为0才继续执行 2 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //await方法的计时等待版本 3 public void countDown() { }; //将CountDownLatch对象count值(初始化时作为参数传入构造方法)减1
- Exchanger:允许两个线程在要交换的对象准备好时交换对象。适用场景:当两个线程工作在统一数据结构的两个实例上时,一个向实例中添加数据,另一个从实例中移除数据。
- Semaphore:允许线程集等待直到被允许继续运行为止。适用场景:限制同一时刻对某一资源并发访问的线程数,初始化Semaphore需要指定许可的数目,线程要访问受限资源时需要获取一个许可,当所有许可都被获取,其他线程就只有等待许可被释放后才能获取。
- SynchronousQueue:允许一个线程把对象交给另一个线程。适用场景:在没有显式同步的情况下,当两个线程准备好将一个对象从一个线程传递到另一个线程。
关于CountDownLatch、CyclicBarrier、Semaphore的具体介绍和使用示例大家可以参考这篇博文:Java并发编程:CountDownLatch、CyclicBarrier和Semaphore。
三、参考资料
- Java并发编程:Callable、Future和FutureTask
- Java并发编程:阻塞队列
- 《Java核心技术(卷一)》
- 《深入理解计算机系统(第二版)》
- Java并发编程:Lock