Java多线程 JUC之线程池的使用详解及其扩展分析(ThreadPoolExecutor、ForkJoinPool等)

一. 前言

  谈线程池前有必要回顾一下多线程,而多线程离不开两个概念,即进程和线程。那什么是进程和线程了?

  用官方的语言来说,进程是操作系统进行资源分配的最小单位,线程是CPU(任务)调度和执行的最小单位。通俗来讲:进程就好比整个QQ;你在QQ里面给人发消息,看空间,点赞,发评论就属于四个线程。

二. 多线程实现

2.1 在Java中的多线程

  在Java中,多线程的实现主要有三种方式,三种方式大同小异,各有各的特点和用处。三种方式分别是:继承Thread类,重写run()方法;实现Runnable接口,重写run()方法;实现Callable接口,重写call()方法。

2.2 使用Thread实现多线程

public class ThreadTest {

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            ThreadThread thread = new ThreadThread();
            thread.start();
        }
    }

}
class ThreadThread extends Thread {

    /**
     * 重写实现run()方法
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":使用Thread实现多线程");
    }
}

在这里插入图片描述

运行截图 3.1

2.3 使用Runnable实现多线程

public class ThreadTest {

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {

            ThreadThread run = new ThreadThread();
            // 比使用Thread时多了这一步
            Thread thread = new Thread(run);
            thread.start();
        }
    }

}

class ThreadThread implements Runnable {

    /**
     * 重写实现run()方法
     */
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":使用Thread实现多线程");
    }
}

在这里插入图片描述

运行截图 3.2

2.4 使用Thread实现多线程

public class ThreadTest {

    public static void main(String[] args) throws InterruptedException, ExecutionException {

        // 最好写成接口等于实现类的方式
        Callable<String> call = new ThreadCallable();

        for (int i = 0; i < 10; i++) {

            FutureTask<String> future = new FutureTask<>(call);

            Thread thread = new Thread(future);
            thread.start();

            System.out.println(future.get());
        }
    }
}

class ThreadCallable implements Callable<String> {

    /**
     * 重写实现call()方法
     */
    @Override
    public String call() throws Exception {
        return "这是调用call()方法-->" + Thread.currentThread().getName();
    }
}

在这里插入图片描述

运行截图 3.3

2.5 三种方式实现多线程小结

  从上面的三个示例中,我们可以发现:通过继承Thread类来实现多线程,最为方便简洁。缺点也是显而易见的,比如由于Java单继承的特点,线程类继承了Thread类之后,就再也不能继承其它类了,即很难得到扩展;显然,如果我们通过实现Runnable接口来实现多线程,就完美的避开了Java单继承的这一特性,轻松扩展线程类;而以实现Callable接口的方式实现多线程,是三种实现方式中最难的一个,但是优势也非常确定,即支持返回值

三. 线程池基础

3.1 什么是线程池

  线程的基本操作讲完了,接下来就是线程池的内容了,那什么是线程池了?

  现代的计算机基本都是多核,而多线程的软件设计方法可以最大限度地发挥现代多核处理器的计算能力,提高生产系统的吞吐量和性能。但是,若不加控制和管理,随意使用线程,对系统的性能反而会产生不利的影响。当线程数过多时,会更快的耗尽CPU和内存,因为线程的创建和销毁都是耗资源和时间的,同时也会增加垃圾回收的频率和时间。

此时,线程池应运而生。为的就是重复利用线程资源,防止线程频繁的创建和销毁。

  类似于数据库链接池,为了避免每次数据库查询都重新建立和销毁数据库连接,使用数据库连接池维护一些数据库连接,让它们长期保持在一个激活状态。当系统需要使用数据库时,并不是创建一个新的连接,而是从连接池中获得一个可用的连接。反之,当需要关闭连接时,并不真的把连接关闭,而是将这个连接“还”给连接池即可。这种方式可以节约不少创建和销毁对象的时间。

简单的说:在使用线程池后,创建线程变成了从线程池取空闲线程,关闭线程变成了向线程池还线程,极大的节约了系统资源

四. Executors工具类

4.1 Executors工具类简介

  在Java中,有个特性,一般后缀为s的类基本都是工具类。这点在我们的项目开发中也同样适用。jdk在Executors类中给我们自定义好了四种线程池,分别是:newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool四种线程池。接下来介绍一下这四种线程池的使用。在此之前,先看一下本工具类的UML图。
在这里插入图片描述

截图 4.1

4.2 四种基本线程池的使用

4.2.1 newFixedThreadPool()方法的使用

介绍: newFixedThreadPool()返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理任务队列中的任务。

public class ThreadPoolTest {
    public static void main(String[] args) {

        ExecutorService pool = Executors.newFixedThreadPool (10);

        //提交了30个任务,看最终会有多个线程开启?
        for (int i = 0; i < 30; i++) {
            pool.execute (new ThreadnewFixedThreadPool (i));
        }
    }
}

class ThreadnewFixedThreadPool implements Runnable {

    private int i;

    public ThreadnewFixedThreadPool(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        try {
            //睡个10毫秒,防止执行过快,线程池不开启新线程
            TimeUnit.MILLISECONDS.sleep (10);
        } catch (InterruptedException e) {
            e.printStackTrace ();
        }
        System.out.println (Thread.currentThread ().getName () + ":newFixedThreadPool方法:" + i);
    }
}

解析: 如上代码,线程池的大小设置为10,共创建30个任务(此处不能理解为开启了30个线程,因为开启线程只能通过调用start()方法,而我们并没有使用start()方法),为了防止任务执行过快,使得线程池不用开启10个线程就能把任务执行完,所以sleep了10毫秒,保证10个线程都被开启。执行结果如下,我们能够很清晰的看到30个任务,由10个线程通过线程池调度执行。
在这里插入图片描述

运行截图 4.2.1

4.2.2 newSingleThreadExecutor()方法的使用

介绍:newSingleThreadExecutor()返回一个只有一个线程的线程池。下个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先进先出(FIFO)的顺序执行队列中的任务。即它只会用唯一的一个工作线程来执行任务,有点类似于SpringBoot中的异步注解@Async(只是类似,并不等同,线程池强大的多)。

public class ThreadPoolTest {
    public static void main(String[] args) {

        //只有一个线程的线程池
        ExecutorService pool = Executors.newSingleThreadExecutor ();

        //提交了30个任务,看最终会有多个线程开启?
        for (int i = 0; i < 30; i++) {
            pool.execute (new ThreadnewFixedThreadPool (i));
        }
    }
}

class ThreadNewSingleThreadExecutor implements Runnable {
    
    private int i;

    public ThreadNewSingleThreadExecutor(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        try {
            //睡个10毫秒,防止执行过快,线程池不开启新线程
            TimeUnit.MILLISECONDS.sleep (10);
        } catch (InterruptedException e) {
            e.printStackTrace ();
        }
        System.out.println (Thread.currentThread ().getName () + ":ThreadNewSingleThreadExecutor方法:" + i);
    }
}

解析: 如上代码,使用Executors工具类的newSingleThreadExecutor ()方法创建线程池,提交30个任务,结果如下所示,无论有多少个任务进入线程池,最终都只有一个线程在执行任务。
运行截图 4.2.2

运行截图 4.2.2

4.2.3 newCachedThreadPool()方法的使用

介绍:newCachedThreadPool()方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用,并逐步关闭多余线程。理论上讲,本方法创建的线程池中包含的线程数可以视为无限(Integer的最大值),因此本方法需要慎重使用。

public class ThreadPoolTest {
    public static void main(String[] args) {

        //缓存线程池
        ExecutorService pool = Executors.newCachedThreadPool ();

        /**
         * 提交了3000个任务.
         *
         * 1. 线程执行时停顿15ms,观察最终会有多个线程被开启
         *
         * 2. 线程执行时不停顿,观察最终会有多个线程被开启
         */
        for (int i = 0; i < 3000; i++) {
            pool.execute (new ThreadNewCachedThreadPool (i));
        }
    }
}

class ThreadNewCachedThreadPool implements Runnable {

    private int i;

    public ThreadNewCachedThreadPool(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        try {
            //睡个10毫秒,观察会开启多少个线程
            //TimeUnit.MILLISECONDS.sleep (10);
        } catch (Exception e) {
            e.printStackTrace ();
        }
        System.out.println ("线程名称:"+Thread.currentThread ().getName () + ":" + i);
    }
}

解析: 如上面代码,提交3000个任务,分两种情况运行。

  • 直接运行情况如下:
    在这里插入图片描述
运行截图 4.2.3.1
  在run()方法中,只写一个打印语句。向线程池中提交3000个任务,此时观察控制台,大概开启了550个线程。
  • 线程中睡眠15ms运行
运行截图 4.2.3.2

  在run()方法中,写一个打印语句并且sleep15毫秒。向线程池中提交3000个任务,此时观察控制台,大概开启了860个线程。通过比对我们可以发现,两个实现中,大概有300个线程的差距,说明线程池对线程执行了复用

4.2.4 newScheduledThreadPool()方法的使用

介绍:newScheduledThreadPool()创建的是用于执行周期任务和计划任务的线程池,主要包含了schedule(…) 、scheduleAtFixedRate(…)和scheduleWithFixedDelay(…)三种实现。

4.2.4.1 schedule(…)方法

  schedule(…)方法会在给定时间,对任务进行一次调度。

public class ThreadPoolTest {
    public static void main(String[] args) {

        //10s执行
        ScheduledExecutorService pool = Executors.newScheduledThreadPool (10);
        System.out.println (Thread.currentThread ().getName ()+":"+System.currentTimeMillis ());
        pool.schedule (new ScheduleThread (), 10, TimeUnit.SECONDS);
    }
}

class ScheduleThread implements Runnable {

    @Override
    public void run() {
        System.out.println (Thread.currentThread ().getName () + ":" + System.currentTimeMillis ());
    }
}

解析: 在代码中,当项目启动时输出当前时间,线程池创建好线程,10后执行任务。(打印10s后的时间)
在这里插入图片描述

运行截图 4.2.4.1
4.2.4.2 scheduleAtFixedRate(…)方法

  scheduleAtFixedRate(…)创建一个周期性任务。开始于给定的初始延时时间(即线程启动initialDelay后开始执行)。后续的任务则按照给定的周期进行:第一个任务将会在initialDelay+period时执行,第二个任务将在initialDelay+2*period时进行,依此类推。

public class ThreadSchedule {

    public static void main(String[] args) {

        // 此处只开启2个线程
        ScheduledExecutorService schedule = Executors.newScheduledThreadPool(2);
        ThreadScheduleTest test = new ThreadScheduleTest();
        System.out.println("开始时间:" + System.currentTimeMillis());

        // 线程开始后1000ms执行,每隔100ms执行一次
        schedule.scheduleAtFixedRate(test, 1000, 100, TimeUnit.MILLISECONDS);
    }
}

class ThreadScheduleTest implements Runnable {

    /**
     *  */
    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName() + ":执行时间点:" + System.currentTimeMillis());

        // 模拟本方法的执行时间是60ms
        try {
            TimeUnit.MILLISECONDS.sleep(60);
        } catch (InterruptedException e) {
            System.err.println(e);
        }
    }
}

解析: 在代码中,开启了两个线程,当线程启动1000ms后才开始运行,任务执行耗时为60ms,每隔100ms执行一次。通过下面的运行截图比对我们可以发现,即使开启了几个线程,任务也是一次间隔100ms执行一次的。
在这里插入图片描述

运行截图 4.2.4.2
4.2.4.3 scheduleWithFixedDelay(…)方法

  scheduleWithFixedDelay(…)方法创建并执行一个周期性任务。任务开始于初始延时时间(即线程启动initialDelay后开始执行),后续任务将会按照给定的延时进行:即上一个任务的结束时间(task end)到下一个任务的开始时间(next task begin)的时间差。

public class ThreadSchedule {

    public static void main(String[] args) {

        // 此处只开启2个线程
        ScheduledExecutorService schedule = Executors.newScheduledThreadPool(2);
        ThreadScheduleTest test = new ThreadScheduleTest();
        System.out.println("开始时间:" + System.currentTimeMillis());

        // 线程开始后1000ms执行,每隔100ms执行一次(与个代码相比仅仅改了本块)
        schedule.scheduleWithFixedDelay(test, 1000, 100, TimeUnit.MILLISECONDS);

    }
}

class ThreadScheduleTest implements Runnable {

    /**
     *  */
    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName() + ":执行时间点:" + System.currentTimeMillis());

        // 模拟本方法的执行时间是60ms
        try {
            TimeUnit.MILLISECONDS.sleep(60);
        } catch (InterruptedException e) {
            System.err.println(e);
        }
    }
}

解析: 在代码中,对比scheduleAtFixedRate(…)方法的代码,只修改了第11行,。开启两个线程,当线程启动1000ms后才开始运行,任务执行耗时为60ms,每隔(100+60)ms执行一次。通过下面的运行截图比对我们可以发现,即使开启了多个个线程,任务也是一次间隔160ms左右执行一次。
在这里插入图片描述

运行截图 4.2.4.3
4.2.4.4 周期任务小结

  计划任务的三个方法目前已经讲完了。方法schedule()会在给定时间,对任务进行一次调度。方法scheduleAtFixedRate()和方法scheduleWithFixedDelay()会对任务进行周期性调度

  • 方法schedule()会在给定的延迟时间后执行一次,方法scheduleAtFixedRate()会在任务提交的间隔时间T内周期执行,方法scheduleWithFixedDelay()会在上一次任务完成和下一次任务开始的间隔时间T内周期执行。
  • 值得注意的是两个周期任务存在给定的周期时间T内,存在无法完成任务的情况。则此时执行周期如下两图:

在这里插入图片描述
               运行截图 4.2.4.4.1:scheduleAtFixedRate()方法周期T>运行时间
在这里插入图片描述
               运行截图 4.2.4.4.2:scheduleWithFixedDelay()方法周期T>运行时间

五. ThreadPoolExecutor分析

5.1 ThreadPoolExecutor类介绍

  无论是newFixedThreadPool()方法、newSingleThreadExecutor()方法,还是newCachedThreadPool()方法,创建的线程虽然看起来有着完全不同的功能特点,但其内部实现均使用了ThreadPoolExecutor类。而在实际的开发中,我们也是推荐本类来创建线程池。因为上面介绍的用Executors创建的线程池或多或少的有些缺点

5.2 ThreadPoolExecutor构造方法介绍

  在ThreadPoolExecutor类的顶级构造方法中,共有七个参数,每个参数都有着重要的意义。其源码如下

 /**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

在这里插入图片描述

截图 5.2
  • corePoolSize:线程池中的核心线程数,即使空闲也依然会存活。但当ThreadPoolExecutor类的allowCoreThreadTimeOut属性设置为true(该值默认为false)时,空闲keepAliveTime时间后,核心线程也可以停止。
  • maximumPoolSize :线程池中的最大线程数量,必须大于等于corePoolSize
  • keepAliveTime:当线程池线程数量超过corePoolSize时,多余空闲线程的存活时间;即超过corePoolSize的空闲线程,不管有没有设置allowCoreThreadTimeOut属性,在空闲keepAliveTime时间后,空闲非核心线程会被回收。
  • unit:keepAliveTime的单位。
  • workQueue:任务队列,用于存放被提交但尚未被执行的任务。
  • threadFactory:线程工厂,定义如何创建新线程,一般用默认的即可。
  • handler:拒绝策略,当任务太多来不及处理时,如何拒绝任务。

  下面三个栏目将重点阐述一下workQueuethreadFactoryhandler,这也是彻底理解线程池的关键所在。

六. 四种阻塞队列

6.1 阻塞队列(针对Runnable的实例)

  workQueue中存放的是被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象。根据队列功能,在ThreadPoolExecutor类的构造函数中可使用四种BlockingQueue接口,即直接提交队列SynchronousQueue有界任务队列ArrayBlockingQueue无界任务队列LinkedBlockingQueue优先任务队列PriorityBlockingQueue

6.2 直接提交队列SynchronousQueue

6.2 .1 介绍

  SynchronousQueue队列是直接提交的队列,本质是一个特殊的BlockingQueue,SynchronousQueue没有容量,不能存放任何任务,每一个入队操作都要等待一个相应的出队操作,反之,每一个出队操作都要等待相应的入队操作。当使用SynchronousQueue时,提交的任务不会被真实地保存,而是将新任务提交给线程池中的线程执行,如果没有空闲的进程,则尝试创建新的进程,如果进程数量已经达到最大值(maximumPoolSize),则会触发拒绝策略。因此,使用SynchronousQueue队列,通常要设置很大的maximumPoolSize值,否则很容易执行拒绝策略,导致任务不能执行。

6.2 .2 使用

public class ThreadPoolExecutorTest {

    public static volatile int a = 0;

    //定义一个全局直接提交队列
    static SynchronousQueue synchronousQueue = new SynchronousQueue<Runnable> ();

    public static void main(String[] args) {

        //创建线程池。1个核心线程,4个非核心线程
        ExecutorService executorService = new ThreadPoolExecutor (1, 5, 10, TimeUnit.MILLISECONDS, synchronousQueue,
                Executors.defaultThreadFactory (), new ThreadPoolExecutor.DiscardPolicy ());
        ThreadRunTest thread = new ThreadRunTest ();
        //定义十万个线程
        for (int i = 0; i < 100000; i++) {
            executorService.execute ( thread);
        }
    }
}

class ThreadRunTest implements Runnable {

    @Override
    public void run() {
    	//  TimeUnit.MILLISECONDS.sleep (1); //加上这个更明显
        System.out.println ("当前线程为:" + Thread.currentThread ().getName () + "****" + ThreadPoolExecutorTest.a+++":queueSize:"+ThreadPoolExecutorTest.synchronousQueue.size());
    }
}

在这里插入图片描述

运行截图 6.2

  如上图所示,用5个线程并使用SynchronousQueue 阻塞队列,接受100000个任务,最终只执行上百个任务,就会完全停止;而queueSize中的大小一致都是0,很好的证明SynchronousQueue队列不存放任务,只作为一交换通道。

6.3 有界任务队列ArrayBlockingQueue

6.3.1 介绍

  有界的任务队列使用ArrayBlockingQueue类实现。ArrayBlockingQueue类的构造函数必须带一个容量参数,表示该队列的最大容量。容量大小的设置比较麻烦,多了撑爆内存,任务堆积,少了不能满足并发量。
  使用有界的任务队列,当有新的任务需要执行:

  1. 若线程池的实际线程数小于corePoolSize,则会优先创建新的线程,
  2. 若大于corePoolSize,则会将新任务加入等待队列。若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的进程执行任务。
  3. 若大于maximumPoolSize,则执行拒绝策略。有界队列仅当在任务队列装满时,才可能将线程数提升到corePoolSize以上,除非系统非常繁忙,否则要确保核心线程数维持在corePoolSize。

6.3.2 使用

public class ThreadPoolExecutorTest {

    public static volatile int a = 0;

    //定义一个有界任务队列(同6.2.1相比,改了队列,为了让效果更明显,同时更改了拒绝策略)
    static ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<Runnable> (15);

    public static void main(String[] args) {

        //创建线程池。1个核心线程,4个非核心线程,拒绝策略换成AbortPolicy 
        ExecutorService executorService = new ThreadPoolExecutor (1, 5, 10, TimeUnit.MILLISECONDS, arrayBlockingQueue,
                Executors.defaultThreadFactory (), new ThreadPoolExecutor.AbortPolicy ());
        ThreadRunTest thread = new ThreadRunTest ();
        //定义十万个线程
        for (int i = 0; i < 100000; i++) {
            executorService.execute (thread);
        }
    }
}
class ThreadRunTest implements Runnable {
    @Override
    public void run() {

        try {
            TimeUnit.MILLISECONDS.sleep (1);
        } catch (InterruptedException e) {
            e.printStackTrace ();
        }
        System.out.println ("当前线程为:" + Thread.currentThread ().getName () + "****" + ThreadPoolExecutorTest.a++ + ":queueSize:" + ThreadPoolExecutorTest.arrayBlockingQueue.size ());
    }
}

在这里插入图片描述

运行截图 6.3.2

6.4 无界任务队列LinkedBlockingQueue

6.5 优先任务队列PriorityBlockingQueue

七. 线程工厂

八. 四种拒绝策略

九. 分而治之:Fork/Join框架

十. 停止线程池的正确方法

总共有五个

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值