线程waitting到runnable切换也就是上下文切换,是需要时间代价的,这个就需要根据具体情况去衡量,多少个线程运行合适,
windows下可以使用visulavm查看进行pid,或者命令行输入 jps

输出线程信息到文件
jstack pid > 文件全路径

统计线程状态
grep 关键字 文件名 | awk ‘{print $2$3$4$5}’ |sort|uniq -c
grep java.lang.Thread.State dump | awk ‘{print $2$3$4$5}’ |sort|uniq -c

一、Java并发基础原理

1. synchronized

是通过进入和退出monitor对象来实现方法同步和代码块同步,代码块同步使用monitorEnter和monitorExit指令来实现,方法同步使用另外一种方式实现(细节书里面没有说)。但是,方法的同步统一可以使用这两个指令来实现

synchronized用的锁是存在Java对象头里面的。

1.1 偏向锁

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块的时候不需要CAS操作来加锁和解锁,只需要测试一下对象头里是否存着指向当前线程的偏向锁。

偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否存活,如果线程不处于活动状态,则将对象头设置成无锁状态。如果线程还存活,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,恢复到无锁状态或者标记对象不合适作为偏向锁,最后唤醒暂停的线程。

1.2 轻量级锁

加锁和解锁都采用CAS进行,所以是轻量级的。

加锁
线程执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,当前线程尝试自旋来获取锁。

解锁
解锁的时候会使用CAS将Displaced Mark Word替换回到对象头,如果成功,就没有竞争发生。如果失败,表示当前锁存在竞争,锁膨胀为重量级锁。

1.3 重量级锁

竞争时线程会阻塞,有上下文切换过程,比较耗费资源

1.4 锁对比

优点 缺点 适用场景
偏向锁 加锁解锁不需要额外的消耗 如果线程间存在竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争过程线程不会阻塞,提高了程序响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU 最求响应时间
同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量
同步块执行速度较长

2. volatile

volatile是一种轻量级锁,能够保证在多处理器开发中保证线程共享变量的可见性,java语言规范对volatile的定义为:java语言规范允许线程访问共享变量,为了确保共享变量能够被准确和一致性地更新,线程应该确保通过排它锁单独获得这个变量。
volatile变量进行写操作的时候,会在汇编指令里面插入lock指令,lock指令的作用:
1)将当前处理器缓存行的数据写回系统内存
2)这个回写内存的操作会使在其他CPU里缓存了该地址的数据无效。

3. 原子操作实现原理

原子操作是指不可中断的一个或者一系列操作,处理器通过总线锁和缓存锁来保证原子性,java通过CAS操作实现原子操作。
CAS存在三个问题:
1)ABA问题;
2)循环时间长开销大;
3)只能保证一个共享变量的原子操作

CAS操作利用的处理器提供的CMPXCHG指令实现的,自旋CAS实现就是循环进行CAS操作直到成功。同时有volatile的写/读语义

二、Java内存模型

2.1 Java内存模型基础

java内存模型采用共享内存模型,线程间通信总是隐式进行,通信对程序员透明,线程直接通信由JMM控制,JMM定义了线程和主内存之间的抽象关系:线程直接的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。

JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性。线程直接的通信也是通过主内存完成的。

重排序:
1)编译器重排序,在不改变单线程语义的情况下对指令重排序;
2)指令级并行的重排序;
3)内存系统的重排序。如果要禁止重排序,在编译器生成指令序列的时候要插入特定的内存屏障

happens-before规则:

  • 程序顺序规则:一个线程中的每个操作,hanppens-before于该线程中的任意后续操作

  • 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁

  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读

  • 传递性:如果A happens-before B,且B happens-before C , 那么A happens-before C。

两个操作如果有happens-before关系,仅要求前一个操作的结果对后一个操作可见
单CPU和CPU多级缓存示意图
并发编程 - 图1并发编程 - 图2

2.1.1 缓存cache 的作用:

CPU 的频率很快,主内存跟不上 cpu 的频率,cpu 需要等待主存,浪费资源。所以 cache 的出现是解决 cpu
和内存之间的频率不匹配的问题。
缓存cache 带来的问题:
并发处理的不同步
解决方式有:总线锁、缓存一致性。

2.1.2 缓存一致性 :

MESI 协议缓存状态

状态 描述 监听任务
M修 改
(Modified)
该 Cache line 有效,数据被修改
了,和主内存中的数据不一致,
数据只存在于本 Cache 中。
缓存行必须时刻监听所有试图读该缓存行相对就主存的
操作,这种操作必须在缓存将该缓存行写回主存并将状态

变成 S(共享)状态之前被延迟执行 | | E 独享、互斥
(Exclusive) | 该 Cache line 有效,数据和内存
中的数据一致,数据只存在于本
Cache 中。 | 缓存行也必须监听其它缓存读主存中该缓存行的操作,一 旦有这种操作,该缓存行需要变成 S(共享)状态。 | | S 共 享
(Shared) | 该 Cache line 有效,数据和内存
中的数据一致,数据存在于很多
Cache 中。 | 缓存行也必须监听其它缓存使该缓存行无效或者独享该
缓存行的请求,并将该缓存行变成无效(Invalid) | | I 无 效
(Invalid) | 该 Cache line 无效。 | 无 |

Heap(堆):
java 里的堆是一个运行时的数据区,堆是由垃圾回收来负责的, 堆的优势是可以动态的分配内存大小,生存期也不必事先告诉编译器, 因为他是在运行时动态分配内存的,java 的垃圾回收器会定时收走不用的数据, 缺点是由于要在运行时动态分配,所有存取速度可能会慢一些
Stack(栈):
栈的优势是存取速度比堆要快,仅次于计算机里的寄存器,栈的数据是可以共享的, 缺点是存在栈中的数据的大小与生存期必须是确定的,缺乏一些灵活性 栈中主要存放一些基本类型的变量,比如 int,short,long,byte,double,float,boolean,char,
对象句柄,并发编程 - 图3
Java 并发编程的三个概念
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看
得到修改的值。
有序性: 即程序执行的顺序按照代码的先后顺序执行
程序顺序和我们的编译运行的执行一定是一样
编译优化
指令重排
Happens-before:
传递原则:lock unlock
A>B>C A>C
内存同步:
并发编程 - 图4并发编程 - 图5

2.2 volatile

volatile写的内存语义

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存中

volatile读的内存语义

当读一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量置为无效,然后从主内存中读取

volatile内存语义的实现

为了实现volatile的内存语义,编译器在生成字节码的时候,会插入内存屏障来禁止特定类型的处理器重排序

  • 每个volatile写操作的前面插入一个StoreStore屏障

  • 每个volatile写操作的后面插入一个StoreLoad屏障

  • 每个volatile读操作的前面插入一个LoadLoad屏障

  • 每个volatile读操作的后面插入一个LoadStore屏障

2.2 锁

锁的获取和释放的内存语义

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中,当线程获取锁时,JMM会把线程对应的本地内存置为无效,从而使被监视器保护的临界区代码必须从主内存中读取共享变量。

锁内存语义的实现

锁的实现背后是借助volatile变量,使用CAS操作来实现,CAS操作同时具有volatile读和写的内存语义
Lock 的获取分公平和非公平,公平锁和非公平锁最后都会写一个volatile变量;公平锁获取先读volatile变量,非公平锁先用CAS更新volatile变量

2.3 final

通过禁用重排序规则来实现final的内存语义

写final域的重排序规则
1)禁止把final域的写重排序到构造函数之外
2)编译器会在final域写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。

读final域的重排序规则:在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。编译器会在读final域操作的前面插入一个LoadLoad屏障。

双重检查锁与延迟初始化

由于new一个对象要分三步:1)申请空间;2)初始化;3)设置实例内存地址,2/3之间可能会出现重排序,导致没有初始化完就分配了地址,出现问题,可以采用以下几种方案:
1)使用volatile修饰
2)基于类初始化的解决方案,利用类加载的特性,将创建实例包装到一个内部类,这样有虚拟机保证只会被初始化一次

2.4 happens-before

JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性
JMM对编译器和处理器的束缚已经尽可能少。

三、并发基础

3.1 线程

线程是现代操作系统调度的最小单位,也叫轻量级进程,一个进程可以创建多个线程。

线程状态:
1)new,初始状态,线程被构建,但是还没有调用start()方法
2)Runnable, 运行状态,java线程将操作系统中的就绪状态和运行状态笼统地称作“运行中”
3)blocked, 阻塞状态,表示线程阻塞于锁
4)waiting, 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定动作(通知或中断)
5)time_waiting,超时等待状态,该状态不同于waiting,它是可以在指定的时间自行返回的
6)terminaled, 终止状态,表示当前线程已经执行完毕

调用wait(),notify(),notifyAll()需要注意:
1)调用前需要先获得对象的锁
2)调用wait()方法后,线程由RUNNING状态转为WAITING状态,并加入等待队列
3)notify(),notifyAll()调用后等待线程还不会从wait()返回,需要等到线程释放锁之后等待线程才会返回
4)notify()将等待队列中的一个线程移到同步队列,notifyAll()将等待队列所有线程全部移到等待队列,等待线程状态由WAITING变为BLOCKED

使用多线程的原因:
1)更多的处理器核心
2)更快的响应时间
3)更好的编程模型

3.1.1 Thread.join()的使用

这个方法的作用就是当前线程等待thread线程执行完毕后才从join返回

3.2 ThreadLocal的使用

线程变量,用来存储线程独享的数据,可以使用get、set方法进行取、存,可以应用于关键参数的传递,计算方法执行时间等,使用线程作为key

四、锁

Lock接口提供的特性:1)非阻塞的获取锁;2)能被中断地获取锁;3)超时获取锁

4.1 队列同步器

队列同步器,AbstractQueuedSynchronizer是用来构建锁或者其他同步组件的基础框架,使用一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作.
同步器的主要使用方式是继承,通过实现相应的方法来控制同步状态,通过CAS算法来保证状态的修改是安全的,既可以支持独占式获取,也可以支持共享式地获取同步状态。
同步器是实现锁的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。锁是面向使用者的,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向实现者的,它简化了锁的实现方式,屏蔽了同步状态管理、线程排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

4.2 重入锁

ReentrantLock能够被一个线程多次加锁,还支持公平和非公平选择,默认是非公平锁

实现重进入

需要解决两个问题:
1)线程再次获取锁,通过判断当前线程时候是获取锁的线程来决定是否获取成功;
2)锁的最终释放,已经获取锁的线程再次获取锁时,只是增加同步状态值,释放锁时也会减少同步状态值

公平锁与非公平锁的区别

对于非公平锁先通过CAS设置同步状态,如果成功代表同步状态设置成功,公平锁还有一个是否有前驱节点的判断。

并发编程 - 图6
并发编程 - 图7

4.3 读写锁

读写锁允许多个读线程访问,当写线程访问时,其他所有线程均被阻塞,读写锁维护了一对读锁、一对写锁,通过分离读锁和写锁,大大提升了性能。

实现分析

读写状态设计

读写锁同样依赖自定义同步器来实现同步功能,在同步状态上维护多个读线程和写线程的状态,采用“按位切割使用”这个变量,高16位表示读,低16位表示写

写锁的获取与释放

写锁支持重进入的排它锁,如果当前线程获得了写锁,则增加写状态,如果存在读锁,则写锁不能被获取

读锁的获取与释放

读锁是一个支持重进入的共享锁,能够被多个线程同时获取,没有写线程访问时,可以被多个读线程获取

锁降级

降级指的是写锁降级为读锁,把持着写锁,然后获得读锁,最后再释放写锁。如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程T获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取读锁进行数据更新。

4.4 LockSupport

当需要阻塞或唤醒一个线程的时候,都会使用LockSupport工具类来完成相应工作,提供了park/unpark实现。

4.5 Condition

Condition接口提供了类似Object的监视器方法,与Lock配合可以实现等待通知模式,在调用condition.await()方法之前需要先获得锁
1、等待队列
等待队列是一个先进先出队列,每个节点都包含了一个线程的引用
2、等待
调用await()方法会是当前线程进入等待队列,并释放锁
3、通知
调用signal()方法,会唤醒在等待队列中等待时间最长的节点,唤醒之前会将节点移到同步队列中
Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。

五、并发容器和框架

5.1 ConcurrentLinkedQueue

是一个基于链接节点的无界线程安全队列,采用先进先出的规则进行排序

5.2 阻塞队列

阻塞队列是一个支持两个附加操作的队列:
1)支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满
2)支持阻塞的移除方法:当队列为空时,获取元素的线程会等待队列变为非空
当队列不可用时有四种方式:
1)抛出异常:
2)返回特殊值
3)一直阻塞
4)超时退出

5.2.1 ArrayBlockingQueue

是一个用数组实现的有界阻塞队列,按照先进先出对元素进行排序,默认情况下不保证线程公平的访问队列

5.2.2 LinkedBlockingQueue

是一个用链表实现的有界阻塞队列,默认最大长度为Integer.MAX_VALUE

5.2.3 PriorityBlockingQueue

是一个支持优先级的无界阻塞队列,默认采用自然升序排列,也可以自定义排序规则,需要注意的是不能保证同优先级元素的排序

5.2.4 DelayQueue

是一个支持延时获取元素的无界阻塞队列,队列使用PriorityQueue来实现,元素必须实现Delayed接口,创建元素时可以指定需要多久才能从队列中拿到元素,运用场景:
1)缓存系统设计:用DelayQueue保存缓存有效期,使用现场循环查询DelayQueue
2)定时任务调度:使用DelayQueue保存需要执行的任务

5.2.5 SynchronousQueue

是一个不存储元素的阻塞队列,每个put必须等待一个take操作,否则不能继续添加元素。

5.2.6 LinkedTransferQueue

是一个由链表结构组成的无界阻塞队列,多了两个方法transfer和tryTransfer
1)transfer:当有消费者在等待接收元素时,transfer可以立刻将元素传输给消费者,如果没有消费者,transfer方法会将元素放在队列的tail节点,等到该元素被消费者消费了才返回。因为自旋会消耗CPU,所以自旋一定次数后使用Thread.yield()方法来暂停当前正在执行的线程,并执行其他线程。
2)tryTransfer:试探是否有消费者等待接收元素,有就返回true,没有就返回false

5.2.7 LinkedBlockingDeque

是一个由链表组成的双向阻塞队列

5.2.8 阻塞队列实现原理

使用通知模式实现,所谓通知模式,当生产者添加元素被阻塞,当消费者消费了一个元素后,会通知生产者当前队列可用,代码的底层实现,是用了LockSupport的park/unpark来实现阻塞的。

5.3 Fork/Join框架

Fork/Join框架是一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇聚每个小任务结果后得到大任务结果的框架。

5.3.1 工作窃取算法

是指某个线程从其他线程任务队列里窃取任务来执行,通常使用双端队列,一个从头部拿,一个从尾部拿。

优点:充分利用线程进行并行计算,减少了线程间的竞争
缺点:在某些情况下还是存在竞争,比如双端队列里只有一个任务时,并且该算法会消耗更多的系统资源,比如创建多个线程和多个双端队列。

5.3.2 异常处理

由于在主线程里没办法直接捕获异常,可以通过ForkJoinTask的isCompletedAbnormally

5.3.3 实现原理

fork实现原理
当调用ForkJoinTask的fork方法时,会调用ForkJoinWorkerThread的push方法,并立即返回this,push方法
把当前任务放到ForkJoinTask数组队列里,然后调用ForkJoinPool的signalWork()方法唤醒或者创建一个工作线程来执行任务。
join实现原理
作用是阻塞当前线程并等待获取结果,首先调用doJoin()方法判断当前任务的状态,如果是已完成直接返回结果
如果是取消或者抛出异常,会抛出异常

六、原子类

6.1 原子更新基本类型

AtomicBoolean、AtomicInteger、AtomicLong(AtomicLong和AtomicBoolean底层都是用AtomicInteger来实现),以AtomicInteger为例
1)int addAndGet 以原子方式将输入的数值与实例中的值相加,并返回结果
2)boolean compareAndSet(int expect, int update) 如果输入的值等于预期值,则以原子方式设置为输入的值
3)int getAndIncrement() 以原子方式将当前值加一,并返回自增前的值
4)void lazySet(int newValue) 延迟设置值,调用方法后的一小段时间内,其他线程还是可以读到旧值的
5)int getAndSet(int newValue) 以原子方式更新成newValue值,并返回旧值

6.2 原子更新数组

AtomicLongArray,AtomicReferenceArray,AtomicIntegerArray,以AtomicIntegerArray为例:
1)int addAndGet(int i, int delta) 以原子方式将输入值与数组中索引i的元素加一
2)Boolean compareAndSet(int i, int expect, int update) 如果当前值等于预期值,则以原子方式将数组位置i的元素设置为update值

通过构造方法传进去的数组,AtomicIntegerArray会复制一份,当修改时不会影响传入的数组。

6.3 原子更新引用类型

AtomicReference(原子更新引用类型)、AtomicRefenceFieldUpdater(原子更新引用类型里的字段)、AtomICMarkableReference(原子更新带有标记的引用类型),可以用来更新多个变量,以AtomicReference为例
调用compareAndSet进行原子更新

6.4 原子更新字段类

如果需要更新某个类里面的某个字段时,就需要原子更新字段类,AtomicIntegerFieldUpdater(原子更新整型字段更新器),AtomicLongFieldUpdater(原子更新长整型更新器),AtomicStampedReference(原子更新带有版本号的引用类型,可以解决CAS更新的ABA问题)

七、并发工具类

7.1 等待多线程完成的CountDownLatch

允许一个或多个线程等待其他线程完成操作,构造函数接受一个int的参数作为计数器,当调用countDown()方法时计数器会减一,await()方法会阻塞当前线程,直到计数器变为0,await()方法有个带超时时间的方法,可以避免无线等待

7.2 同步屏障CyclicBarrier

让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障后,所有线程才能继续执行,还有个更高级的构造方法CyclicBarrier(int paries, Runnable barrierAction) 当线程都到达屏障后,优先执行barrierAction,方便处理更复杂的业务场景

7.3 CountDownLatch和CyclicBarrier比较

CountDownLatch只能使用一次,而CyclicBarrier可以用reset()方法重置,CyclicBarrier可以处理更复杂的场景

7.4 控制并发线程数的Semaphore

是用来控制同时访问特定资源的线程数量,通过协调线程,保证合理利用公共资源,可以用来做流量控制

7.5 线程间交换数据的Exchange

是一个用于线程间协作的工具类,可以进行线程间数据交换,它提供一个同步点,在这个点上,两个线程可以交换彼此数据,通过exchange() 方法交换数据,可用于遗传算法

八、线程池

8.1 实现原理

当提交一个新任务时
1)先判断核心线程池里的线程是否都在执行任务,如果不是则选取一个线程执行任务,如果核心线程都在执行任务,进入下一个流程;
2)判断工作队列是否已经满了,如果没满,就存到工作队列,如果满了,就进入下个流程;
3)判断线程池的线程是否都处于工作模式,如果没有就创建一个新的工作线程来执行任务,如果已经满了,就交给饱和策略处理任务

并发编程 - 图8
并发编程 - 图9

8.2 使用

使用new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory,handler)
1)corePoolSize,核心线程的数量,当提交一个任务时,如果核心线程数量还没有达到corePoolSize,会直接创建新线程执行,如果调用prestartAllCoreThreads()方法,会提前创建并启动所有核心线程
2)maximumPoolSize,线程池最大数量,当提交新的任务时,如果任务队列满了,并且已创建的线程数小于maximumPoolSize的值,会创建新的线程,当任务队列为无界队列的时候,这个参数不起作用,因为任务队列不会满
3)keepAliveTime,线程空闲后的存活时间
4)unit,keepAliveTime的单位
5)workQueue,任务队列,可以选择ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、PriorityBlockingQueue
6)threadFactory,线程工厂,用来自定义创建线程的一些参数,例如线程名称,组的信息,方便查找问题
7)handler,拒绝策略,挡队列和线程池都满了的时候,采取的措施,jdk提供了一下几种:

  • AbortPolicy:直接抛出异常

  • CallerRunsPolicy:由调用者执行

  • discardOldestPolicy:丢弃最近的一个任务,并执行当前任务

  • discardPolicy:直接丢弃

8.3 提交任务

提交任务有execute()提交不需要返回值的任务,submit()提交需要返回值的任务,返回一个Future对象,可以调用get方法获取返回值。

8.4 关闭线程池

可以调用shutdown或shutdownNow方法来关闭线程池,通常使用shutdown,如果不需要等任务执行完,可以调用shuntdownNow

8.5 合理配置

任务的特性可以从以下几点分析:
1)任务的性质:CPU密集型、IO密集型任务和混合任务
2)任务的优先级:高、中、低
3)任务的执行时长:长、中、短
4)任务依赖性:是否依赖其他系统资源,比如数据库连接

cpu密集型任务应尽量配置比较少的线程,IO密集型任务应配置尽可能多的线程,混合型任务,如果能拆分成一个CPU密集型任务和IO密集型任务,时间相差不多的话,分解执行任务会好点
优先级不同的任务可以使用优先级队列
执行时间可以交给不同规模的线程池来处理,或者可以使用优先级队列,让时间短的先执行
依赖性的任务,应该设置线程池尽量大,才能更好利用CPU

8.6 监控

  1. taskCount:线程池需要执行的任务数量

  2. completedTaskCount:线程池在运行过程中已完成的任务数

  3. largestPoolSize:线程池里曾经创建过的最大线程数量

  4. getPoolSize:线程池的线程数,只要线程池不销毁,这个只增不减

  5. getActiveCount:获取活动的线程数