写在前面
重点章节:
第二章:Java并发机制的底层实现原理
第四章:Java并发编程基础
第五章:Java中的锁
第六章:Java并发容器和框架(ConcurrentHashMap 高频考点)
第八章:Java中的并发工具类
第九章:Java中的线程池
第二章 volatile和synchronized
volatile的应用
- volatile是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”
- 可见性:是当一个线程 修改一个共享变量时,另外一个线程能读到这个修改的值
- volatile使用恰当则比synchronized的使用和执行成本更低
因为它不会引起线程上下文的切换和调度
定义与实现原理
-
CPU的术语定义:
-
volatile的两条实现原则
1)Lock前缀指令会引起处理器缓存回写到内存。
2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效
volatile的使用优化
- 新增一个队列集合类Linked- TransferQueue,用一种追加字节的方式来优化
LinkedTransferQueue这个类,它使用一个内部类类型来定义队列的 头节点(head)和尾节点(tail)
而这个内部类PaddedAtomicReference相对于父类 AtomicReference只做了一件事情:
就是将共享变量追加到64字节
一个对象的引用占4个字节,它追加了15个变量(共占60个字节),再加上父类的value变量,一共64个字节。
原因:多数cpu的高速缓存行是64个字节宽,且不支持部分填充缓存行
若队列头/尾节点都不足64,会读到同一个高速缓存行,此时修改头会将整个缓存行都锁定。
使用追加字节填满缓存行,避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定。
- 不使用追加到64字节的情况:一是缓存行非64字节宽的处理器,二是共享变量不会被频繁地写
synchronized的实现原理与应用
- 很多人都会称呼它重量级锁,但优化后就不那么重了
- 为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程
Java对象头
- synchronized用的锁是存在Java对象头里的。
如果对象是数组类型,则虚拟机用3个字宽 (Word)存储对象头,
如果对象是非数组类型,则用2字宽存储对象头。
在32位虚拟机中,1字宽 等于4字节,即32bit,如表2-2所示。
锁的升级与对比
为减少获得和释放锁的开销,引入“偏向锁”和“轻量级锁”。
- 在JavaSE1.6的锁共有4种状态,级别由低到高:
无锁状态、偏向锁状态、轻量级锁状 态和重量级锁状态 - 锁可以升级但不能降级
偏向锁
- 引入:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁
- 方法:获取锁时,在对象头和栈帧中的锁记录里存储锁偏向的线程ID
- 判断:对象头的Mark Word里是否存储着指向当前线程的偏向锁
若测试成功,表示线程已经获得了锁。
若测试失败,则再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
- 偏向锁的撤销
采用一种等到竞争出现才释放锁的机制
它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着, 如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈 会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他 线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
- 关闭偏向锁
轻量级锁
-
轻量级锁加锁
-
轻量级锁解锁
锁的优缺点对比
原子操作的实现原理
- 处理器如何实现原子操作
第一个机制是通过总线锁保证原子性
第二个机制是通过缓存锁定来保证原子性
但是有两种情况下处理器不会使用缓存锁定:
- Java如何实现原子操作
在Java中可以通过锁和循环CAS的方式来实现原子操作
(1) 锁机制
(2)循环CAS
第六章 Java并发容器和框架
ConcurrentHashMap
- ConcurrentHashMap是线程安全且高效的HashMap
为什么使用ConcurrentHashMap?
- HashMap:线程不安全,HashMap在并发执行put操作时会引起死循环
在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所 以在并发情况下不能使用HashMap。
多线程会导致HashMap的Entry链表 形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获 取Entry。
- HashTable:效率低下,使用synchronized来保证线程安全
在线程竞争激烈的情况下HashTable 的效率非常低下。
因为所有访问HashTable的线程都必须竞争同一把锁。
其他线程也访问同步方法时,会进入阻塞或轮询状态。
如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低
- 综上,ConcurrentHashMap的锁分段技术,有效提升并发访问率
容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么 当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并 发访问效率
首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数 据也能被其他线程访问
ConcurrentHashMap的结构
- 由Segment数组结构和HashEntry数组结构组成
- Segment是一种可重入锁(ReentrantLock),扮演锁的角色
一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构。
- HashEntry则用于存储键值对数据
一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时, 必须首先获得与它对应的Segment锁
ConcurrentHashMap的初始化
- 1.初始化segments数组
- 2.初始化segmentShift和segmentMask
- 3.初始化每个segment
- 初始化segments数组
数组长度ssize:
通过concurrencyLevel计算,必须保证segments数组的长度是2的N次方。
目的:定位索引
通过按位与的散列算法来定位segments数组的索引
例如:
concurrencyLevel等于14、15或16,ssize都会等于16,即容器里锁的个数也是16。
注意:
concurrencyLevel的最大值是65535,这意味着segments数组的长度最大为65536, 对应的二进制是16位
- 初始化segmentShift和segmentMask
在定位segment时的散列算法里使用,是两个全局变量
segmentShift:
用于定位参与散列运算的位数
segmentMask:
是散列运算的掩码,等于ssize减1
- 初始化每个segment
int c = initialCapacity / ssize;通过c判断cap
initialCapacity:
是初始化容量,容量threshold=(int)cap*loadFactor,默认情况下initialCapacity等于16
loadfactor:
是每个segment的负载因子,默认情况下loadfactor等于 0.75,
cap
是segment里HashEntry数组的长度,如果c大于1,就会取大于等于c的2的N次方值,所以cap不是1,就是2的N次方。
默认情况下通过运算cap等于1,threshold等于零。
定位Segment
- 使用分段锁Segment来保护不同段的数据,则在插入/获取元素时,必须先通过散列算法定位到Segment
- ConcurrentHashMap会首先使用 Wang/Jenkins hash的变种算法对元素的hashCode进行一次再散列
进行再散列,目的是减少散列冲突,使元素能够均匀地分布在不同的Segment上, 从而提高容器的存取效率
ConcurrentHashMap的操作
get操作
- 先经过一次再散列,然后使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素
高效之处在于整个get过程不需要加锁,除非读到的值是空才会加锁重读。
原因:
它的get方法里将要使用的共享变量都定义成volatile类型
put操作
- put方法需要对共享变量进行写入操作,为线程安全,必须加锁
- put方法:首先定位到Segment,然后在Segment里进行插入操作
插入操作两个步骤:
第一步:判断是否需要对Segment里的HashEntry数组进行扩容
第二步:定位添加元素的位置,然后将其放在HashEntry数组里
(1)是否需要扩容
Segment的扩容判断比HashMap更恰当
Segment:
在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈 值,则对数组进行扩容
HashMap:
在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,此时无效扩容
(2)如何扩容
首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。
为了高效,ConcurrentHashMap只对某个segment进行扩容
size操作
- 如果要统计整个ConcurrentHashMap里元素的大小,就必须统计所有Segment里元素的大小后求和
- 最安全的做法:是在统计size的时候把所有Segment的put、remove和clean方法全部锁住,但是这种做法显然非常低效。
- ConcurrentHashMap的做法:
先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count变化,则再采用加锁的方式来统计所有Segment的大小。
如何判断count发生变化?
使用modCount变量,在put、remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否变化,从而得知容器的大小是否发生变化。
ConcurrentLinkedQueue
- 是一个基于链接节点的无界线程安全队列,它采用了“wait-free”算法(即CAS算法)来实现
- 线程安全队列的两种实现方式:一种是使用阻塞算法,另一种是使用非阻塞算法
阻塞算法:可以用一个锁 (入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现
非阻塞式:循环CAS的方式来实现
ConcurrentLinkedQueue的结构
- 由head节点和tail节点组成,每个节点(Node)由节点元素(item)和 指向下一个节点(next)的引用组成,节点之间通过next关联,从而组成一张链表结构的队列
- 默认情况下head节点存储的元素为空,tail节点等于head节点
入队列
- 从源代码角度来看,整个入队过程主要做两件事情:第一是定位尾节点;第二是使用CAS算法将入队节点设置成尾节点的next节点,如不成功则重试。
- 注意:更新tail节点,tail节点不总是尾节点
如果tail.next != null,则将入队节点设置成tail节点
如果tail.next == null,则将入队节点设置成 tail.next节点
所以tail节点不总是尾节点(理解这一点对于我们研究源码会非常有帮助)。
- 通过减少tail节点的更新频率,提升入队效率。
从本质上来看它通过增加对volatile变量的读操作来减少对volatile变量的写操作,而对volatile变量的写操作开销要远远大于读操作。
使用hops变量来控制更新频率,当tail节点和尾节点的距离≥常量HOPS的值(默认等于1)时才更新tail节点,tail和尾节点的距离越长,使用CAS更新tail节点的次数就会越少
- 注意:入队方法永远返回true,所以不要通过返回值判断入队是否成功。
出队列
- 当head节点里有元素时,直接弹出head 节点里的元素,而不会更新head节点。只有当head节点里没有元素时,出队操作才会更新head 节点。
- 这种做法也是通过hops变量来减少使用CAS更新head节点的消耗,从而提高出队效率。
- 同理入队列,head节点也不是每次都更新
首先获取头节点的元素,然后判断head元素是否为空
如果为空,表示另外一个线程已经进行了一次出队操作将该节点的元素取走;
如果不为空,则使用CAS的方式将头节点的引 用设置成null。
如果CAS成功,则直接返回头节点的元素。
如果不成功,表示另外一个线程已经进行了一次出队操作更新了head节点,导致元素发生了变化,需要重新获取头节点。
Java中的阻塞队列
四种处理方式
- 注意:如果是无界阻塞队列,队列不可能会出现满的情况,所以使用put或offer方法永 远不会被阻塞,而且使用offer方法时,该方法永远返回true。
七种阻塞队列
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
阻塞队列的实现原理:使用通知模式实现。
所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生 产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用
第八章 Java中的并发工具类
CountDownLatch:等待多线程完成
- 需求
- CountDownLatch也可实现join()的功能,且比其功能更多
- 原理
CountDownLatch的构造函数接收一个int类型的参数作为计数器,如果你想等待N个点完成,这里就传入N。
调用countDown方法,N就会减1,await方法阻塞当前进程,直到N=0
若某个子线程很慢,主线程不能一直等待,此时使用另一个带指定时间的await方法:
await(long time,TimeUnit unit),这个方法等待特定时间后,就会不再阻塞当前线程
- 注意
计数器必须≥0,只是==0时候,计数器就是零,调用await方法时不会阻塞当前线程。
CountDownLatch不可能重新初始化或者修改CountDownLatch对象的内部计数器的值。
一个线程调用countDown方法happen-before,另外一个线程调用await方法
CyclicBarrier:同步屏障
- 含义
让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会 开门,所有被屏障拦截的线程才会继续运行。
- 应用场景
import java.util.Map.Entry;
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
/*** 银行流水处理服务类 ** @authorftf **/
publicclass BankWaterService implements Runnable {
/*** 创建4个屏障,处理完之后执行当前类的run方法 */
private CyclicBarrier c = new CyclicBarrier(4, this);
/*** 假设只有4个sheet,所以只启动4个线程 */
private Executor executor = Executors.newFixedThreadPool(4);
/*** 保存每个sheet计算出的银流结果 */
private ConcurrentHashMap<String, Integer>sheetBankWaterCount = new ConcurrentHashMap<String, Integer>();
private void count() {
for (inti = 0; i< 4; i++) {
executor.execute(new Runnable() {
@Override publicvoid run() {
// 计算当前sheet的银流数据,计算代码省略sheetBankWaterCount.put(Thread.currentThread().getName(), 1);
// 银流计算完成,插入一个屏障
try {c.await(); }
catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } } }); } } @Override
publicvoid run() { intresult = 0;
// 汇总每个sheet计算出的结果
for (Entry<String, Integer>sheet : sheetBankWaterCount.entrySet()) {
result += sheet.getValue();
}
// 将结果输出 sheetBankWaterCount.put("result", result);
System.out.println(result);
}
public static void main(String[] args) {
BankWaterService bankWaterCount = new BankWaterService(); bankWaterCount.count();
}
}
使用线程池创建4个线程,分别计算每个sheet里的数据,每个sheet计算结果是1,再由 BankWaterService线程汇总4个sheet计算出的结果
- 原理
默认的构造方法是CyclicBarrier(int parties)
参数:屏障拦截的线程数量
每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞
- 因为主线程和子线程的调度是由CPU决定的,两个线程都有可能先执行,所以会产生两种输出
第一种:
1
2
第二种:
2
1
- 如果把new CyclicBarrier(2)修改成new CyclicBarrier(3)
则主线程和子线程会永远等待, 因为没有第三个线程执行await方法,即没有第三个线程到达屏障,所以之前到达屏障的两个 线程都不会继续执行
- 更高级的构造函数:CyclicBarrier(int parties,Runnab,le barrier- Action)
用于在线程到达屏障时,优先执行barrierAction
方便处理更复杂的业务场景
因为CyclicBarrier设置了拦截线程的数量是2,所以必须等代码中的第一个线程和线程A 都执行完之后,才会继续执行主线程,然后输出2,所以代码执行后的输出如下
CyclicBarrier 与CountDownLatch区别
Semaphore:控制并发线程数
- 含义
Semaphore(信号量):
是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
类比:“红绿灯”,车就是线程,驶入马路就表示线程在执行,离开马路就表示线程执行完 成,看见红灯就表示线程被阻塞,不能执行。
-
应用场景
-
原理
构造方法 Semaphore(int permits)接受一个整型的数字,表示可用的许可证数量。
Semaphore(10)表示允许10个线程获取许可证,也就是最大并发数是10。
Semaphore的用法也很简单,首先线程使用 Semaphore的**acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()**方法尝试获取许可证。
- 其他方法:
Exchanger:线程间交换数据
- 含义
用于线程间协作的工具类,用于进行线程间的数据交换。
- 原理
它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。
这两个线程通过exchange方法交换数据:
如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
- 应用场景
第九章 Java中的线程池
- 好处
线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序,都可以使用线程池。在开发过程中,合理地使用线程池能够带来3个好处。
第一:降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三:提高线程的可管理性
线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
线程池实现原理
ThreadPoolExecutor执行execute方法
- 四种情况
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤 需要获取全局锁)
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执
行这一步骤需要获取全局锁)
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法
-
尽可能地避免获取全局锁,几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁
-
源码分析
-
线程执行任务
线程池中的线程执行任务分两种情况,如下
1)在execute()方法中创建一个线程时,会让这个线程执行当前任务。
2)这个线程执行完上图中1的任务后,会反复从BlockingQueue获取任务来执行。
线程池的使用
线程池的创建
- 通过ThreadPoolExecutor创建
- 创建时参数
1)corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线 程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任 务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法, 线程池会提前创建并启动所有基本线程。
2)runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。
可以选择以下几个阻塞队列。ArrayBlockingQueue:
是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原 则对元素进行排序。
LinkedBlockingQueue:
一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue:
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用 移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue:
一个具有优先级的无限阻塞队列。
3)maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并 且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如 果使用了无界的任务队列这个参数就没什么效果。
4)ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设 置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线 程设置有意义的名字,代码如下。 new ThreadFactoryBuilder().setNameFormat(“XX-task-%d”).build();
5)RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状 态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法 处理新任务时抛出异常。
在JDK 1.5中Java线程池框架提供了以下4种策略。
AbortPolicy:直接抛出异常。
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。
当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。
如记录 日志或持久化存储不能处理的任务。
keepAliveTime(线程活动保持时间):
线程池的工作线程空闲后,保持存活的时间。所以, 如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。
TimeUnit(线程活动保持时间的单位):
可选的单位有天(DAYS)、小时(HOURS)、分钟 (MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒
(NANOSECONDS,千分之一微秒)。
向线程池提交任务
-
两种方式:execute()和submit()方法
-
execute()方法用于提交不需要返回值的任务
-
submit()方法用于提交需要返回值的任务-
关闭线程池
- 两种方式:shutdown和shutdownNow方法
- 原理:遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务 可能永远无法终止
- 区别
shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程
shutdownNow首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
- 调用任意一个,isShutdown方法都会返回true
- 当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true
通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
合理地配置线程池
- 分析角度
任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
任务的优先级: 高、中和低。
任务的执行时间:长、中和短。
任务的依赖性:是否依赖其他系统资源,如数据库连接
- CPU密集型:尽可能小,Ncpu+1个线程的线程池
- IO密集型:尽可能多的线程,如2*Ncpu。
- 混合型的任务:若能拆分成CPU和IO两种则拆,若两个任务执行时间相差太大,则没必要分解。可通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数
- 优先级:使用PriorityBlockingQueue来处理
建议使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千
线程池的监控
- 通过扩展线程池进行监控。
- 可以通过继承线程池来自定义线程池,重写线程池的 beforeExecute、afterExecute和terminated方法
- 也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。
-例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。 这几个方法在线程池里是空方法。
- 参数
taskCount:线程池需要执行的任务数量。
completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是 否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销 毁,所以这个大小只增不减。
getActiveCount:获取活动的线程数。