1.概念

多线程是Java中不可避免的一个重要主体。从本章开始,我们将展开对多线程的学习。接下来的内容,是对“JDK中新增JUC包”之前的Java多线程内容的讲解,涉及到的内容包括,Object类中的wait(), notify()等接口;Thread类中的接口;synchronized关键字。
注:JUC包是指,Java.util.concurrent包,它是由Java大师Doug Lea完成并在JDK1.5版本添加到Java中的。
在进入后面章节的学习之前,先对了解一些多线程的相关概念。
线程状态图
image.png
说明:
线程共包括以下5种状态。

  1. 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
  2. 就绪状态(Runnable) : 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
  3. 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
  4. 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

    (01) 等待阻塞 — 通过调用线程的wait()方法,让线程等待某工作的完成。
    (02) 同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
    (03) 其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  5. 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。


    这5种状态涉及到的内容包括Object类, Thread和synchronized关键字。这些内容我们会在后面的章节中逐个进行学习。
    Object类,定义了wait(), notify(), notifyAll()等休眠/唤醒函数。
    Thread类,定义了一些列的线程操作函数。例如,sleep()休眠函数, interrupt()中断函数, getName()获取线程名称等。
    synchronized,是关键字;它区分为synchronized代码块和synchronized方法。synchronized的作用是让线程获取对象的同步锁。
    在后面详细介绍wait(),notify()等方法时,我们会分析为什么“wait(), notify()等方法要定义在Object类,而不是Thread类中”。

run( ): 启动线程由主线程执行。
start( ) :让线程进入可执行状态 由cpu决定执行顺序。

线程分类 :守护线程,非守护线程
守护线程 :是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因 此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

2.基础知识

2.1 实现Runable接口

  1. class ThreadRunable implements Runnable{
  2. @Override
  3. public void run() {
  4. System.out.println("i am from runable");
  5. }
  6. }

2.2 继承Thread线程类

  1. class ThreadExtend extends Thread{
  2. @Override
  3. public void run() {
  4. System.out.println("i am from thread");
  5. }
  6. }

启动类

  1. public class TestThread1 {
  2. public static void main(String[] args) {
  3. ThreadExtend t = new ThreadExtend();
  4. t.start();
  5. Thread t1 = new Thread(new ThreadRunable());
  6. Thread t2 = new Thread(new ThreadRunable());
  7. t1.run();
  8. t2.run();
  9. }
  10. }

实现Runable的优点

  1. 避免继承的局限性,一个类只能继承一个父类
  2. 多线程共享一个接口的子类的对象,适合多个线程处理同一份资源。

2.3 生产者消费者

2.3.1 名词解释

  1. join:

让当前cpu执行的线程等待 直到调用join( ) 的那个线程执行完成后 继续执行等待的线程
不传入时间默认join(0) 传入0 当调用join() 的线程isAlive活着的时候 是让调用join的线程执行完成后 再执行其他线程

  1. public class TestThreadJoin {
  2. public static void main(String[] args) {
  3. Thread t1 = new Thread(new ThreadRunable3());
  4. t1.start();
  5. try {
  6. t1.join(2000); //强制让cpu执行2s
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. System.out.println("==============================================");
  11. }
  12. }
  13. class ThreadRunable3 implements Runnable{
  14. @Override
  15. public void run() {
  16. try {
  17. for (int i = 0; i < 5; i++) {
  18. Thread.sleep(1000);
  19. System.out.println("i am from runable " + Thread.currentThread().getName());
  20. }
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. }
  26. i am from runable Thread-0
  27. ==============================================
  28. i am from runable Thread-0
  29. i am from runable Thread-0
  30. i am from runable Thread-0
  31. i am from runable Thread-0
  32. ==============================================
  33. i am from runable Thread-0
  34. i am from runable Thread-0
  35. i am from runable Thread-0
  36. i am from runable Thread-0
  37. i am from runable Thread-0
  1. synchronized

synchronized(this) 其中this 指的是锁住当前类的对象, 所以当是继承Thread时 实例化多个Thread对象并不能 锁住多个对象的方法
synchronize 被修饰在方法和代码块上 使用在代码块上更加能提高效率
结论:

  • 当一个线程访问某个对象的synchronize关键字修饰代码块或者方法 ,其他线程访问该对象的 该synchronize关键字修饰代码块或者方法被阻塞
  • 当一个线程访问某个对象的synchronize关键字修饰代码块或者方法 ,其他线程还可以访问该对象的其它非同步的代码块
  • 当一个线程访问某个对象的synchronize关键字修饰代码块或者方法 ,其他线程访问该对象的其它同步的代码块被阻塞

  1. notify 基于线程调度算法,随机唤醒当前对象一个等待的线程。
  2. notifyAll 基于线程调度算法,随机唤醒当前对象所有等待的线程。

线程调度算法:

  • 先进先出
  • 最短耗时任务优先
  • 时间片轮转
  • 最大最小公平算法
    1. wait 的作用是让当前线程进入等待状态,同时,wait( )也会让当前线程释放它所持有的锁。”直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法”,当前线程被唤醒(进入”就绪状态”)
    2. sleep 让线程睡眠,不释放锁

2.3.2 实例

  1. public class TestPC {
  2. public static void main(String[] args) {
  3. YaoDian yaoDian = new YaoDian();
  4. Thread p1 = new Thread(new Producer(yaoDian));
  5. // Thread p2 = new Thread(new Producer(yaoDian));
  6. p1.start();
  7. // p2.start();
  8. Thread c1 = new Thread(new Customer(yaoDian));
  9. // Thread c2 = new Thread(new Customer(yaoDian));
  10. c1.start();
  11. // c2.start();
  12. }
  13. }
  14. class KouZhao {
  15. KouZhao(String code){
  16. this.code = code;
  17. }
  18. private String code;
  19. public String getCode() {
  20. return code;
  21. }
  22. public void setCode(String code) {
  23. this.code = code;
  24. }
  25. }
  26. class YaoDian{
  27. //线程安全集合
  28. public static volatile List<KouZhao> kzList = new ArrayList<>();
  29. public synchronized void inKz(KouZhao kouZhao) throws Exception{
  30. while (kzList.size() == 3){ //使用while 的意思是 当前线程被唤醒后继续判断
  31. System.out.println("口罩库存已满:" + kzList.size());
  32. this.wait();
  33. }
  34. Thread.sleep(100);
  35. System.out.println("生产口罩:" + kouZhao.getCode());
  36. kzList.add(kouZhao);
  37. this.notifyAll();
  38. }
  39. public synchronized void outKz() throws Exception{
  40. while (kzList.size() == 0){
  41. System.out.println("口罩库存不足!");
  42. this.wait();
  43. }
  44. Thread.sleep(500);
  45. System.out.println("消耗口罩:" + kzList.get(0).getCode() + "剩下口罩数量" + (kzList.size() - 1));
  46. kzList.remove(kzList.get(0));
  47. this.notifyAll();
  48. }
  49. }
  50. //消费者
  51. class Producer implements Runnable{
  52. private YaoDian yaoDian;
  53. Producer(YaoDian yaoDian){
  54. this.yaoDian = yaoDian;
  55. }
  56. @Override
  57. public void run() {
  58. try {
  59. for (int i = 0; i < 5; i++) {
  60. KouZhao kz = new KouZhao("BH" + i);
  61. yaoDian.inKz(kz);
  62. }
  63. } catch (Exception e) {
  64. e.printStackTrace();
  65. }
  66. }
  67. }
  68. //生产者
  69. class Customer implements Runnable{
  70. private YaoDian yaoDian;
  71. Customer(YaoDian yaoDian){
  72. this.yaoDian = yaoDian;
  73. }
  74. @Override
  75. public void run() {
  76. try {
  77. // while (true) {
  78. for (int i = 0; i < 5; i++) {
  79. yaoDian.outKz();
  80. }
  81. } catch (Exception e) {
  82. e.printStackTrace();
  83. }
  84. }
  85. }

3.原理

3.1 CAS compare and swap

结构图:
image.png
解释:先读取当前值 a 假设 a = 1,修改后再次读取a的值 ,假如两次一样 则可以修改,不一样不能修改。
应用:java concurrent 包下面的 原子类的操作。如: AtomicInteger
底层:调用汇编指令 lock cmpxchg

3.2 volatile

1 : 保持线程可见性。

  1. public class TestVolatile {
  2. public static void main(String[] args) {
  3. Thread t = new Thread(new VolatileClass());
  4. t.start();
  5. //
  6. while (true){
  7. if(VolatileClass.a == 1){
  8. System.out.println("读取到了a = 1");
  9. }
  10. }
  11. }
  12. }
  13. class VolatileClass implements Runnable{
  14. public static int a = 0;
  15. @Override
  16. public void run() {
  17. try {
  18. Thread.sleep(2000);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. a = 1;
  23. }
  24. }

image.png
解释: 线程会读取常量池的数据,修改的话会同步到常量池中,因为上面的例子中,子线程睡眠2秒导致主线程执行了 while的内存,所以不会再主动修改a的数值。
加上volatile 关键字后: 相当于每一次获取值是直接从常量池 直接重新拿生成的新的副本。

2:禁止指令重排 加上 lock

  1. class ClassSingleton{
  2. public static volatile ClassSingleton classSingleton = null;
  3. private ClassSingleton(){}
  4. private ClassSingleton getSingleton(){
  5. if(null == classSingleton){
  6. synchronized (ClassSingleton.class){
  7. if(null == classSingleton){
  8. classSingleton = new ClassSingleton();
  9. }
  10. }
  11. }
  12. return classSingleton;
  13. }
  14. }

在单例设计模式中,双重check加锁实现多线程的单例模式,在定义class时,在上volatile防止在指令重排时,对象new 一半的时候返回。

3.3 synchronized

JVM 2.6 有关于对象的内存结构的详细分析,我们知道锁信息是存放在对象头信息中的。

synchronized :过程分析,

  1. 首先是加上偏向锁(就是在对象头的前三位中存放这个要加锁的线程id,如果下次有线程执行的时候,发现id相同直接执行,以增加效率),以避免CPU效率低。这个叫偏向锁
  2. 假如是多线程,就是说其他线程也要和之前的线程来争抢这把锁,会通过CAS的方式来获取到这把锁。

    假如现在头信息存储的线程id是1,线程1去获取锁的时候首选读取是1,然修改成1,然后再读一次对象头的线程id还是1,则算获取到锁。线程2也执行类型过程:首先读取线程id是1,然后准备修改为2,修改前再去读一次还是1,则修改成功,这个过程只有一个线程能获取到当前锁。 这个过程叫做自旋锁

  3. 如果自旋次数超过10time 就进入重量级锁。竞争太激烈的情况。

—2021.07.03
就是jvm给synchronized这个方法做了一些优化,并不是最开始就使用指令把同步代码块锁住,当一段同步代码没有被线程访问的时候,这个时候是处于无锁状态(0),如果这个同步代码块一只被同一个线程访问,那么jvm会给这段代码块加上偏量锁(1),如果不断的有线程进来,那么其他线程开始竞争的时候使用的是cas方式去竞争,这个时候jvm又会把锁升级成自旋锁(00),如果10次都没获取到锁,则进入重量级锁(11),开启指令。

用户态:大部分操作程序叫 用户态,但是一些特殊操作需要调用内核。比如加锁。
内核态:指的是和硬件的内核操作。

3.4 AQS

AbstractQueuedSynchronizer(以下简写AQS)这个抽象类

sync : 上面说了 是 偏向锁 自旋锁 重量级锁的演变
ReentrantLock :jvm实现

  1. 自旋
  2. park
  3. CAS

加锁过程:判断是否有锁,没有 -> 判断是否需要排队,不需要就直接返回。有锁,入队,

TODO

1.使用park 实现锁

  1. public class TestPark {
  2. public static void main(String[] args) {
  3. Thread t = new Thread(new ParkClass());
  4. t.start();
  5. try {
  6. Thread.sleep(1000);
  7. } catch (InterruptedException e) {
  8. e.printStackTrace();
  9. }
  10. System.out.println("main");
  11. LockSupport.unpark(t);
  12. }
  13. }
  14. class ParkClass implements Runnable{
  15. @Override
  16. public void run() {
  17. System.out.println("执行 1");
  18. LockSupport.park();
  19. System.out.println("执行2");
  20. }
  21. }

4.线程池

4.1概念

4.1.1 线程模型:

用户线程(ULT):用户程序实现,不依赖操作系统核心,应用提供创建,同步,调度,管理线程的函数来控制用户线程,不需要 用户态/内核态切换 速度快,内核对ULT 无感知,线程阻塞则进程阻塞。

内核线程(KLT):系统内核管理线程,内核保存线程状态和上下文信息,线程阻塞不会引起进程阻塞,多线程处理器并行运行,线程创建调度和管理 由内核完成,效率不ULT慢。

image.png

java 创建线程采用的是 KLT.
image.png

综上:线程是稀缺资源,创建或者销毁耗费资源,java创建线程是内核操作,需要频繁切换,线程池就是一个线程缓存,负责对线程的统一分配,调优和 调度。

阻塞队列:在任意时刻,只有一个线程可以对该队列进行入队或者出队操作。是线程安全的。

4.2基本原理:

image.png
线程五种状态:

  • running

能接受新的任务 以及处理添加的任务

  • shutdown

不接受新任务 可以处理添加的任务

  • stop

不接新的任务,不处理已经添加的任务,并中断正在处理的任务

  • tidying

所有的任务已经终止,ctl记录的任务数量为0 ctl负责记录线程池的运行状态以及线程数量

  • terminated

线程池彻底终止

shutdown:不接受新任务 可以处理添加的任务
shutdown now:stop

image.png
执行task的时候 会调用execute 方法

  1. int c = ctl.get();
  2. // 1. 如果工作线程数小于核心线程数(corePoolSize),则创建一个工作线程执行任务
  3. if (workerCountOf(c) < corePoolSize) {
  4. if (addWorker(command, true))
  5. return;
  6. c = ctl.get();
  7. }
  8. // 2. 如果当前是running状态,并且任务队列能够添加任务
  9. if (isRunning(c) && workQueue.offer(command)) {
  10. int recheck = ctl.get();
  11. if (! isRunning(recheck) && remove(command))
  12. reject(command);
  13. else if (workerCountOf(recheck) == 0)
  14. addWorker(null, false);
  15. }
  16. // 3. 队列已经满了的情况下,则新启动一个工作线程来执行任务
  17. else if (!addWorker(command, false))
  18. reject(command);

而在addWorker方法中还存在有一些必要的判断逻辑,比如当前线程池是否是非running状态,队列是否为空等条件,当然最主要的逻辑还是判断当前工作线程数量是否大于maximumPoolSize以及启动工作线程执行任务。

  1. private boolean addWorker(Runnable firstTask, boolean core) {
  2. retry:
  3. for (;;) {
  4. for (;;) {
  5. int wc = workerCountOf(c);
  6. // 1. 判断当前工作线程是否满足条件
  7. if (wc >= CAPACITY ||
  8. wc >= (core ? corePoolSize : maximumPoolSize))
  9. return false;
  10. // 2. 增加工作线程数量
  11. if (compareAndIncrementWorkerCount(c))
  12. break retry;
  13. c = ctl.get(); // Re-read ctl
  14. if (runStateOf(c) != rs)
  15. continue retry;
  16. }
  17. }
  18. // 3. 创建工作线程
  19. w = new Worker(firstTask);
  20. final Thread t = w.thread;
  21. workers.add(w);
  22. if (workerAdded) {
  23. // 4. 运行工作线程
  24. t.start();
  25. workerStarted = true;
  26. }
  27. return workerStarted;
  28. }

总结:

  1. 提交一个任务,如果线程池中的工作线程数小于corePoolSize,则新建一个工作线程执行任务
  2. 如果线程池当前的工作线程已经等于了corePoolSize,则将新的任务放入到工作队列中正在执行
  3. 如果工作队列已经满了,并且工作线程数小于maximumPoolSize,则新建一个工作线程来执行任务
  4. 如果当前线程池中工作线程数已经达到了maximumPoolSize,而新的任务无法放入到任务队列中,则采用对应的策略进行相应的处理(默认是拒绝策略)

4.3线程池线程安全

  1. private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
  2. private static final int COUNT_BITS = Integer.SIZE - 3;
  3. private static final int CAPACITY = (1 << COUNT_BITS) - 1;
  4. // runState is stored in the high-order bits
  5. private static final int RUNNING = -1 << COUNT_BITS;
  6. private static final int SHUTDOWN = 0 << COUNT_BITS;
  7. private static final int STOP = 1 << COUNT_BITS;
  8. private static final int TIDYING = 2 << COUNT_BITS;
  9. private static final int TERMINATED = 3 << COUNT_BITS;

线程池用整个int 32 为去记录整个线程池的生命状态

高三位记录记录线程池生命状态,低29记录当前线程数

4.4java提供的四种常用线程池

4.4.1 FixedThreadPool()

  1. 固定大小的线程池,可以指定线程池的大小,该线程池corePoolSize和maximumPoolSize相等,阻塞队列使用的是LinkedBlockingQueue,大小为整数最大值。
  2. 该线程池中的线程数量始终不变,当有新任务提交时,线程池中有空闲线程则会立即执行,如果没有,则会暂存到阻塞队列。对于固定大小的线程池,不存在线程数量的变化。同时使用无界的LinkedBlockingQueue来存放执行的任务。
  3. 当任务提交十分频繁的时候,LinkedBlockingQueue迅速增大,存在着耗尽系统资源的问题。而且在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源,需要shutdown。。

    1. public class FixPoolDemo {
    2. private static Runnable getThread(final int i) {
    3. return new Runnable() {
    4. @Override
    5. public void run() {
    6. try {
    7. Thread.sleep(500);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. System.out.println(i);
    12. }
    13. };
    14. }
    15. public static void main(String args[]) {
    16. ExecutorService fixPool = Executors.newFixedThreadPool(5);
    17. for (int i = 0; i < 10; i++) {
    18. fixPool.execute(getThread(i));
    19. }
    20. fixPool.shutdown();
    21. }
    22. }

4.4.2 SingleThreadExecutor()

定长线程池:
可控制线程最大并发数(同时执行的线程数)
超出的线程会在队列中等待
按照先入先出的顺序执行任务

  1. public class SingPoolDemo {
  2. private static Runnable getThread(final int i){
  3. return new Runnable() {
  4. @Override
  5. public void run() {
  6. try {
  7. Thread.sleep(500);
  8. } catch (InterruptedException e) {
  9. e.printStackTrace();
  10. }
  11. System.out.println(i);
  12. }
  13. };
  14. }
  15. public static void main(String args[]) throws InterruptedException {
  16. ExecutorService singPool = Executors.newSingleThreadExecutor();
  17. for (int i=0;i<10;i++){
  18. singPool.execute(getThread(i));
  19. }
  20. singPool.shutdown();
  21. }
  22. }

4.4.3 ScheduledThreadPool()

定时线程池,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据
scheduleAtFixedRate:是以固定的频率去执行任务,周期是指每次执行任务成功执行之间的间隔。
schedultWithFixedDelay:是以固定的延时去执行任务,延时是指上一次执行成功之后和下一次开始执行的之前的时间。

  1. public class ScheduledExecutorServiceDemo {
  2. public static void main(String args[]) {
  3. ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
  4. ses.scheduleAtFixedRate(new Runnable() {
  5. @Override
  6. public void run() {
  7. try {
  8. Thread.sleep(4000);
  9. System.out.println(Thread.currentThread().getId() + "执行了");
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. }, 0, 2, TimeUnit.SECONDS);
  15. }
  16. }

4.4.4 CachedThreadPool()

  1. 缓存线程池,缓存的线程默认存活60秒。线程的核心池corePoolSize大小为0,核心池最大为Integer.MAX_VALUE,阻塞队列使用的是SynchronousQueue。是一个直接提交的阻塞队列他总会迫使线程池增加新的线程去执行新的任务。
  2. 在没有任务执行时,当线程的空闲时间超过keepAliveTime(60秒),则工作线程将会终止被回收,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销。如果同时又大量任务被提交,而且任务执行的时间不是特别快,那么线程池便会新增出等量的线程池处理任务,这很可能会很快耗尽系统的资源。

    1. public class CachePool {
    2. private static Runnable getThread(final int i){
    3. return new Runnable() {
    4. @Override
    5. public void run() {
    6. try {
    7. Thread.sleep(1000);
    8. }catch (Exception e){
    9. }
    10. System.out.println(i);
    11. }
    12. };
    13. }
    14. public static void main(String args[]){
    15. ExecutorService cachePool = Executors.newCachedThreadPool();
    16. for (int i=1;i<=10;i++){
    17. cachePool.execute(getThread(i));
    18. }
    19. }
    20. }

4.5阿里规范

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
  主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
  主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

阿里巴巴建立手动创建线程池、

手动创建线程池有几个注意点
1.任务独立。如何任务依赖于其他任务,那么可能产生死锁。例如某个任务等待另一个任务的返回值或执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁。
2.合理配置阻塞时间过长的任务。如果任务阻塞时间过长,那么即使不出现死锁,线程池的性能也会变得很糟糕。在Java并发包里可阻塞方法都同时定义了限时方式和不限时方式。例如
Thread.join,BlockingQueue.put,CountDownLatch.await等,如果任务超时,则标识任务失败,然后中止任务或者将任务放回队列以便随后执行,这样,无论任务的最终结果是否成功,这种办法都能够保证任务总能继续执行下去。
3.设置合理的线程池大小。只需要避免过大或者过小的情况即可,上文的公式线程池大小=NCPU *UCPU(1+W/C)
4.选择合适的阻塞队列。newFixedThreadPool和newSingleThreadExecutor都使用了无界的阻塞队列,无界阻塞队列会有消耗很大的内存,如果使用了有界阻塞队列,它会规避内存占用过大的问题,但是当任务填满有界阻塞队列,新的任务该怎么办?在使用有界队列是,需要选择合适的拒绝策略,队列的大小和线程池的大小必须一起调节。对于非常大的或者无界的线程池,可以使用SynchronousQueue来避免任务排队,以直接将任务从生产者提交到工作者线程。

在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
下面我们就对ThreadPoolExecutor的使用方法进行一个详细的概述。
首先看下ThreadPoolExecutor的构造函数

  1. public ThreadPoolExecutor(int corePoolSize,
  2. int maximumPoolSize,
  3. long keepAliveTime,
  4. TimeUnit unit,
  5. BlockingQueue<Runnable> workQueue,
  6. ThreadFactory threadFactory,
  7. RejectedExecutionHandler handler) {
  8. if (corePoolSize < 0 ||
  9. maximumPoolSize <= 0 ||
  10. maximumPoolSize < corePoolSize ||
  11. keepAliveTime < 0)
  12. throw new IllegalArgumentException();
  13. if (workQueue == null || threadFactory == null || handler == null)
  14. throw new NullPointerException();
  15. this.acc = System.getSecurityManager() == null ?
  16. null :
  17. AccessController.getContext();
  18. this.corePoolSize = corePoolSize;
  19. this.maximumPoolSize = maximumPoolSize;
  20. this.workQueue = workQueue;
  21. this.keepAliveTime = unit.toNanos(keepAliveTime);
  22. this.threadFactory = threadFactory;
  23. this.handler = handler;
  24. }

构造函数的参数含义如下:
corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;
unit:keepAliveTime的单位
workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;
threadFactory:线程工厂,用于创建线程,一般用默认即可;
handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;
接下来我们对其中比较重要参数做进一步的了解:
一、workQueue任务队列
上面我们已经介绍过了,它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列;
1、直接提交队列:设置为SynchronousQueue队列,SynchronousQueue是一个特殊的BlockingQueue,它没有容量,没执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一个删除操作也都要等待对应的插入操作。

  1. public class ThreadPool {
  2. private static ExecutorService pool;
  3. public static void main( String[] args )
  4. {
  5. //maximumPoolSize设置为2 ,拒绝策略为AbortPolic策略,直接抛出异常
  6. pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
  7. for(int i=0;i<3;i++) {
  8. pool.execute(new ThreadTask());
  9. }
  10. }
  11. }
  12. public class ThreadTask implements Runnable{
  13. public ThreadTask() {
  14. }
  15. public void run() {
  16. System.out.println(Thread.currentThread().getName());
  17. }
  18. }

输出:

  1. pool-1-thread-1
  2. pool-1-thread-2
  3. Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.hhxx.test.ThreadTask@55f96302 rejected from java.util.concurrent.ThreadPoolExecutor@3d4eac69[Running, pool size = 2, active threads = 0, queued tasks = 0, completed tasks = 2]
  4. at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(Unknown Source)
  5. at java.util.concurrent.ThreadPoolExecutor.reject(Unknown Source)
  6. at java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)
  7. at com.hhxx.test.ThreadPool.main(ThreadPool.java:17)

可以看到,当任务队列为SynchronousQueue,创建的线程数大于maximumPoolSize时,直接执行了拒绝策略抛出异常。
使用SynchronousQueue队列,提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize,则尝试创建新的进程,如果达到maximumPoolSize设置的最大值,则根据你设置的handler执行拒绝策略。因此这种方式你提交的任务不会被缓存起来,而是会被马上执行,在这种情况下,你需要对你程序的并发量有个准确的评估,才能设置合适的maximumPoolSize数量,否则很容易就会执行拒绝策略;
2、有界的任务队列:有界的任务队列可以使用ArrayBlockingQueue实现,如下所示

  1. static ExecutorService fixPool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
  2. new LinkedBlockingQueue<Runnable>(2000),
  3. Executors.defaultThreadFactory(),
  4. new ThreadPoolExecutor.CallerRunsPolicy());

使用ArrayBlockingQueue有界任务队列,若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在corePoolSize以下,反之当任务队列已满时,则会以maximumPoolSize为最大线程数上限。

3、无界的任务队列:有界任务队列可以使用LinkedBlockingQueue实现,如下所示

  1. static ExecutorService fixPool0 = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
  2. new LinkedBlockingQueue<Runnable>(),
  3. Executors.defaultThreadFactory(),
  4. new ThreadPoolExecutor.AbortPolicy());

使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你corePoolSize设置的数量,也就是说在这种情况下maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到corePoolSize后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。
4、优先任务队列:优先任务队列通过PriorityBlockingQueue实现,下面我们通过一个例子演示下

  1. public class ThreadPool {
  2. private static ExecutorService pool;
  3. public static void main( String[] args )
  4. {
  5. //优先任务队列
  6. pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new PriorityBlockingQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
  7. for(int i=0;i<20;i++) {
  8. pool.execute(new ThreadTask(i));
  9. }
  10. }
  11. }
  12. public class ThreadTask implements Runnable,Comparable<ThreadTask>{
  13. private int priority;
  14. public int getPriority() {
  15. return priority;
  16. }
  17. public void setPriority(int priority) {
  18. this.priority = priority;
  19. }
  20. public ThreadTask() {
  21. }
  22. public ThreadTask(int priority) {
  23. this.priority = priority;
  24. }
  25. //当前对象和其他对象做比较,当前优先级大就返回-1,优先级小就返回1,值越小优先级越高
  26. public int compareTo(ThreadTask o) {
  27. return this.priority>o.priority?-1:1;
  28. }
  29. public void run() {
  30. try {
  31. //让线程阻塞,使后续任务进入缓存队列
  32. Thread.sleep(1000);
  33. System.out.println("priority:"+this.priority+",ThreadName:"+Thread.currentThread().getName());
  34. } catch (InterruptedException e) {
  35. // TODO Auto-generated catch block
  36. e.printStackTrace();
  37. }
  38. }
  39. }

我们来看下执行的结果情况
priority:0,ThreadName:pool-1-thread-1
priority:9,ThreadName:pool-1-thread-1
priority:8,ThreadName:pool-1-thread-1
priority:7,ThreadName:pool-1-thread-1
priority:6,ThreadName:pool-1-thread-1
priority:5,ThreadName:pool-1-thread-1
priority:4,ThreadName:pool-1-thread-1
priority:3,ThreadName:pool-1-thread-1
priority:2,ThreadName:pool-1-thread-1
priority:1,ThreadName:pool-1-thread-1

大家可以看到除了第一个任务直接创建线程执行外,其他的任务都被放入了优先任务队列,按优先级进行了重新排列执行,且线程池的线程数一直为corePoolSize,也就是只有一个。
通过运行的代码我们可以看出PriorityBlockingQueue它其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize的数量,只不过其他队列一般是按照先进先出的规则处理任务,而PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行。
二、拒绝策略
一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池”超载”的情况。ThreadPoolExecutor自带的拒绝策略如下:
1、AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作;
2、CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行;
3、DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;
4、DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失;
以上内置的策略均实现了RejectedExecutionHandler接口,当然你也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略,我们看下示例代码:

  1. public class ThreadPool {
  2. private static ExecutorService pool;
  3. public static void main( String[] args )
  4. {
  5. //自定义拒绝策略
  6. pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5),
  7. Executors.defaultThreadFactory(), new RejectedExecutionHandler() {
  8. public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
  9. System.out.println(r.toString()+"执行了拒绝策略");
  10. }
  11. });
  12. for(int i=0;i<10;i++) {
  13. pool.execute(new ThreadTask());
  14. }
  15. }
  16. }
  17. public class ThreadTask implements Runnable{
  18. public void run() {
  19. try {
  20. //让线程阻塞,使后续任务进入缓存队列
  21. Thread.sleep(1000);
  22. System.out.println("ThreadName:"+Thread.currentThread().getName());
  23. } catch (InterruptedException e) {
  24. // TODO Auto-generated catch block
  25. e.printStackTrace();
  26. }
  27. }
  28. }

输出结果:
com.hhxx.test.ThreadTask@33909752执行了拒绝策略
com.hhxx.test.ThreadTask@55f96302执行了拒绝策略
com.hhxx.test.ThreadTask@3d4eac69执行了拒绝策略
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1
ThreadName:pool-1-thread-2
ThreadName:pool-1-thread-1

可以看到由于任务加了休眠阻塞,执行需要花费一定时间,导致会有一定的任务被丢弃,从而执行自定义的拒绝策略;
三、ThreadFactory自定义线程创建
线程池中线程就是通过ThreadPoolExecutor中的ThreadFactory,线程工厂创建的。那么通过自定义ThreadFactory,可以按需要对线程池中创建的线程进行一些特殊的设置,如命名、优先级等,下面代码我们通过ThreadFactory对线程池中创建的线程进行记录与命名

  1. public class ThreadPool {
  2. private static ExecutorService pool;
  3. public static void main( String[] args )
  4. {
  5. //自定义线程工厂
  6. pool = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5),
  7. new ThreadFactory() {
  8. public Thread newThread(Runnable r) {
  9. System.out.println("线程"+r.hashCode()+"创建");
  10. //线程命名
  11. Thread th = new Thread(r,"threadPool"+r.hashCode());
  12. return th;
  13. }
  14. }, new ThreadPoolExecutor.CallerRunsPolicy());
  15. for(int i=0;i<10;i++) {
  16. pool.execute(new ThreadTask());
  17. }
  18. }
  19. }
  20. public class ThreadTask implements Runnable{
  21. public void run() {
  22. //输出执行线程的名称
  23. System.out.println("ThreadName:"+Thread.currentThread().getName());
  24. }
  25. }

我们看下输出结果

线程118352462创建
线程1550089733创建
线程865113938创建
ThreadName:threadPool1550089733
ThreadName:threadPool118352462
线程1442407170创建
ThreadName:threadPool1550089733
ThreadName:threadPool1550089733
ThreadName:threadPool1550089733
ThreadName:threadPool865113938
ThreadName:threadPool865113938
ThreadName:threadPool118352462
ThreadName:threadPool1550089733
ThreadName:threadPool1442407170

可以看到线程池中,每个线程的创建我们都进行了记录输出与命名。

四、ThreadPoolExecutor扩展
ThreadPoolExecutor扩展主要是围绕beforeExecute()、afterExecute()和terminated()三个接口实现的,
1、beforeExecute:线程池中任务运行前执行
2、afterExecute:线程池中任务运行完毕后执行
3、terminated:线程池退出后执行
通过这三个接口我们可以监控每个任务的开始和结束时间,或者其他一些功能。下面我们可以通过代码实现一下

  1. public class ThreadPool {
  2. private static ExecutorService pool;
  3. public static void main( String[] args ) throws InterruptedException
  4. {
  5. //实现自定义接口
  6. pool = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5),
  7. new ThreadFactory() {
  8. public Thread newThread(Runnable r) {
  9. System.out.println("线程"+r.hashCode()+"创建");
  10. //线程命名
  11. Thread th = new Thread(r,"threadPool"+r.hashCode());
  12. return th;
  13. }
  14. }, new ThreadPoolExecutor.CallerRunsPolicy()) {
  15. protected void beforeExecute(Thread t,Runnable r) {
  16. System.out.println("准备执行:"+ ((ThreadTask)r).getTaskName());
  17. }
  18. protected void afterExecute(Runnable r,Throwable t) {
  19. System.out.println("执行完毕:"+((ThreadTask)r).getTaskName());
  20. }
  21. protected void terminated() {
  22. System.out.println("线程池退出");
  23. }
  24. };
  25. for(int i=0;i<10;i++) {
  26. pool.execute(new ThreadTask("Task"+i));
  27. }
  28. pool.shutdown();
  29. }
  30. }
  31. public class ThreadTask implements Runnable{
  32. private String taskName;
  33. public String getTaskName() {
  34. return taskName;
  35. }
  36. public void setTaskName(String taskName) {
  37. this.taskName = taskName;
  38. }
  39. public ThreadTask(String name) {
  40. this.setTaskName(name);
  41. }
  42. public void run() {
  43. //输出执行线程的名称
  44. System.out.println("TaskName"+this.getTaskName()+"---ThreadName:"+Thread.currentThread().getName());
  45. }
  46. }

我看下输出结果

java-多线程 - 图8线程118352462创建
线程1550089733创建
准备执行:Task0
准备执行:Task1
TaskNameTask0—-ThreadName:threadPool118352462
线程865113938创建
执行完毕:Task0
TaskNameTask1—-ThreadName:threadPool1550089733
执行完毕:Task1
准备执行:Task3
TaskNameTask3—-ThreadName:threadPool1550089733
执行完毕:Task3
准备执行:Task2
准备执行:Task4
TaskNameTask4—-ThreadName:threadPool1550089733
执行完毕:Task4
准备执行:Task5
TaskNameTask5—-ThreadName:threadPool1550089733
执行完毕:Task5
准备执行:Task6
TaskNameTask6—-ThreadName:threadPool1550089733
执行完毕:Task6
准备执行:Task8
TaskNameTask8—-ThreadName:threadPool1550089733
执行完毕:Task8
准备执行:Task9
TaskNameTask9—-ThreadName:threadPool1550089733
准备执行:Task7
执行完毕:Task9
TaskNameTask2—-ThreadName:threadPool118352462
TaskNameTask7—-ThreadName:threadPool865113938
执行完毕:Task7
执行完毕:Task2
线程池退出

可以看到通过对beforeExecute()、afterExecute()和terminated()的实现,我们对线程池中线程的运行状态进行了监控,在其执行前后输出了相关打印信息。另外使用shutdown方法可以比较安全的关闭线程池, 当线程池调用该方法后,线程池中不再接受后续添加的任务。但是,此时线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。

五、线程池线程数量
线程吃线程数量的设置没有一个明确的指标,根据实际情况,只要不是设置的偏大和偏小都问题不大,结合下面这个公式即可

  1. /**
  2. * Nthreads=CPU数量
  3. * Ucpu=目标CPU的使用率,0<=Ucpu<=1
  4. * W/C=任务等待时间与任务计算时间的比率
  5. */
  6. Nthreads = Ncpu*Ucpu*(1+W/C)