├── LICENSE
├── README.md
├── dia
└── ProducerConsumer.dia
├── figures
├── ExecutionException.png
├── ProducerConsumer.png
└── RejectedExecutionHandler.png
├── markdown
├── ArrayBlockingQueue.md
├── Atomics.md
├── Concurrent Collections Overview.md
├── Executors_and_ThreadPoolExecutor.md
└── synchronized_and_Reentrantlock.md
└── source
└── src.zip
/LICENSE:
--------------------------------------------------------------------------------
1 | ------Java Concurrency Recipes------
2 | Author: CarpenterLee
3 | Name: 李豪
4 | Mail: hooleeucas@163.com
5 | URL: https://github.com/CarpenterLee/JCRecipes
6 |
7 | 欢迎转载,转载请注明出处,谢谢~~
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Java Concurrency Recipes
2 |
3 | # Authors
4 |
5 | | Name | Weibo Id | Blog | Mail |
6 | |:-----------|:-------------|:-------------|:-----------|
7 | | 李豪 |[@计算所的小鼠标](http://weibo.com/icttinymouse) | [CarpenterLee](http://www.cnblogs.com/CarpenterLee/) | hooleeucas@163.com |
8 |
9 | # Introduction
10 |
11 | “能够驾驭多核程序的,都是稀缺人才。” ——孔子
12 |
13 | 摩尔定律失效促使人们寻找多核/众核的解决方案,当下手机平板都已四核八核,更不用说动则多块CPU数十个核的服务器了。多核使得并发/并行程序大行其道,但是如何编写正确、有效的并行程序并非易事。跟单核程序不同,多核程序牵涉到合理的任务划分,以及线程之间的同步、竞争等问题。糟糕的多核程序效率可能还不如单核程序,更可怕的是还可能出现死锁、状态错误等严重问题。
14 |
15 | 为简化并发编程,Java语言为我们提供很多工具类。本系列文章对Java并发相关的常见工具类给出介绍,让读者快速对Java并发编程建立一个简洁而深入的认识。为了不把读者吓走,本系列文章从将从现实中常遇到的问题出发,尽可能的围绕实际问题进行介绍。如果想全面了解Java并发编程,建议阅读相关书籍。
16 |
17 | # Contents
18 |
19 | 具体内容安排如下:
20 |
21 | 1. [Atomics](./markdown/Atomics.md),介绍常见原子变量。
22 | 2. [synchronized and Reentrantlock](./markdown/synchronized_and_Reentrantlock.md),介绍内置锁和显式锁的区别。
23 | 3. [Concurrent Collections Overview](/markdown/Concurrent%20Collections%20Overview.md),介绍常见并发容器。
24 | 4. [ArrayBlockingQueue](./markdown/ArrayBlockingQueue.md),结合ArrayBlockingQueue的源码讲解生产者-消费者模式的实现原理。
25 | 5. [Executors and ThreadPoolExecutor](./markdown/Executors_and_ThreadPoolExecutor.md) 线程池的用法和注意点。
26 | 6. 显示锁
27 | 7. ...
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/dia/ProducerConsumer.dia:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CarpenterLee/JCRecipes/fef297a0dc1c064cb4b090ef3aa936534bc80723/dia/ProducerConsumer.dia
--------------------------------------------------------------------------------
/figures/ExecutionException.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CarpenterLee/JCRecipes/fef297a0dc1c064cb4b090ef3aa936534bc80723/figures/ExecutionException.png
--------------------------------------------------------------------------------
/figures/ProducerConsumer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CarpenterLee/JCRecipes/fef297a0dc1c064cb4b090ef3aa936534bc80723/figures/ProducerConsumer.png
--------------------------------------------------------------------------------
/figures/RejectedExecutionHandler.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CarpenterLee/JCRecipes/fef297a0dc1c064cb4b090ef3aa936534bc80723/figures/RejectedExecutionHandler.png
--------------------------------------------------------------------------------
/markdown/ArrayBlockingQueue.md:
--------------------------------------------------------------------------------
1 | # ArrayBlockingQueue
2 |
3 | 本节我们介绍ArrayBlockingQueue的实现原理,学完本节你将学会如何手动实现一个生产者-消费者队列,并对Java显式锁(ReentrantLock)的使用有深入理解。
4 |
5 | ## 前言
6 |
7 | `ArrayBlockingQueue`是一种有界的`BlockingQueue`,常用于生产者-消费者模式,能够容纳指定个数的元素,当队列满时不能再放如新的元素,当队列为空时不能取出元素,此时相应的生产者或者消费者线程往往会挂起。本类是一种先进先出(FIFO)的队列结构,内部通过数组实现。来回顾一下`BlockingQueue`的常见接口:
8 |
9 |
| 抛异常 | 返回特殊值 | 阻塞 | 阻塞直到超时 |
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
删除 | remove() | poll() | take() | poll(time, unit) |
查看头部 | element() | peek() | 无 | 无 |
10 |
11 | 我们着重关注阻塞的`put()`和`take()`方法的实现。
12 |
13 | ## 显式锁和内置锁
14 |
15 | 解读ArrayBlockingQueue源码之前我们有必要区分一下Java中的显式锁和内置锁。
16 |
17 | ### 内置锁
18 |
19 | 我们都知道java中的synchronized关键字,用该关键字修饰方法可以保证方法是同步的,用该关键字和一个对象来包裹一个代码块,可以保证该代码块是同步的,出现synchronized的地方就是使用内置锁的地方,内置锁使用起来非常方便,不需要显式的获取和释放,任何一个对象都能作为一把内置锁。使用内置锁能够解决大部分的同步场景。
20 |
21 | ```Java
22 | // synchronized关键字用法示例
23 | public synchronized void add(int t){// 同步方法
24 | this.v += t;
25 | }
26 | public int decrementAndGet(){
27 | synchronized(this){// 同步代码块
28 | return --v;
29 | }
30 | }
31 | ```
32 |
33 | ### 显式锁
34 |
35 | 内置锁虽然好用,但它不可中断,不可定时,并且只有一个条件队列,有时候我们需要更灵活的获取锁机制,显式锁(RenentrantLock)应运而生。ReentrantLock是可重入的(线程可以同时多次请求同一把锁,而不会自己导致自己死锁)可定时、可中断并且支持多个条件队列。我们来分别解释这些概念。
36 |
37 | - 可中断:你一定见过InterruptedException,很多跟多线程相关的方法会抛出该异常,这个异常并不是一个缺陷导致的负担,而是一种必须,或者说是一件好事。可中断性给我们提供了一种让线程提前结束的方式(而不是非得等到线程执行结束),这对于要取消耗时的任务非常有用。对于内置锁,线程拿不到内置锁就会一直等待,除了获取锁没有其他办法能够让其结束等待。`RenentrantLock.lockInterruptibly()`给我们提供了一种以中断结束等待的方式。
38 |
39 | - 可定时:`RenentrantLock.tryLock(long timeout, TimeUnit unit)`提供了一种以定时结束等待的方式,如果线程在指定的时间内没有获得锁,该方法就会返回false并结束线程等待。
40 |
41 | - 条件队列(condition queue):线程在获取锁之后,可能会由于等待某个条件发生而进入等待状态(内置锁通过`Object.wait()`方法,显式锁通过`Condition.await()`方法),进入等待状态的线程会挂起并自动释放锁,这些线程会被放入到条件队列当中。synchronized对应的只有一个条件队列,而ReentrantLock可以有多个条件队列,多个队列有什么好处呢?请往下看。
42 |
43 | - 条件谓词:线程在获取锁之后,有时候还需要等待某个条件满足才能做事情,比如生产者需要等到“缓存不满”才能往队列里放入消息,而消费者需要等到“缓存非空”才能从队列里取出消息。这些条件被称作条件谓词,线程需要先获取锁,然后判断条件谓词是否满足,如果不满足就不往下执行,相应的线程就会放弃执行权并自动释放锁。使用同一把锁的不同的线程可能有不同的条件谓词,如果只有一个条件队列,当某个条件谓词满足时就无法判断该唤醒条件队列里的哪一个线程;但是如果每个条件谓词都有一个单独的条件队列,当某个条件满足时我们就知道应该唤醒对应队列上的线程(内置锁通过`Object.notify()`或者`Object.notifyAll()`方法唤醒,显式锁通过`Condition.signal()`或者`Condition.signalAll()`方法唤醒)。这就是多个条件队列的好处。
44 |
45 | 使用内置锁时,对象本身既是一把锁又是一个条件队列;使用显式锁时,RenentrantLock的对象是锁,条件队列通过`RenentrantLock.newCondition()`方法获取,多次调用该方法可以得到多个条件队列。
46 |
47 | 一个使用显式锁的典型示例如下:
48 |
49 | ```Java
50 | // 显式锁的使用示例
51 | ReentrantLock lock = new ReentrantLock();
52 |
53 | lock.lock();
54 | try{
55 | // do something
56 | }finally{
57 | lock.unlock();
58 | }
59 | ```
60 |
61 | 注意,上述代码将`unlock()`放在finally块里,这么做是必需的。显式锁不像内置锁那样会自动释放,使用显式锁一定要手动释放,如果获取锁后由于异常的原因没有释放锁,那么这把锁将永远得不到释放!所以要将unlock()放在finally块中,保证无论发生什么都能够正常释放。
62 |
63 | ## ArrayBlockingQueue.put()
64 |
65 | 有了上面的知识,理解阻塞队列的代码就变的很简单。
66 |
67 | `put(E e)`方法会以阻塞的方式向队列尾部放入元素,如果队列缓存不满就立即放入,否则挂起等待直到缓存不满,这里的谓词就是“缓存不满”,这是生产者要调用的方法。该方法的具体代码如下:
68 |
69 | ```Java
70 | final ReentrantLock lock = new ReentrantLock();// 显式锁对象
71 | private final Condition notFull = lock.newCondition();// put()方法的条件队列
72 | private final Condition notEmpty = lock.newCondition();// take()方法的条件队列
73 | ...
74 |
75 | public void put(E e) throws InterruptedException {
76 | ...
77 | lock.lockInterruptibly();
78 | try {
79 | while (count == items.length)// 条件谓词“缓存不满”
80 | notFull.await();// 挂起等待,直到缓存非满
81 | items[putIndex] = e;// 将元素放入缓存
82 | putIndex = inc(putIndex);
83 | ++count;
84 | notEmpty.signal();// 唤醒消费者线程
85 | } finally {
86 | lock.unlock();
87 | }
88 | }
89 | ```
90 |
91 | 上述代码首先创建了一个可重入锁,并通过调用两次`newCondition()`方法得到两个跟这把锁相关的条件队列,这两个条件队列分别对用生产者队列和消费者队列。
92 |
93 | put()方法的代码中首先以可中断的方式获取锁,之后在谓词“缓存不满”上等待,如果队列满了,就调用`notFull.await()`挂起当前线程并释放锁,这里说的释放锁是`await()`方法带来的效果,不是指最后finally代码块中的`unlock()`。当缓存不满的条件满足时,会将元素放到缓存当中,并调用`notEmpty.signal()`方法唤醒一个消费者线程。
94 |
95 | 代码中`notFull.await()`被放在了一个`while`循环而不是`if`语句中,这么做也是必需的。因为线程从`await()`语句中倍唤醒时,不一定意味着自己的条件谓词一定成立,有很多原因可以导致一个等待的线程被唤醒,条件谓词被满足只是其中一个。
96 |
97 | ## ArrayBlockingQueue.take()
98 |
99 | `take()`方法是以阻塞的方式获取队列首部的元素,如果队列缓存非空就立即取出,否则挂起等待直到队列非空,这里的谓词是“缓存非空”,这是消费者调用的方法。该方法具体代码如下:
100 |
101 |
102 | ```Java
103 | final ReentrantLock lock = new ReentrantLock();// 显式锁对象
104 | private final Condition notEmpty = lock.newCondition();// take()方法的条件队列
105 | private final Condition notFull = lock.newCondition();// put()方法的条件队列
106 | ...
107 |
108 | public E take() throws InterruptedException {
109 | lock.lockInterruptibly();
110 | try {
111 | while (count == 0)// 条件谓词“缓存非空”
112 | notEmpty.await();// 挂起等待,直到缓存非空
113 | E x = (E)items[takeIndex];// 取出元素
114 | items[takeIndex] = null;
115 | takeIndex = inc(takeIndex);
116 | --count;
117 | notFull.signal();// 唤醒生产者线程
118 | return x;
119 | } finally {
120 | lock.unlock();
121 | }
122 | }
123 | ```
124 |
125 | 上述`take()`方法首先以可中断的方式获取锁,之后在谓词“缓存非空”上等待,如果队列为空,就调用`notEmpty.await()`挂起当前线程并释放锁,当等待条件满足时,会从缓存中取出一个元素,并调用`notFull.signal()`唤醒一个生产者线程。
126 |
127 | 理解了`put()`和`take()`方法的实现,也就理解了实现生产者-消费者模式的精髓。
128 |
129 | ## 源码说明
130 |
131 | 本文采用的是JDK 1.7u79的源码,[下载地址](http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html)。[这里复制了一份](../source/src.zip)。
132 |
--------------------------------------------------------------------------------
/markdown/Atomics.md:
--------------------------------------------------------------------------------
1 | # Atomics
2 |
3 | 实现全局自增id最简单有效的方式是什么?`java.util.concurrent.atomic`包定义了一些常见类型的原子变量。这些原子变量为我们提供了一种操作单一变量无锁(*lock-free*)的线程安全(*thread-safe*)方式。实际上该包下面的类为我们提供了类似`volatile`变量的特性,同时还提供了诸如`boolean compareAndSet(expectedValue, updateValue)`的功能。不使用锁实现线程安全听起来似乎很不可思议,这其实是通过CPU的compare and swap指令实现的,由于硬件指令支持当然不需要加锁了。
4 |
5 | 先不去讨论这些细节,我们来看一下原子变量的用法。一个典型的用法是可以使用原子变量轻松实现全局自增id,就像下面这样:
6 |
7 | ```Java
8 | // 线程安全的序列id生成器
9 | class Sequencer {
10 | private final AtomicLong sequenceNumber = new AtomicLong(0);
11 | public long next() {
12 | return sequenceNumber.getAndIncrement();
13 | }
14 | }
15 | ```
16 |
17 | 上述代码利用AtomicLong创建了一个Sequencer类,不断调用该类的next()方法就可以得到线程安全的自增id,用起来非常简单直观。下面我们给出每种原子变量类型的用法说明。
18 |
19 | ## AtomicInteger and AtomicLong
20 |
21 | *AtomicInteger*和*AtomicLong*分别代表原子类型的整型和长整型,这两个类提供十分相似的功能,仅仅是位宽不同。如上例所示,原子整型可用于多线程下全局自增id,除此之外还提供了原子*比较-赋值*等操作,诸如`compareAndSet(expect, update)`, `decrementAndGet()`,`getAndDecrement()`,`getAndSet(newValue)`等等,更全面的接口描述可参考JDK文档。需要提醒的是这些函数都是通过原子CPU指令实现,执行效率较高。
22 |
23 | 原子整型看似跟普通整型(*Integer, Long*)类型相似,但不能使用原子整型替代普通整型,因为原子整型是可变的,而普通整型不可变。由于这个原因,使用原子整型作为Map的key并不是个好主意。
24 |
25 | 你可能会想当然的以为应该有*AtomicFloat*和*AtomicDouble*,遗憾的是类库里并没有这两个类型,*AtomicByte*和*AtomicShort*也没有。如果需要替代方案是使用*AtomicInteger*和*AtomicLong*。可通过`Float.floatToRawIntBits(float)`和`Float.intBitsToFloat(int)`将Float存储到*AtomicInteger*中,类似的Double类型也可以存储到*AtomicLong*中。
26 |
27 | ## AtomicReference
28 |
29 | *AtomicReference*用于存放一个可以原子更新的对象引用。该类包含`get()`, `set()`, `compareAndSet()`, `getAndSet()`等原子方法来获取和更新其代表的对象引用。
30 |
31 | ## AtomicXXXArray
32 |
33 | atomic包下面有三种原子数组:`AtomicIntegerArray`, `AtomicLongArra`, `AtomicReferenceArray`,分别代表整型、长整型和引用类型的原子数组。原子数组使得我们可以线程安全的方式去修改和访问数组里的单个元素。简单示例如下:
34 |
35 | ```Java
36 | // 原子数组示例
37 | AtomicLongArray longArray = new AtomicLongArray(10);// 创建长度为10的原子数组
38 | longArray.set(1, 100);
39 | long v = longArray.getAndIncrement(1);
40 |
41 | AtomicReferenceArray referenceArray = new AtomicReferenceArray<>(16);
42 | referenceArray.set(3, "love");
43 | referenceArray.compareAndSet(3, "love", "you");
44 | ```
45 |
46 | 简单来说原子数组就是一种支持线程安全的数组,仍然具有数组“定长”的性质,如果访问元素超过了数组的长度,将会抛出`IndexOutOfBoundsException`。你可能已经想到了,可以使用线程安全的容器来避免容量不足,我们会在后续章节介绍。
47 |
48 | ## 什么是线程安全?
49 |
50 | 线程安全是指多线程访问是时,无论线程的调度策略是什么,程序能够正确的执行。导致线程不安全的一个原因是状态不一致,如果线程A修改了某个共享变量(比如给id++),而线程B没有及时知道,就会导致B在错误的状态上执行,结果的正确性也就无法保证。原子变量为我们提供了一种保证单个状态一致的简单方式,一个线程修改了原子变量,另外的线程立即就能看到,这比通过锁实现的方式效率要高;如果要同时保证多个变量状态一致,就只能使用锁了。
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/markdown/Concurrent Collections Overview.md:
--------------------------------------------------------------------------------
1 | # Concurrent Collections Overview
2 |
3 | 生产者-消费者模型最简单的实现方式是什么?使用Java语言中的`BlockingQueue`是最简单有效的实现方式。本节我们将对Java并发容器给出介绍,完成我们在[《深入理解Java集合框架》](https://github.com/CarpenterLee/JCFInternals)系列文章中未竟的内容。
4 |
5 | ## BlockingQueue
6 |
7 | `BlockingQueue`是一个阻塞队列接口,所谓阻塞队列就是在添加元素或获取元素时,线程会阻塞等待直到队列不满或者非空。该接口常见实现类有`ArrayBlockingQueue`,`LinkedBlockingQueue`和`PriorityBlockingQueue`等,前两个分别是依靠数组和链表实现的阻塞队列,后一个是阻塞的优先队列。关于[数组](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/2-ArrayList.md)、[链表](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/3-LinkedList.md)以及[优先队列](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/8-PriorityQueue.md)数据结构方面的知识,[《深入理解Java集合框架》](https://github.com/CarpenterLee/JCFInternals)系列文章已经讲解的非常清楚,不再重复。此处主要考察阻塞队列的特点和用法,阻塞队列常见的接口方法如下表,不同方法对特殊情况的处理方式不同:
8 |
9 | | 抛异常 | 返回特殊值 | 阻塞 | 阻塞直到超时 |
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
删除 | remove() | poll() | take() | poll(time, unit) |
查看头部 | element() | peek() | 无 | 无 |
10 |
11 | 上表中后两列方法使用较多,因为我们使用阻塞队列显然是想发挥其阻塞的特性。
12 |
13 | BlockingQueue是线程安全的且不允许放入`null`值,常用于生产者-消费者模式的线程间数据共享。通常队列都会有一个固定大小,能够乘放指定个数个元素。当队列空间占满时,生产者将会挂起直到队列不满;当队列为空时,消费者将会挂起直到队列非空。一个简单而实用的例子如下:
14 |
15 | ```Java
16 | // 使用BlockingQueue实现生产者-消费者模式
17 | public class ProducerConsumer {
18 | public static void main(String[] args) {
19 | BlockingQueue queue = new ArrayBlockingQueue<>(16);// 固定容量为16的阻塞队列
20 | new Producer(queue).start();// Producer 0
21 | new Producer(queue).start();// Producer 1
22 | new Consumer(queue).start();// Consumer
23 | }
24 | static class Producer extends Thread{
25 | private BlockingQueue queue;
26 | public Producer(BlockingQueue queue){ this.queue = queue; }
27 | @Override
28 | public void run(){//不停生产元素,添加到共享队列当中
29 | while(true){
30 | try {
31 | E e = produce();
32 | queue.put(e);// 挂起,直到队列非满
33 | } catch (InterruptedException e1) { /* exception */ }
34 | }
35 | }
36 | protected E produce(){return (E)String.valueOf(System.nanoTime());}
37 | }
38 | static class Consumer extends Thread{
39 | private BlockingQueue queue;
40 | public Consumer(BlockingQueue queue){ this.queue = queue; }
41 | @Override
42 | public void run(){//不断从共享队列当中取出元素,并消费
43 | while(true){
44 | try {
45 | E e = queue.take();// 挂起,直到队列非空
46 | consume(e);
47 | } catch (InterruptedException e1) { /* exception */ }
48 | }
49 | }
50 | protected void consume(E e){System.out.println(e);}
51 | }
52 | }
53 | ```
54 |
55 | 上述代码使用ArrayBlockingQueue作为共享队列,实现了生产者-消费者模式,并指定了两个生产者和一个消费者,现实场景中生产者和消费者的数量都是不确定的,可以是多个,也可以暂时是零个。上述代码的示意图如下:
56 |
57 |
58 |
59 | ## BlockingDeque
60 |
61 | `BlockingDeque`是阻塞双端队列接口,所谓双端队列就是队列的首尾都可以添加或删除元素,这意味着双端队列既可以当作栈使用,也可以当作队列使用,`LinkedBlockingDeque`是该接口的唯一实现类。关于[双端队列接口介绍](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/4-Stack%20and%20Queue.md)可参考前文,跟阻塞队列类似,阻塞双端队列的方法也分为阻塞和非阻塞,具体[可参考JDK API](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/BlockingDeque.html),这里比在列举。跟BlockingQueue类似,阻塞双端队列也不允许放入`null`值,常用于多线程之间共享数据,如果对应到生产者-消费者模式上,就是生产者和消费者都可以产生或者消费数据。
62 |
63 | LinkedBlockqingDeque可以在构造是指定固定大小,如果没有指定,则默认大小为Integer.MAX_VALUE。
64 |
65 | ## ConcurrentLinkedQueue and ConcurrentLinkedDeque
66 |
67 | `ConcurrentLinkedQueue`是线程安全的队列,`ConcurrentLinkedDeque`是线程安全的双端队列,关于[Queue和Deque的接口说明](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/4-Stack%20and%20Queue.md)前面已经讲的非常清楚,不再赘述。这两个类都是基于链表的,并且没有容量限制,不允许放入`null`值;这两个类的迭代器都是弱一致的(weakly consistent),在迭代的过程中插入和删除元素并不会导致*ConcurrentModificationException*,并且插入和删除效果会直接体现在迭代器中。值得注意的是,跟大多数容器不同这两个类的size()操作并开销较大,因为需要遍历内部的所有元素(而不是通过一个计数器直接返回),由于遍历过程中也可能有元素的插入和删除操作,最后返回的大小不一定表示当前的实际大小。
68 |
69 | ## CopyOnWriteArrayList and CopyOnWriteArraySet
70 |
71 | `CopyOnWriteArraySet`内部通过`CopyOnWriteArrayList`实现,这里直接介绍CopyOnWriteArrayList,它是一个线程安全的ArrayList,内部通过数组实现,任何改变(add()或者set()等)都会导致产生一个新的内部数组(就像名字中Copy on Write暗示的那样)。你是不是觉得这样实现效率太低,因为如果元素修改频繁,就会导致大量的拷贝,毕竟哪怕只修改一个元素也会导致对所有元素的拷贝。事实确实如此,但该类定位于修改极少而迭代很多的场景,因为修改是在副本上进行的,对CopyOnWriteArrayList的迭代内部并不需要加锁,这使得迭代效率很高。
72 |
73 | `CopyOnWriteArraySet`内部通过`CopyOnWriteArrayList`实现,适合于集合很小并且迭代次数远远多于修改次数的场景。
74 |
75 | ## ConcurrentSkipListMap and ConcurrentSkipListSet
76 |
77 | 首先说明`ConcurrentSkipListSet`内部实现是对`ConcurrentSkipListMap`的包装,就像[参阅:HashSet和HashMap](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/6-HashSet%20and%20HashMap.md#hashset)的关系那样,所以这里只着重说一下ConcurrentSkipListMap是个什么东西。ConcurrentSkipListMap是一种基于跳表(skip list)的并发有序Map。跟[参阅:TreeMap](https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/5-TreeSet%20and%20TreeMap.md)类似ConcurrentSkipListMap是按照key值有序的,但内部不是通过红黑树实现,而是通过跳表实现。如果你需要一个按照key值排序且线程安全的Map,相比使用`Collectoins.synchronizedMap(TreeMap)`,ConcurrentSkipListMap显然是最佳选择。
78 |
79 | ## ConcurrentHashMap
80 |
81 | 终于轮到大名鼎鼎的`ConcurrentHashMap`登场了,它是线程安全的HashMap,用法跟HashMap一样。其内部采用分块加锁的形式来提高并发度,实现的非常巧妙很值得深入学习,我们会专门花一解单独介绍它的具体实现,敬请期待!
82 |
83 | ## 总结
84 |
85 | 本文介绍了Java常见的并发容器,使用这些并发容器能够简化编程,同时保证并发效率,当需要使用并发容器时,应首先考虑这里列举的类,而不是使用`Collections.synchronizedXXX()`对非并发容器进行包装。这么多工具类,总有一款适合你!
--------------------------------------------------------------------------------
/markdown/Executors_and_ThreadPoolExecutor.md:
--------------------------------------------------------------------------------
1 | # ThreadPoolExecutor
2 |
3 | 构造一个线程池为什么需要几个参数?如果避免线程池出现OOM?`Runnable`和`Callable`的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常见场景和代码片段。
4 |
5 | ## 基础知识
6 |
7 | ### Executors创建线程池
8 |
9 | Java中创建线程池很简单,只需要调用`Executors`中相应的便捷方法即可,比如`Executors.newFixedThreadPool(int nThreads)`,但是便捷不仅隐藏了复杂性,也为我们埋下了潜在的隐患(OOM,线程耗尽)。
10 |
11 | `Executors`创建线程池便捷方法列表:
12 |
13 | | 方法名| 功能 |
14 | | -------- | -------- |
15 | | newFixedThreadPool(int nThreads) | 创建固定大小的线程池 |
16 | | newSingleThreadExecutor() | 创建只有一个线程的线程池 |
17 | | newCachedThreadPool() | 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行 |
18 |
19 | 小程序使用这些快捷方法没什么问题,对于服务端需要长期运行的程序,创建线程池应该直接使用`ThreadPoolExecutor`的构造方法。没错,上述`Executors`方法创建的线程池就是`ThreadPoolExecutor`。
20 |
21 | ### ThreadPoolExecutor构造方法
22 |
23 | `Executors`中创建线程池的快捷方法,实际上是调用了`ThreadPoolExecutor`的构造方法(定时任务使用的是`ScheduledThreadPoolExecutor`),该类构造方法参数列表如下:
24 |
25 | ```java
26 | // Java线程池的完整构造函数
27 | public ThreadPoolExecutor(
28 | int corePoolSize, // 线程池长期维持的线程数,即使线程处于Idle状态,也不会回收。
29 | int maximumPoolSize, // 线程数的上限
30 | long keepAliveTime, TimeUnit unit, // 超过corePoolSize的线程的idle时长,
31 | // 超过这个时间,多余的线程会被回收。
32 | BlockingQueue workQueue, // 任务的排队队列
33 | ThreadFactory threadFactory, // 新线程的产生方式
34 | RejectedExecutionHandler handler) // 拒绝策略
35 | ```
36 |
37 | 竟然有7个参数,很无奈,构造一个线程池确实需要这么多参数。这些参数中,比较容易引起问题的有`corePoolSize`, `maximumPoolSize`, `workQueue`以及`handler`:
38 | - `corePoolSize`和`maximumPoolSize`设置不当会影响效率,甚至耗尽线程;
39 | - `workQueue`设置不当容易导致OOM;
40 | - `handler`设置不当会导致提交任务时抛出异常。
41 |
42 | 正确的参数设置方式会在下文给出。
43 |
44 | ### 线程池的工作顺序
45 |
46 | > If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
47 | > If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
48 | > If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.
49 |
50 | corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略
51 |
52 | ### Runnable和Callable
53 |
54 | 可以向线程池提交的任务有两种:`Runnable`和`Callable`,二者的区别如下:
55 |
56 | 1. 方法签名不同,`void Runnable.run()`, `V Callable.call() throws Exception`
57 | 2. 是否允许有返回值,`Callable`允许有返回值
58 | 3. 是否允许抛出异常,`Callable`允许抛出异常。
59 |
60 | `Callable`是JDK1.5时加入的接口,作为`Runnable`的一种补充,允许有返回值,允许抛出异常。
61 |
62 | ### 三种提交任务的方式:
63 |
64 | | 提交方式 | 是否关心返回结果 |
65 | | -------- | -------- |
66 | | `Future submit(Callable task)` | 是 |
67 | | `void execute(Runnable command)` | 否 |
68 | | `Future> submit(Runnable task)` | 否,虽然返回Future,但是其get()方法总是返回null |
69 |
70 | ## 如何正确使用线程池
71 |
72 | ### 避免使用无界队列
73 | 不要使用`Executors.newXXXThreadPool()`快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用`ThreadPoolExecutor`的构造方法手动指定队列的最大长度:
74 | ```java
75 | ExecutorService executorService = new ThreadPoolExecutor(2, 2,
76 | 0, TimeUnit.SECONDS,
77 | new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
78 | new ThreadPoolExecutor.DiscardPolicy());
79 | ```
80 |
81 | ### 明确拒绝任务时的行为
82 |
83 | 任务队列总有占满的时候,这是再`submit()`提交新的任务会怎么样呢?`RejectedExecutionHandler`接口为我们提供了控制方式,接口定义如下:
84 |
85 | ```java
86 | public interface RejectedExecutionHandler {
87 | void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
88 | }
89 | ```
90 | 线程池给我们提供了几种常见的拒绝策略:
91 |
92 |
93 |
94 | | 拒绝策略 | 拒绝行为 |
95 | | -------- | -------- |
96 | | AbortPolicy | 抛出RejectedExecutionException |
97 | | DiscardPolicy | 什么也不做,直接忽略 |
98 | | DiscardOldestPolicy | 丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置 |
99 | | CallerRunsPolicy | 直接由提交任务者执行这个任务 |
100 |
101 | 线程池默认的拒绝行为是`AbortPolicy`,也就是抛出`RejectedExecutionHandler`异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成`DiscardPolicy`,这样多余的任务会悄悄的被忽略。
102 |
103 | ```java
104 | ExecutorService executorService = new ThreadPoolExecutor(2, 2,
105 | 0, TimeUnit.SECONDS,
106 | new ArrayBlockingQueue<>(512),
107 | new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略
108 | ```
109 |
110 | ### 获取处理结果和异常
111 |
112 | 线程池的处理结果、以及处理过程中的异常都被包装到`Future`中,并在调用`Future.get()`方法时获取,执行过程中的异常会被包装成`ExecutionException`,`submit()`方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:
113 |
114 | ```Java
115 | ExecutorService executorService = Executors.newFixedThreadPool(4);
116 | Future