Java线程池与多线程详解


文章目录

  • Java线程池与多线程详解
  • 一、前言
  • 二、创建线程的几种方式
  • 1、继承Thread类创建线程
  • 2、实现Runnable接口创建线程
  • 3、实现Callable接口创建线程
  • 4、通过线程池创建线程
  • 三、线程池的执行流程以及常用函数
  • 3.1 、线程的执行流程
  • 3.2、线程睡眠(sleep)
  • 3.3、线程等待(wait)
  • 3.4、sleep()方法和wait()方法的区别
  • 3.5、为什么wait(),notify()方法要和synchronized一起使用
  • 3.6、线程让步(yield())
  • 3.7、线程中断(interrupt)
  • 3.8、线程强制执行(join)
  • 3.9、线程的关闭
  • 3.10、线程死锁
  • 3.10.1、如何查看线程死锁
  • 3.10.2、如何避免死锁
  • 四、Java创建线程池的五种方式
  • 4.1、ewSingleThreadPool
  • 4.2、newFixedThreadPool
  • 4.3、newCachedThreadPool
  • 4.4、newScheduledThreadPool
  • 4.5、newWorkStealingPool
  • 五、ThreadPoolExecutor 类介绍
  • 5.1、线程池的7大核心参数详解
  • 1、corePoolSize :核心线程数
  • 2、maximumPoolSize:最大线程数
  • 3、keepAliveTime :存活时间,线程没有任务执行时最多保持多久时间会终止
  • 4、unit:时间单位
  • 5、workQueue: 阻塞队列
  • 6、ThreadFactory:线程工厂
  • 7、handler:拒绝策略
  • 7.1、RejectedExecutionHandler接口
  • 8、补充参数:allowCoreThreadTimeOut
  • 5.2、Excutor 和 Submit的区别
  • 5.3、线程池的参数设置
  • 六、关闭线程池
  • 七、为什么《阿里巴巴Java开发手册》上要禁止使用Executors来创建线程池
  • 7.1、Executors各个方法的弊端
  • 八、Spring已经实现的线程池
  • 九、在SpringBoot中使用线程池
  • 9.1、第一种方式
  • 9.2、第二种方式
  • 9.3、第三种方式
  • 十、总结


一、前言

Java中经常需要使用多线程来处理一些业务,我们非常不建议单纯的使用继承Thread类或者实现Runnable接口Callable接口的方式来创建线程,线程不断的创建,不断的关闭就会过度的消耗CPU资源、线程上下切换(频繁的切换线程,可能会导致系统崩溃)等问题,同时创建过多的线程也会引发资源耗尽的风险(栈溢出问题),因此引入线程池是比较合理的解决方案,方便线程任务的管理,Java中涉及到线程池相关的类均在java.util.concurrent包中,涉及到核心接口包括:Executor、Executors、ExecutorService、ThreadPoolExecutor、FutureTask、Callable、Runnable等。

线程池的优势:

  1. 降低CPU资源的消耗:使得线程可以重复利用,不需要在频繁的创建线程和销毁线程上浪费资源
  2. 提高响应速度:任务到达时,线程可以不用创建就能执行
  3. 线程的可管理性:线程是稀缺资源,如果无限制的创建就会严重影响系统效率,线程池可以对线程进行管理、监控、调优

二、创建线程的几种方式

1、继承Thread类创建线程

参考上一篇博文:

2、实现Runnable接口创建线程

参考上一篇博文:

3、实现Callable接口创建线程

参考上一篇博文:

4、通过线程池创建线程

  • Java提供了构建线程池的方式(通过Executors.newFixedThreadPool(int) 创建指定数量的线程池)但是阿里编码规范中不允许使用这种方式创建线程,原因是这种方式对线程的控制粒度比较低,通过查看源码可以看到,这中方式创建的线程只允许你指定线程数量,而不能控制核心线程数,最大线程数,拒绝策略等
//这是newFixedThreadPool的源码,可以看到他返回的是Java自己构建的线程池    
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  • 推荐使用自己手动创建线程池的方式(就是自己 new 一个ThreadPoolExecutor(…))

三、线程池的执行流程以及常用函数

3.1 、线程的执行流程

  • 当线程池中的线程数量小于核心线程数时,会一直创建线程直到线程数量等于核心线程数
  • 当线程池中的线程数量 等于核心线程数时,新加入的任务会被放到任务队列等待执行
  • 当任务队列已满,又有新任务时,会创建新线程直到线程数量等于最大线程数
  • 当线程数量等于最大线程数,且队列任务已满时,新加入的任务就会被拒绝

java多线程与NIO java多线程与线程池_经验分享

3.2、线程睡眠(sleep)

//源码
public static void sleep(long millis) throws InterruptedException // 普通函数:设置休眠时间的毫秒数
public static void sleep(long millis,int nanos) throws InterruptedException // 普通函数:设置休眠毫秒数和纳秒数

sleep()是Thread类的一个静态方法,,由Thread.sleep()调用实现,作用是在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),使当前线程从运行状态进入到阻塞状态,sleep()方法会指定休眠时间,线程的休眠时间大于或等于该休眠时间时,该线程会被唤醒,此时它会从 阻塞状态变成 就绪状态然后等待CPU的调度执行。

//使用案例-睡眠一秒钟,模拟网络演示
public void testSonThread(){
        for (int i = 0; i < 50; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"我是子线程"+i);
        }
    }

3.3、线程等待(wait)

//源码
public final void wait() throws InterruptedException {
        wait(0);
}

wait()是Object类的一个方法,wait()方法不是线程对象的方法,是Java中任何一个Java对象都有的方法,wait()方法是让当前正在执行的线程进入等待状态,无限期的等待,直到被唤醒(通过 notify()和notifyAll()方法唤醒)为止。wait()方法、必须要与synchronized一起使用,也就是说wait()和notify()方法是针对获取了object锁的对象进行操作,从语法的角度来说,Obj.wait()、Obj.notify()必须在synchronized(Obj).{…}语句块中,从功能上来说wait()就是在线程获取对象锁后,主动释放对象锁,同时让线程休眠,直到有其它线程调用对象的notify()唤醒该线程,才能继续获得对象锁,并继续执行。相应的notify()方法就是对对象锁的唤醒操作,但是有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized{}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予对象锁,唤醒线程继续执行,这样就提供了线程间同步,唤醒的操作。

3.4、sleep()方法和wait()方法的区别

Thread.sleep()与Object.wait()二者都是可以暂停当前线程,释放CPU的控制权,主要区别在于Obiect.wait()在释放CPU的同时,释放了对象锁的控制。

  1. sleep()是Thread类中的方法,而wait()是Obiect类中的方法
  2. 最主要的是sleep()方法没有释放对象锁,而wait()方法释放了锁,使得其它线程可以使用同步控制块或者方法
  3. 使用范围:wait、notify和notifyAll只能在同步控制方法或者同步代码块中使用,一旦一个对象调用了wait()方法,必须要采用notify()或者notifyAll()方法唤醒该线程,而sleep()方法在任何地方都能使用
  4. 相同点:sleep()方法必须捕获异常,wait()、notify()、notifyAll()同样也需要捕获异常

3.5、为什么wait(),notify()方法要和synchronized一起使用

因为wait()方法是通知当前线程等待并释放对象锁,notify()方法是通知等待此对象锁的线程重新获得对象锁,然后如果没有获得对象锁,wait方法和notify方法是没有意义的,即必须先 获得对象锁,才能对对象锁进行操作,于是才必须把notify()和wait()方法写到synchronized方法或者是synchronized代码块中。

3.6、线程让步(yield())

yield()的作用是让步,它也是Thread类的一个静态方法,它也可以让当前正在执行的线程暂停,但是并不会阻塞线程,他能让当前线程由 运行状态进入到 就绪状态,从而让其它具有相同优先级的等待线程获得CPU的使用权,但是并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得CPU的执行权,也可能当前线程又进入到运行状态继续执行

3.7、线程中断(interrupt)

//源码
public boolean isInterrupted() //普通函数:判断线程是否被中断
public void interrupt() //普通函数:中断线程执行

interrupt方法定义在Thread类中,由Thread.interrupt()调用实现,该方法将设置该线程的中断标志位,即设置为true,中断的结果线程死亡、还是等待新的任务或者是继续运行至下一步,取决于程序本身,线程会时不时的检测这个中断标志位,以判断线程是否被中断,它并不像stop方法那样会中断一个正在运行的线程。interrupt()方法只是改变中断状态,不会中断一个正在运行的线程,需要用户自己去监视线程的状态并做处理,支持线程中断的方法(也就是线程中断后会抛出interruptedException的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。这一方法实际完成的是,给受阻塞的线程发出一个中断信号,这样受阻线程检查到中断标识,就得以退出阻塞的状态。更确切的说,如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态。如果线程没有被阻塞,这时调用 interrupt()将不起作用,直到执行到wait(),sleep(),join()时,才马上会抛出 InterruptedException。

3.8、线程强制执行(join)

// Thread类方法
public final void join() throws InterruptedException //普通函数:强制执行

join()方法定义在Thread类中,由Thread.join()调用实现,作用是允许其它线程加入到当前线程中,当某个线程调用该方法时,加入并阻塞当前线程,直到加入的线程执行完毕,当前线程才能继续执行。可以简单的理解为插队,10个人排队买票,突然有个人插队,后面的人得等刚刚插入进来得人买完,才能继续买票。子线程加入到主线程并阻塞了主线程,子线程执行完毕后才恢复主线程的运行。

3.9、线程的关闭

Java终止正在运行的线程主要有以下三种方式

  1. 使用中断标志位
    通过设置一个boolean类型的变量,使用While循环执行线程,当flag标志为true时一直执行线程内容,当flag标志为false时跳出循环不再执行线程内容,此时run()方法执行完毕,线程正常关闭!
public class TicketThread extends Thread {
    //定义买票的总数
    private static int ticket = 100;
    //定义循环终止标记
    boolean flag = true;
    private ReentrantLock lock = new ReentrantLock();
    public void run() {
        //确保有票就卖出
        while (flag) {
            if (ticket <= 0) {
                flag = false;
                System.out.println("没有票了,售罄!");
                return;
            }
            //使用Synchronized确保每次只有一个线程进入
            synchronized (TicketThread.class) {
                //lock.lock();
                //try {
                if (ticket > 0) {
                    //便于观察线程执行过程,这里使线程休眠一下
                    try {
                        Thread.sleep(500);//模拟买票所需要的时间
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖了第" + ticket + "张票!");
                    ticket--;
                }
            }
        }

    }
    public static void main(String[] args) {
        TicketThread thread = new TicketThread();
        new Thread(thread, "窗口1").start();
        new Thread(thread, "窗口2").start();
        new Thread(thread, "窗口3").start();
    }
}
  1. 使用interrupt方法中断线程
    要中断一个正在运行中的线程,可调用线程类(Thread)对象的实例方法:interrupt()方法,interrupt()方法并不能真正的停止线程,而是在当前线程打了一个停止的标记,如果在中断时:
  • 线程正处于非阻塞状态,则将中断标志设置为true,则此后,一旦线程调用了wait、join、sleep方法中的一种,立马就会抛出InterruptedException异常,且中断标志被清楚,重新设置为false。可以理解为,调用线程的interrupt方法,其本质就是设置该线程的中断标志,将中断标志设置为true,并根据线程的状态决定是否抛出异常。
  • 如果此线程正处于阻塞状态(比如调用了wait、sleep、join等方法),则会立即退出阻塞,并抛出InterruptedException异常,线程就可以通过捕获异常处理,然后让线程退出
public static void main(String[] args) throws InterruptedException {
    // 创建可中断的线程实例
    Thread thread = new Thread(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("thread 执行步骤1:线程即将进入休眠状态");
            try {
                // 休眠 1s
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                System.out.println("thread 线程接收到中断指令,执行中断操作");
                // 中断当前线程的任务执行
                break;
            }
            System.out.println("thread 执行步骤2:线程执行了任务");
        }
    });
    thread.start(); // 启动线程

    // 休眠 100ms,等待 thread 线程运行起来
    Thread.sleep(100);
    System.out.println("主线程:试图终止线程 thread");
    // 修改中断标识符,中断线程
    thread.interrupt();
}

执行结果

java多线程与NIO java多线程与线程池_java多线程与NIO_02

  1. 使用stop方法强行终止线程(不推荐使用)
    stop方法是@Deprecated注解修饰过的方法,也就是不推荐使用的过期方法,因为stop方法会直接停止线程,这样就没有给线程足够的时间来处理停止前的工作,就会造成数据不完成的问题,因此不建议使用。

3.10、线程死锁

  • 当第一个线程拥有A对象锁标记,并等待B对象锁 标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记,就会产生死锁
  • 一个线程可以同时拥有多个对象锁的标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。

死锁的概念:当多个线程因竞争系统资源或相互通信而处于半永久阻塞状态时,若无外力作用,这些线程都将无法向前推进,这些线程将无限期的等待此组线程中的某个线程占用的,自己永远无法得到的资源,这种现象称为死锁。

通过以下按钮演示死锁的发生:假如两个人A和B再桌子上同时吃饭,桌子上只有一双筷子,当一个人拥有两根筷子的是hi才能吃

/**
*锁对象(筷子)
*/
public class Chopsticks {
    private String color;//颜色
    private Integer sum;//数量
    private Integer length;//长度
    private Integer width;//宽度
}
package com.study.threadStudy.service;
import com.study.threadStudy.entity.Chopsticks;

/**
 * 测试死锁问题
 */
public class TestChopSticks {
    public static void main(String[] args) {
        //创建两个筷子对象
        Chopsticks chopsticksA=new Chopsticks();
        Chopsticks chopsticksB=new Chopsticks();
        //A
        Runnable A=new Runnable() {
            @Override
            public void run() {
                //持有第一根筷子
                synchronized (chopsticksA){
                    System.out.println("A拿到了一根筷子");
                    //持有第二根筷子
                    synchronized (chopsticksB){
                        System.out.println("A拿到了第二根筷子,一双筷子拿到了,开始吃饭");
                    }
                }
            }
        };
        //B
        Runnable B=new Runnable() {
            @Override
            public void run() {
                //持有第一根筷子
                synchronized (chopsticksB){
                    System.out.println("B拿到了一根筷子");
                    //持有第二根筷子
                    synchronized (chopsticksA){
                        System.out.println("B 拿到了第二根筷子,一双筷子拿到了,开始吃饭");
                    }
                }
            }
        };

        //开启A和B,开始抢筷子
        new Thread(A).start();
        new Thread(B).start();
    }
}
//运行结果

A拿到了一根筷子
B拿到了一根筷子

此时A和B各自持有一双筷子,并且都在等待对方持有的另一根筷子,导致两个人都吃不了饭。可以通过sleep()方式使其中一个线程休眠一会,A(B)吃完B(A)再吃,或者把A(B)同步代码块中的锁换一下位置,一开始两个人都抢同一根筷子,没抢到的等另一个吃完饭再吃。

3.10.1、如何查看线程死锁

可以通过这个工具(位于jdk安装路径bin目录)去排查,现在介绍一下

java多线程与NIO java多线程与线程池_经验分享_03

java多线程与NIO java多线程与线程池_线程池_04

java多线程与NIO java多线程与线程池_java多线程与NIO_05

可以看到两个死锁

3.10.2、如何避免死锁
  • 同一个代码块,不要同时持有两个对象锁
  • 同一个方法只能被一个线程使用,不要嵌套锁,独木桥每次只能通过一个人
  • 如果业务场景需要一次锁定多个资源对象,可以定义锁的先后顺序,例如通过sleep()方法,打破死锁的互相占有

四、Java创建线程池的五种方式

首先,我们的线程池类型一共有四种 newSingleThreadPoolnewFixedThreadPoolnewCachedThreadPoolnewScheduledThreadPool,在JDK1.8时又加入了一种:newWorkStealingPool,所以一共是五种

线程池类型

解释

newSingleThreadPool

创建单核心的线程池

newFixedThreadPool

创建指定数量的线程池,可控制线程的最大并发数

newCachedThreadPool

创建一个自动增长的线程池

newScheduledThreadPool

创建一个按照计划规定执行的线程池

newWorkStealingPool

创建一个具有抢占式操作的线程池

4.1、ewSingleThreadPool

//创建一个单核心的线程池
ExecutorService service=Executors.newSingleThreadPool();

//源码
 public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
 }

创建一个单线程化的线程池子,他只会用唯一的工作线程来执行任务,保证所有的任务按照指定的顺序和优先级执行。SingleTHreadPool的意义在于统一所有外界任务在一个线程中,使得这些任务之间不需要处理线程同步的问题。

4.2、newFixedThreadPool

//这里就是创建一个线程池,里面有十个线程
ExecutorService service=Executors.newFixedThreadPool(10);

//newFixedThreadPool的源码
 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
    }

创建指定数量的线程池,可控制线程的最大并发数,超出数量的线程会在队列中等待,它是一种线程数量固定的线程池,当线程处于空闲状态时,它们并不会被回收,除非线程池关闭,当所有的线程都处于运行状态时,新的任务将会处于队列等待状态,直到有线程空闲出来,等待状态中的任务才会被执行。FixedThreadPool只有核心线程,且核心线程都不会被回收,这意味着它可以更快的响应外界的请求

4.3、newCachedThreadPool

//创建一个自动增长的线程池
ExecutorService executorService = Executors.newCachedThreadPool();

//源码
 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,如果没有可回收,则新建线程。由Executors的newCachedThreadPool方法创建的线程池不存在核心线程,只存在数量不定的核心线程,而且其数量最大值为Integer.MAX_VALUE,当线程池中的线程都处于活跃状态时(全满)线程池就会创建新的线程来处理新的任务,线程池中的空闲线程都有超时机制,默认超时时间为60s,超过60s的空闲线程就会被回收,和FixedThreadPool不同的是,CachedThreadPool的任务队列其实相当于是一个空的集合,这将导致任何进来的任务都将会被执行,因为在这种场景下SynchronousQueue是不能插入任务的,SynchronousQueue是一个特殊的队列,在很多情况下可以理解为一个无法存储元素的队列,当CachedThreadPool的特性看,这类线程比较适合执行大量耗时任务较小的任务,当整个线程池都处于闲置状态时,线程池中的线程都会因为超时而被停止回收,几乎是不占任何系统资源的。

4.4、newScheduledThreadPool

//创建一个按照计划规定执行任务的线程池
ExecutorService executorService = Executors.newScheduledThreadPool(2);

//源码
 public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
  }
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
 }

通过Executors.newScheduledThreadPool方式创建的线程池,核心线程数量固定,而非核心线程数量是没有限制的,并且当核心线程闲置时他才会被立即回收,ScheduledThreadPool这类线程池主要用于执行定时任务和具有固定时期的重复任务。

4.5、newWorkStealingPool

//创建一个具有抢占式操作的线程池,也称之为 任务窃取式线程池
ExecutorService executorService = Executors.newWorkStealingPool();

//源码
public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
}

public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
 }

newWOrkStealingPool时Java1.8新添加的线程池,创建时如果不设置任何参数,则以当前机器处理器个数作为线程个数,此线程池会并行处理任务,不能保证执行顺序,和另外四种不同的是,它用的是ForkJoinPool,使用ForkJoinPool的好处是,把一个任务拆分成多个小任务分发到多个线程上去执行,这些小任务都执行完成后,再将结果合并。newWorkStealingPool中每一个线程都有自己的队列,当线程发现自己的队列没有任务了,就会到别的线程的队列中获取任务执行,可以简单的理解为窃取。

**使用场景:**能够合理的使用CPU进行对任务操作(并行操作),适合使用在很耗时的任务中。底层用的ForkJoinPool 来实现的。 ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”分发到不同的cpu核心上执行,执行完后再把结果收集到一起返回。

五、ThreadPoolExecutor 类介绍

ThreadPoolExecutor是线程池最核心的一个类,它为线程池提供了四个构造方法,我们来看一下其中最原始的一个构造方法,其余三个都是由它衍生而来

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
 }

5.1、线程池的7大核心参数详解

可以看到这个构造方法有7个参数,这7个参数直接影响到线程池的效果,以下进行详细解析:

1、corePoolSize :核心线程数

表示当前线程池的核心线程数,当初始化线程池时会创建核心线程进入等待状态,即使他是空闲的,核心线程也不会被回收摧毁,从而降低了任务一来就要创建新线程的时间和性能开销。设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。

2、maximumPoolSize:最大线程数

表示当前线程池的 最大线程数量,意味着核心线程都被用完了,那就只能创建新的线程来执行任务,但是前提是不能超过最大线程数量,否则该任务只能进入阻塞队列进行排队等候,直到有线程空闲了,才能继续执行任务。

3、keepAliveTime :存活时间,线程没有任务执行时最多保持多久时间会终止

表示非核心线程的存活时间,除了核心线程以外,那些被新创建出来的线程可以存活多久,意味着这些新的线程一旦完成任务,而后面都是空闲状态时,就会在设定的时间到达后被回收摧毁。

4、unit:时间单位

存活时间单位,它们取值有以下几种

单位名称

取值

TimeUnit.DAYS;


TimeUnit.HOURS;

小时

TimeUnit.MINUTES;

分钟

TimeUnit.SECONDS;


TimeUnit.MILLISECONDS;

毫秒

TimeUnit.MICROSECONDS;

微妙

TimeUnit.NANOSECONDS;

纳秒

5、workQueue: 阻塞队列

表示任务的 阻塞队列,由于任务可能会有很多,而线程就那么几个,所以那些还未被执行的任务就会进入队列中排队,可以分为有界、无界、同步移交三种队列类型,当核心线程数都在被使用时,新提交的任务就会被存储到阻塞队列中,BlockingQueue(任务阻塞队列)的实现类主要有以下几种:

  • ArrayBlockingQueue:基于数组的先进先出队列,有界(创建队列时指定队列的最大容量)
  • LinkedBlockingQueue:基于链表的先进先出队列,无界与有界队列相比,除非系统资源耗尽,否则不存在任务入队失败的情况
  • synchronousQueue:没有容量,总是将新的任务提交给线程执行,如果没有空闲线程则尝试创建新的线程,如果线程数大于最大线程数(maximumPoolSize),则执行拒绝策略
  • PriorityBlockingQueue:一个具有优先级的无界阻塞队列,总是具有最高优先级的任务先执行
6、ThreadFactory:线程工厂

表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可

7、handler:拒绝策略
7.1、RejectedExecutionHandler接口
//源码
public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
  • RejectedExecutionHandler接口里面就只有一个方法,当要创建的线程属于大于线程池的最大线程数时,新的任务就会被拒绝,就会调用这个接口里的这个方法
  • 我们可以自己实现这个接口,实现对这些超出数量的任务进行处理

表示 拒绝策略(也叫 饱和策略),当队列已满且工作线程大于等于线程池的最大线程数(包括核心线程)时,就会采用拒绝策略去拒绝新进来的任务,以下是几种常见的拒绝策略:

  • AbortPolicy:直接抛出RejectedExecutionException(拒绝执行异常),默认的拒绝策略
//查看源码可知它的逻辑就是直接抛出异常
 public static class AbortPolicy implements RejectedExecutionHandler {
        public AbortPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
       	                                         e.toString());
        }
    }
  • DiscardPolicy:什么也不做,直接忽略
//查看源码,这个处理策略就是什么都不做的
public static class DiscardPolicy implements RejectedExecutionHandler {
        public DiscardPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }
  • DiscardOldestPolicy:丢弃执行任务队列中最老的任务,尝试为当前提交的任务腾出位置
//查看源码发现当任务拒绝添加时会抛弃队列中最旧的任务,再把这个新的任务添加进去
   public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        public DiscardOldestPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }
  • CallerRunsPolicy:直接由提交任务着执行这个任务
//查看源码,该策略不会抛弃任务,也不会抛出异常,而是将某些任务回调到调用者去执行
 public static class CallerRunsPolicy implements RejectedExecutionHandler {
        public CallerRunsPolicy() { }
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }
8、补充参数:allowCoreThreadTimeOut

该属性用来控制是否允许核心线程超时退出,默认值为false,如果线程池的大小已经达到了核心线程数,不管有没有任务执行,线程池都会保证这些核心线程处于存活状态,可以直到该属性是用来控制核心线程的

ps:通过corePoolsize和maximumPoolSize来控制如何新增线程,通过allowCoreThreadTimeOut和keepAliveTime,控制如何销毁线程

5.2、Excutor 和 Submit的区别

  1. Excutor用于提交不需要返回值的任务,线程在执行完后既不会返回结果,也不会抛出异常
  2. Submit则是用于提交需要返回值的任务,在线程执行完Submit提交的任务后,会返回一个 future 对象,这个 future 对象可以用来判断任务是否执行成功,并且可以用 future 的get()方法来获取其返回值

5.3、线程池的参数设置

  • 计算密集型(CPU密集型)的任务比较消耗CPU,所以一般核心线程数设置的大小等于或者略微大于CPU的核数,一般为N+1(N代表CPU的核数)
  • 磁盘密集型(IO密集型)的任务主要时间消耗在IO等待上,CPU压力并不大,所以线程数一般设置较大,例如多线程访问数据库,数据库有128个表,可以直接考虑使用128个线程,IO密集型一般为2*N(N为CPU的核数)
  • 设置参数时需要注意以下两点:
  • corePoolSize和maximumPoolSize设置不当会影响效率,甚至耗尽线程;
  • workQueue设置不当容易导致OOM(Out Of Memory);

六、关闭线程池

  • shutdownNow():立即关闭线程池(暴力),正在执行中的以及队列中的任务会全部中断,同时该方法会返回被中断的队列中的任务列表
  • shutdown():平滑关闭线程池,正在执行中的以及队列中的任务能够执行完成,后续进来的任务会被执行拒绝策略
  • isTerminated():当正在执行的以及队列中的任务全部都执行(清空)完后就会返回true

七、为什么《阿里巴巴Java开发手册》上要禁止使用Executors来创建线程池

Executors创建出来的线程池使用的都是无界队列,而使用无界队列会带来很多弊端,最重要的就是,它可以无限保存任务,因此很有可能会造成OOM(内存溢出)异常,同时这种方式创建的线程池对线程的控制粒度比较低,比如我们无法控制上述的线程池中最重要的7个核心参数。

7.1、Executors各个方法的弊端

  • newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM内存溢出
  • newCachedThreadPool和newScheduledThreadPool:主要问题是线程数最大是:Integer.MAX_VALUE,可能会创建数量非常多的线程,导致OOM内存溢出
  • @Async是Spring的注解,默认异步配置的是SimpleAsyncTaskExecutor,该线程池默认是来一个任务创建一个线程池,若在系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。针对线程创建问题,SimpleAsyncTaskExecutor提供了限流机制,通过concurrencyLimit属性来控制开关,当concurrencyLimit>=0时开启限流机制,默认关闭限流机制即concurrencyLimit=-1,当关闭情况下,会不断创建新的线程来处理任务。基于默认配置,SimpleAsyncTaskExecutor并不是严格意义的线程池,达不到线程复用的功能。

八、Spring已经实现的线程池

  • SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程
  • SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
  • ConcurrentTaskExecutor:Executor 的适配类,不推荐使用。如果 ThreadPoolTaskExecutor 不满足要求时,才用考虑使用这个类。
  • SimpleThreadPoolTaskExecutor:是 Quartz 的 SimpleThreadPool 的类。线程池同时被 quartz 和非 quartz 使用,才需要使用此类。
  • ThreadPoolTaskExecutor :最常使用,推荐。其实质是对 java.util.concurrent.ThreadPoolExecutor 的包装。

九、在SpringBoot中使用线程池

9.1、第一种方式

我们自定义一个线程池,然后使用依赖注入,注入到需要使用到线程池的类中,然后调用exector方法并new 一个Runnable接口并重写run方法来开启多线程处理

  1. 新增线程池配置类
package com.study.threadStudy.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@Slf4j
public class ThreadPoolConfig {
    @Bean(value = "threadExecutor")
    public ThreadPoolTaskExecutor executorService() {
        log.info("start threadpool");
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //获取运行机器的CPU核数,作为核心线程数量
        Integer availibleNum = Runtime.getRuntime().availableProcessors();
        //配置-核心线程数
        executor.setCorePoolSize(availibleNum);
        //配置-最大线程数  线程数*2
        executor.setMaxPoolSize(availibleNum * 2);
        //配置-队列容量,队列大小
        executor.setQueueCapacity(9999);
        //配置-非核心线程存活时间
        executor.setKeepAliveSeconds(60);
        //配置-线程池中线程的名称前缀
        executor.setThreadNamePrefix("test_jiaoThread");
        //配置-拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        return executor;
    }
}
  1. 在业务层使用@Resource注解注入线程池,并开启子线程执行一个循环输出,为了效果的明显,我这里子线程延时一秒钟
package com.study.threadStudy.service;

import com.study.threadStudy.config.ThreadPoolConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

@Service
@Slf4j
public class ThreadPoolService {
    //这里如果你使用@Autowired注解注入可能会报错哦,你自己百度一下两个注解的区别,这里不做讲解了
    @Resource(name = "threadExecutor")
    private ThreadPoolTaskExecutor taskThreadExector;

    public void testThread() {
        log.info("主线程开始执行-------");
        taskThreadExector.execute(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "我是子线程" + i);
                }
            }
        });
        //主线程
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + "我是主线程" + i);
        }
    }

}
  1. 新建Controller,这里只是为了方便测试,你也可以按照你自己的想法来触发这个多线程
package com.study.threadStudy.controller;

import com.study.threadStudy.service.ThreadPoolService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class ThreadPoolController {
    @Autowired
    ThreadPoolService threadPoolService;

    @RequestMapping("/threadPool")
    public String test() {
        threadPoolService.testThread();
        return "测试成功";
    }
}
  1. 浏览器访问一下地址,查看结果
http://localhost:9000/test/threadPool

java多线程与NIO java多线程与线程池_开发语言_06

  1. 查看结果,发现主线程的方法体已经执行完了,但是子线程的方法体还在执行,说明成功开启了异步执行
// ........
http-nio-9000-exec-1我是主线程46
http-nio-9000-exec-1我是主线程47
http-nio-9000-exec-1我是主线程48
http-nio-9000-exec-1我是主线程49
test_jiaoThread1我是子线程5
test_jiaoThread1我是子线程0
test_jiaoThread1我是子线程1
test_jiaoThread1我是子线程2
test_jiaoThread1我是子线程3
// ........

9.2、第二种方式

使用Spring框架提供的@Async注解(并重写默认的线程池),实现创建线程池并开启多线程,但是需要注意的是@Async注解的使用有一些小坑哦,使用这个注解要么你就重写默认的线程池,要么你就使用name属性指定你自定义的线程池,至于原因,上面第六条已经说的很清楚了。

  1. 新增配置类,注意添加@EnableAsync注解开启异步处理,并实现 AsyncConfigurer接口,重写默认的线程池
package com.study.threadStudy.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
@Slf4j
public class ThreadPoolSyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor(){
        ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
        //获取运行机器的CPU核数作为最大核心线程数
        Integer availibleNum=Runtime.getRuntime().availableProcessors();
        //配置-核心线程数
        executor.setCorePoolSize(availibleNum);
        //配置-最大线程数
        executor.setMaxPoolSize(availibleNum*2);
        //配置-队列的最大容量
        executor.setQueueCapacity(9999);
        //配置-非核心线程最大存活时间
        executor.setKeepAliveSeconds(60);
        //配置-线程名称
        executor.setThreadNamePrefix("SyncThreadJiao");
        //配置-饱和策略、拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        //初始化线程池,这里不初始化,下面调用子线程的时候会报错的
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler(){
        return null;
    }
}
  1. 在业务层新建两个方法,一个用于执行主线程逻辑,一个用于处理子线程逻辑,重点是在子线程逻辑方法上增加@Async注解
package com.study.threadStudy.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class ThreadSyncService {
    public void testThread(){
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName()+"我是主线程"+i);
        }
        //不要在这里调用下面的子线程方法,不然你会发现他还是走的主线程
    }
    @Async
    public void testSonThread(){
        for (int i = 0; i < 50; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"我是子线程"+i);
        }
    }
}
  1. controller层中新增一个方法访问这个Service,记住先把上面的Service注入进去先
package com.study.threadStudy.controller;

import com.study.threadStudy.service.ThreadPoolService;
import com.study.threadStudy.service.ThreadSyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class ThreadPoolController {
    @Autowired
    ThreadPoolService threadPoolService;
    @Autowired
    ThreadSyncService threadSyncService;

    @RequestMapping("/threadPool")
    public String test() {
        threadPoolService.testThread();
        return "测试成功";
    }

    @RequestMapping("/threadSync")
    public String testSyncThread() {
        threadSyncService.testThread();
        //调用子线程方法
        threadSyncService.testSonThread();
        //调用子线程方法
        threadSyncService.testSonThread();
        return "测试成功";
    }
}
  1. 查看结果,成功执行了异步,效果和第一种方式是一样的
//........
http-nio-9000-exec-1我是主线程47
http-nio-9000-exec-1我是主线程48
http-nio-9000-exec-1我是主线程49
2022-05-30 16:31:26.563  INFO 14464 --- [nio-9000-exec-1] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService
SyncThread_Jiao1我是子线程0
SyncThread_Jiao1我是子线程1
SyncThread_Jiao1我是子线程2
SyncThread_Jiao1我是子线程3
//........

9.3、第三种方式

使用@Async注解,使用自定义线程池,这种方式的逻辑和上面第二种方式基本上是一样的,不一样的地方在于

  1. 配置类中不是通过实现 AsyncConfigurer接口,而是自定义了一个线程池,重点是会声明这个线程池的名称
package com.study.threadStudy.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;

@EnableAsync
@Configuration
@Slf4j
public class ThreadPoolCustomConfig {
   @Bean(value = "threadCustomExecutor")
    public Executor executorService() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //获取运行机器的CPU核数,作为核心线程数量
        Integer availibleNum = Runtime.getRuntime().availableProcessors();
        //配置-核心线程数
        executor.setCorePoolSize(availibleNum);
        //配置-最大线程数  线程数*2
        executor.setMaxPoolSize(availibleNum * 2);
        //配置-队列容量,队列大小
        executor.setQueueCapacity(9999);
        //配置-非核心线程存活时间
        executor.setKeepAliveSeconds(60);
        //配置-线程池中线程的名称前缀
        executor.setThreadNamePrefix("customExecutor_jiao");
        //配置-拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        //初始化线程池
        executor.initialize();
        return executor;
    }
}
  1. 在使用的时候声明@Async注解名称的时候,添加上你的自定义线程池的名称
@Async("threadCustomExecutor")
    public void testSonThread(){
        for (int i = 0; i < 50; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"我是子线程"+i);
        }
    }
  1. 执行结果
//.......
http-nio-9000-exec-1我是主线程47
http-nio-9000-exec-1我是主线程48
http-nio-9000-exec-1我是主线程49
customExecutor_jiao1我是子线程0
customExecutor_jiao1我是子线程1
customExecutor_jiao1我是子线程2
//.......

十、总结

Java多线程的问题很复杂,小小的学习总结,可能不够完美和充分,有问题的地方欢迎指导补充。