《Java并发编程的艺术》笔记
第一章 并发编程的挑战
第二章 Java并发机制的底层实现原理
volatile的两条实现原则:java
- Lock前缀指令会引发处理器缓存回写到内存
- 一个处理器的缓存回写到内存会致使其余处理器的缓存无效。
volatile的使用优化:共享变量会被频繁读写时,能够经过追加为64字节以提升并发编程的效率。由于目前主流处理器高速缓存行是64个字节宽,不支持部分填充缓存行,经过追加到64字节的方式填满高速缓冲区的缓存行,避免各元素加载到同一缓存行而互相锁定。(Java7后可能不生效,由于Java7更智能,会淘汰或从新排列无用字段,须要使用其余追加字节的方式)算法
Java对象头编程
32/64bit |
Mark Word |
存储对象的hashCode或锁信息等 |
32/64bit |
Class Metadata Address |
存储到对象类型数据的指针 |
32/64bit |
Array Length |
数组的长度(仅当当前对象为数组时存在) |
CAS操做,即Compare And Swap,比较并交换。CAS操做须要输入两个数值,一个旧值(指望操做前的值)和一个新值,在操做期间先比较旧值有没有发生变化,若是没有则交换成新值,不然不交换。数组
锁有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。这几种状态会随着竞争状况逐渐升级,只升不降。缓存
偏向锁安全
- 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该线程再次加锁解锁时不须要进行CAS操做,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
- 偏向锁使用了一种等到竞争出现才释放锁的机制,因此当其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
- 偏向锁的撤销,须要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,而后检查持有偏向锁的线程是否活着,若是线程不处于活动状态,则将对象头设置成无锁状态;若是线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么从新偏向于其余线程,要么恢复到无锁或者标记对象不适合做为偏向锁,最后唤醒暂停的线程。
- 偏向锁默认启动,可是它在应用程序启动几秒钟以后才激活,若有必要可使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。若是肯定应用程序里全部的锁一般状况下处于竞争状态,能够经过JVM参数来关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
轻量级锁并发
- 线程在执行同步块以前,JVM会先在当前线程的栈帧中建立用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中(Displaced Mark Word)。而后线程尝试经过CAS将对象头中的Mark Word替换为指向锁记录的指针。若是成功则当前线程得到锁,不然说明其余线程竞争锁,当前线程尝试使用自旋来获取锁。
- 轻量级锁解锁时,会使用原子的CAS操做将Displaced Mark Word替换回到对象头,若是成功则表示没有竞争发生,不然表示当前锁存在竞争,锁会升级为重量级锁。
重量级锁框架
- 其余线程试图获取锁时都被阻塞,持有锁的线程释放锁以后唤醒这些线程,被唤醒的线程再抢。
锁的优缺点对比函数
偏向锁 |
加锁和解锁无需额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 |
若是线程间存在锁竞争,会带来额外的锁撤销的消耗 |
适用于只有一个线程访问同步块场景 |
轻量级锁 |
竞争的线程不会阻塞,提升了程序的响应速度 |
若是始终得不到锁竞争的线程,使用自旋会消耗CPU |
追求响应时间,同步块执行速度很是快 |
重量级锁 |
线程竞争不适用自旋,不会消耗CPU |
线程阻塞,响应时间缓慢 |
追求吞吐量,同步块执行速度较慢 |
CAS实现原子操做的三大问题高并发
- ABA问题。一个值从A变成B又变成A,使用CAS检查时觉得没有变化,但实际上却变化了。解决思路是使用版本号。
- 循环时长长,开销大。
- 只能保证一个共享变量的原子操做。(Java1.5之后,JDK提供了AtomicReference类来保证引用对象之间的原子性,能够把多个变量放在一个对象里来进行CAS操做)
第三章 Java内存模型
- 并发编程模型的两个关键问题:线程之间如何通讯,如何同步
- 在命令式编程中,线程之间的通讯机制有两种:共享内存和消息传递。
内存屏障类型表
LoadLoad Barriers |
确保Load1数据的装载先于Load2及全部后序装置指令的装载 |
StoreStore Barriers |
确保Store1数据对其余处理器可见(刷新到内存)先于Store2及全部后序存储指令的存储 |
LoadStore Barriers |
确保Load1数据装载先于Store2及全部后序的存储指令刷新到内存 |
StoreLoad Barriers |
确保Store1数据对其余处理器变得可见(指刷新到内存)先于Load2及全部后序装载指令的装载。StoreLoad Barriers会使改屏障以前的全部内存访问指令(存储和装载指令)完成以后,才执行该屏障以后的内存访问指令 |
- 编译器、runtime和处理器都必须遵照as-if-serial语义,即无论怎么重排序,(单线程)程序的执行结果不能被改变。
volatile的内存语义
volatile变量自身具备如下特性:
- 可见性:对一个volatile变量的读,老是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意单个volatile变量的读/写具备原子性,但相似于volatile++这种复合操做不具备原子性。
volatile写-读的内存语义:
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
- 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
JMM内存屏障插入策略:
- 在每一个volatile写操做先后分别插入StoreStore、StoreLoad屏障。
- 在每一个volatile读操做后面插入LoadLoad、LoadStore屏障。
锁的内存语义
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。
- 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
- 公平锁和非公平锁的内存语义:
- 公平锁和非公平锁释放时,最后都要写一个volatile变量state。
- 公平锁获取时,首先会去读volatile变量。
- 非公平锁获取时,首先会用CAS操做更新volatile变量,这个操做同时具备volatile读/写的内存语义。
final域的内存语义
对于final域,编译器和处理器要遵循两个重排序规则:
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操做之间不能重排序。
- 初次读一个包含final域的对象的应用,与随后初次读这个final域,这两个操做之间不能重排序。
- 读final域的重排序规则能够确保:在读一个对象的final域以前,必定会先读包含这个final域的对象的引用。
- 写final域的重排序规则能够确保:在对象引用为任意线程可见以前,对象的final域已经被正确初始化。(前提是对象引用不能在构造函数中逸出)
第四章 Java并发编程基础
- 线程的优先级仅仅是一部分决定因素,由于线程的切换具备随机性,并且针对不一样的系统而言,优先级这个概念可能就不存在,其仅仅是决定程序设计的衡量的一个标准。
等待/通知的经典范式
- 等待方遵循以下原则:
- 获取对象的锁
- 若是条件不知足,那么调用对象的wait()方法,被通知后仍要检查条件。
- 条件知足则执行对应的逻辑。
- 通知方遵循以下原则:
- 得到对象的锁。
- 改变条件。
- 通知全部等待在对象上的线程。
第五章 Java中的锁
Lock接口提供的synchronized关键字不具有的主要特性
尝试非阻塞地获取锁 |
当前线程尝试获取锁,若是这一时刻锁没有被其余线程获取到,则成功获取并持有锁 |
能被中断地获取锁 |
获取到锁的线程可以相应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放 |
超时获取锁 |
在指定的截止时间以前获取锁,若是截止时间到了仍旧没法获取锁,则返回 |
队列同步器(AbstractQueuedSynchronizer)
同步器提供以下3个方法来访问或修改同步状态(int成员变量):
- getState():获取当前同步状态
- setState(int newState):设置当前同步状态
- compareAndSetState(int expect, int update):使用CAS设置当前状态,该方法可以保证状态设置的原子性。
同步器可重写的方法
protected boolean tryAcquire(int arg) |
独占式获取同步状态,实现该方法须要查询当前状态并判断同步状态是否符合预期,而后再进行CAS设置同步状态 |
protected boolean tryRelease(int arg) |
独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
protected int tryAcquireShared(int arg) |
共享式获取同步状态,返回大于等于0的值,表示获取成功,反之失败 |
protected boolean tryReleaseShared(int arg) |
共享式释放同步状态 |
protected boolean isHeldExclusively() |
当前同步器是否在独占模式下被线程占用,通常该方法表示是否被当前线程所独占 |
同步器提供的模板方法

同步队列及等待队列的节点属性类型与名称以及描述

- 同步队列中的结点只有当其前驱结点为头结点时,才尝试获取同步状态。
- 读写锁的同步状态高16位表示读状态、低16位表示写状态。
LockSupport
- 当须要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工做。LockSupport定义了一组公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为了构建同步组建的基础工具。
- LockSupport提供的阻塞和唤醒方法(其中参数blocker是用来标识当前线程在等待的对象,便于问题排查和系统监控)
void park(Object blocker) |
阻塞当前线程,若是调用unpark方法或者当前线程被终端,才能从park方法返回 |
void parkNanos(Object blocker, long nanos) |
阻塞当前线程,最长不超过nanos纳秒,返回条件在park的基础上增长了超时返回 |
void parkUntil(Object blocker, long deadline) |
阻塞当前线程,直到deadline时间 |
void unpark(Thread thread) |
唤醒处于阻塞状态的线程thread |
第六章 Java并发容器和框架
- ConcurrentHashMap使用锁分段技术,将数据分红一段一段地存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其余段的数据也能被其余线程访问。
- ConcurrentLinkedQueue非阻塞的线程安全队列
阻塞队列
- 阻塞队列(BlockingQueue)是一个支持两个附加操做的队列。这两个附加的操做支持阻塞的插入和移除方法。
- 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
- 支持阻塞的移除方法:当队列空时,获取元素的线程会等待队列变为非空。
- 插入和移除操做的4种处理方式
插入方法 |
add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
移除方法 |
remove() |
poll() |
take() |
poll(time, unit) |
检查方法 |
element() |
peek() |
不可用 |
不可用 |
- 抛出异常:当队列满时,若是再往队列里插入元素会抛出IllegalStateException("Queue full")异常。当队列空时,从队列里获取元素会抛出NoSuchElementException异常。
- 返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回true。若是是移除方法,则从队列里取出一个元素,若是没有则返回null。
- 一直阻塞:当阻塞队列满时,若是生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断退出。当队列空时,若是消费者线程从队列列take元素,队列会阻塞消费者线程,直到队列不为空。
- 超时退出:当阻塞队列满时,若是生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,若是超时则退出。
JDK7提供了7个阻塞队列:
- ArrayBlockingQueue:用数组实现的有界阻塞队列,按FIFO原则对元素进行排序。
- LinkedBlockingQueue:用链表实现的有界阻塞队列,默认和最大长度为Integer.MAX_VALUE,按FIFO原则对元素进行排序。
- PriorityBlockingQueue:支持优先级的无界阻塞队列,默认状况下元素采起天然顺序升序排列。不保证同优先级元素的顺序。
- DelayQueue:支持延时获取元素的无界阻塞队列。队列使用PriorityQueue实现,队列中的元素必须实现Delayed接口,在建立元素时能够指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。可应用于:
- 缓存系统的设计:用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素表示缓存有效期到了。
- 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,好比TimerQueue就是使用DelayQueue实现的。
- SynchronousQueue:不存储元素的阻塞队列。每个put操做必须等待一个take操做,不然不能继续添加元素。
- LinkedTransferQueue:链表结构组成的无界阻塞TransferQueue队列。相对于其余阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法。
- transfer方法:若是当前有消费者正在等待接收元素,transfer方法能够把生产者传入的元素马上传给消费者。若是没有消费者在等待接收元素,则将元素存放在队列tail节点并等到钙元素被消费者消费了才返回。
- tryTransfer方法:若是没有消费者等待接收元素,则当即返回false。
- LinkedBlockingDeque:链表结构组成的双向阻塞队列。
Fork/Join框架
- Fork/Join框架是一个用于并行执行任务的框架,是一个把大任务分隔成若干个小任务,最终汇总每一个小任务结果后获得大任务结果的框架。
- 工做窃取算法:假如咱们须要作一个比较大的任务,咱们能够把这个任务分割为若干互不依赖的子任务,为了减小线程间的竞争,因而把这些子任务分别放到不一样的队列里,并为每一个队列建立一个单独的线程来执行队列里的任务,线程和队列一一对应,好比A线程负责处理A队列里的任务。可是有的线程会先把本身队列里的任务干完,而其余线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其余线程干活,因而它就去其余线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,因此为了减小窃取任务线程和被窃取任务线程之间的竞争,一般会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。工做窃取算法的优势是充分利用线程进行并行计算,并减小了线程间的竞争,其缺点是在某些状况下仍是存在竞争,好比双端队列里只有一个任务时。而且消耗了更多的系统资源,好比建立多个线程和多个双端队列。
- ForkJoinTask:咱们要使用ForkJoin框架,必须首先建立一个ForkJoin任务。它提供在任务中执行fork()和join()操做的机制,一般状况下咱们不须要直接继承ForkJoinTask类,而只须要继承它的子类,Fork/Join框架提供了如下两个子类:
- RecursiveAction:用于没有返回结果的任务。
- RecursiveTask :用于有返回结果的任务。
- ForkJoinPool :ForkJoinTask须要经过ForkJoinPool来执行,任务分割出的子任务会添加到当前工做线程所维护的双端队列中,进入队列的头部。当一个工做线程的队列里暂时没有任务时,它会随机从其余工做线程的队列的尾部获取一个任务。
- ForkJoinTask在执行的时候可能会抛出异常,可是咱们没办法在主线程里直接捕获异常,因此ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否一件抛出异常或已经被取消了,而且能够经过ForkJoinTask的getException方法获取异常。其中,getException方法返回Throwable对象,若是任务被取消了则返回CancellationException,若是任务没有完成或者没有抛出异常则返回null。
第七章 Java中的13个原子操做类(Ps:认真数了一下发现只有12个)
- 原子更新方式
- 原子更新基本类型
- 原子更新数组
- 原子更新引用
- 原子更新属性(字段)
- 原子更新基本类型
- AtomicBoolean :原子更新布尔类型
- AtomicInteger: 原子更新整型
- AtomicLong: 原子更新长整型
- 原子更新数组
- AtomicIntegerArray :原子更新整型数组里的元素
- AtomicLongArray :原子更新长整型数组里的元素
- AtomicReferenceArray : 原子更新引用类型数组的元素
- 原子更新引用类型
- AtomicReference :原子更新引用类型
- AtomicReferenceFieldUpdater :原子更新引用类型里的字段
- AtomicMarkableReference:原子更新带有标记位的引用类型。能够原子更新一个布尔类型的标记位和应用类型
- 原子更新字段类
- AtomicIntegerFieldUpdater:原子更新整型的字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整型数值与引用关联起来,可用于原子的更新数据和数据的版本号,能够解决使用CAS进行原子更新时可能出现的ABA问题。
第八章 Java中的并发工具类
CountDownLatch
- CountDownLatch容许一个或多个线程等待其余线程完成操做。
- CountDownLatch的构造函数接收一个int类型的参数做为计数器,若是你想等待N个点完成,就传入N。
- 每次调用CountDownLatch的countDown方法时,N就减1,CountDownLatch的await方法会阻塞当前线程,直到N变成0。
CyclicBarrier
- CyclicBarrier的做用是让一组线程到达一个屏障(也能够称之为同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,全部被屏障拦截的线程才能继续运行。
- CyclicBarrier(int parties)构造函数接收一个int参数用来设置拦截线程的数量,还有一个更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction)设定第一个到达屏障的线程执行barrierAction。
- getNumberWaiting方法能够得到CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断。
Semaphore
- Semaphore(信号量)用来控制同时访问特定资源的线程数量,经过协调各个线程以保证合理的使用公共资源。
- Semaphore(int permits)构造方法接收一个int参数,表示可用的许可证数量。
- 每次线程使用Semaphore的acquire()方法获取一个许可证,用完后调用release()方法归还。
- 其余方法
- intavailablePermits():返回此信号量中当前可用的许可证数。
- intgetQueueLength():返回正在等待获取许可证的线程数。
- booleanhasQueuedThreads():是否有线程正在等待获取许可证。
- void reducePermits(int reduction):减小reduction个许可证,是个protected方法。
- Collection getQueuedThreads():返回全部等待获取许可证的线程集合,是个protected方法。
Exchanger
- 用于线程间交换数据,每两次执行Exchanger的exchange(V data)方法,则交换两次执行的数据并返回。可用于校对工做。
第九章 Java中的线程池
线程池的3个好处:
- 下降资源消耗:经过重复利用已建立的线程下降线程建立和销毁形成的消耗。
- 提升响应速度:当任务到达时,任务能够不须要等到线程建立就能当即执行。
- 提升线程的可管理性:线程是稀缺资源,若是无限制地建立,不只会消耗系统资源,还会下降系统的稳定性。使用线程池能够进行统一分配、调优和监控。
当提交一个新任务到线程池时,线程池的流程:
- 若是当前运行的线程少于corePoolSize,则建立新线程来执行任务。
- 若是运行的线程等于或多于corePollSize,则将任务加入BlockingQueue。
- 若是没法将任务加入BlockingQueue(队列已满),则建立新的线程来处理任务。
- 若是建立新线程将致使当前运行的线程数超过maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
线程池的构造方法
ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, handler),其中:
- corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会建立一个线程来执行任务,即便其余空闲的基本线程可以执行新任务也会建立线程,直到须要执行的任务数大于线程池基本大小。若是调用了线程池的prestartAllCoreThreads()方法,线程池会提早建立并启动全部基本线程。
- runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。能够选择ArratBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。
- maximumPoolSize(线程池最大数量):线程池容许建立的最大线程数。
- ThreadFactory:用于设置建立线程的工厂,能够经过线程工厂给每一个建立出来的线程设置更有意义的名字。
- RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采起一种策略处理提交的新任务。这个策略默认状况下是AbortPolicy,表示没法处理新任务时抛出异常。策略有下列几种:
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:只用调用者所在线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
- DiscardPolicy:不处理,丢弃掉。
- 其余应用场景须要实现RejectedExecutionHandler接口自定义。
- keepAliveTime(线程活动保持时间):线程池的工做线程空闲后,保持存活的时间。若是任务多且执行时间短,能够调高存活时间提升线程利用率。
- TimeUnit(线程活动保持时间的单位)
向线程池提交任务有两种方式:
- execute(Runnable command):没有返回值
- submit(Callable
task):返回一个future类型的对象,经过这个对象判断任务是否执行成功,并经过其get()方法来获取返回值,该方法会阻塞当前线程直到任务完成。
线程池的关闭有两种方法:
- shutdown():将线程池的状态设置成SHUTDOWN状态,而后中断全部没有正在执行任务的线程。
- shutdownNow():将线程池的状态设置成STOP,而后尝试中止全部的正在执行或暂停任务的线程,并返回等待执行任务的列表。
只要调用了两种关闭方法的任一种,isShutdown()方法都会返回true。当且仅当全部任务都关闭,CIA表示线程池关闭成功,这是isTerminaed()方法才会返回true。
合理配置线程池(设N为CPU个数)
- CPU密集型任务,应配置尽量少的线程,如N+1。
- IO密集型任务,应配置尽量多的线程,如2N。
- 优先级不一样的任务能够考虑使用优先级队列priorityBlockingQueue来处理,但优先级低的任务可能永远不被执行。
- 使用有界队列能增长系统的稳定性和预警性,避免队列愈来愈多撑满内存,致使系统不可用。
线程池的监控
监控线程池的时候可使用如下属性:
- taskCount:线程池须要执行的任务数量。
- completedTaskCount:线程池在运行过程当中已完成的任务数量,小于或等于taskCount。
- largestPoolSize:线程池里曾经建立过的最大线程数量。经过这个数据能够知道线程池是否曾经满过。
- getPoolSize:线程池的线程数量。若是线程池不销毁的话,线程池里的线程不会自动销毁,因此这个大小只增不减。
- getActiveCount:获取活动的线程数。
能够经过继承线程池来自定义线程池,重写线程池的beforeExecute, afterExecute和terminated方法,也能够在任务执行先后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。这几个方法在线程池里都是空方法。
第十章 Executor框架
工厂类Executors可建立3种类型的ThreadPoolExecutor:
- FixedThreadPool:可重用固定线程数的线程池。
new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
- SingleThreadExecutor:使用单个worker线程的Executor。
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
- CachedThreadPool:会根据须要建立新线程的线程池。
new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnbalbe>())
工厂类Executors可建立2种类型的ScheduledThreadPoolExecutor:
- ScheduledThreadPoolExecutor:包含若干个线程的ScheduledThreadPoolExecutor。
- SingleThreadScheduledExecutor:只包含一个线程的ScheduledThreadPoolExecutor。
第十一章 Java并发编程实践