Java多线程并发04——合理使用线程池

在此之前,我们已经了解了关于线程的基本知识,今天将为各位带来,线程池这一技术。关注我的公众号「Java面典」了解更多 Java 相关知识点。

为什么使用线程池?线程池做的工作主要是控制运行的线程的数量。

线程池的种类

Java 中常用的线程池主要有四种:newCachedThreadPoolnewFixedThreadPoolnewScheduledThreadPoolnewSingleThreadExecutor

newCachedThreadPool

作用:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程(缓存中已有 60 秒钟未被使用的线程),若无可回收,则新建线程。

特点:线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

实现

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    final int index = i;
    try {
        Thread.sleep(index * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    cachedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(index);
        }
    });
}

newFixedThreadPool

作用:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

特点

  1. 如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要);
  2. 在某个线程被显式地关闭之前,池中的线程将一直存在。

定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。

实现

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
    final int index = i;
    fixedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}      

newScheduledThreadPool

作用:创建一个定长线程池,支持定时及周期性任务执行。

实现

ScheduledExecutorService scheduledThreadPoo l= Executors.newScheduledThreadPool(3); 

scheduledThreadPool.schedule(newRunnable() { 
    @Override 
    public void run() {
        System.out.println("延迟三秒");
    }
}, 3, TimeUnit.SECONDS);

scheduledThreadPool.scheduleAtFixedRate(newRunnable(){ 
    @Override 
    public void run() {
        System.out.println("延迟 1 秒后每三秒执行一次");
    }
}, 1, 3, TimeUnit.SECONDS);

newSingleThreadExecutor

作用:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

特点:这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
    final int index = i;
    singleThreadExecutor.execute(new Runnable() {

        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

线程池原理

作用线程复用控制最大并发数管理线程。线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

线程复用

每一个 Thread 的类都有一个 start() 方法。 当调用 start 启动线程时 Java 虚拟机会调用该类的 run() 方法。 那么该类的 run() 方法中就是调用了 Runnable 对象的 run() 方法。 我们可以继承重写Thread 类,在其 start () 方法中添加不断循环调用传递过来的 Runnable 对象。 这就是线程池的实现原理。循环方法中不断获取 Runnable 是用 Queue 实现的,在获取下一个 Runnable 之前可以是阻塞的。

线程池的组成

一般的线程池主要分为以下 4 个组成部分:

  1. 线程池管理器:用于创建并管理线程池;
  2. 工作线程:线程池中的线程;
  3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行;
  4. 任务队列:用于存放待处理的任务,提供一种缓冲机制。

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 这几个类。ThreadPoolExecutor 的构造方法如下:

/**
* @param corePoolSize 指定线程池中的线程数量
* @param maximumPoolSize 指定线程池中的最大线程数量
* @param keepAliveTime 当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间
* @param unit keepAliveTime 的单位
* @param workQueue 任务队列,被提交但尚未被执行的任务
*/
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
    // threadFactory 线程工厂,用于创建线程,一般用默认的即可
    // handler 拒绝策略,当任务太多来不及处理,如何拒绝任务
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
}

拒绝策略

作用:线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。

JDK 内置的拒绝策略如下

  1. AbortPolicy : 直接抛出异常,阻止系统正常运行;
  2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降;
  3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务;
  4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。

以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。

Java 线程池工作过程

  1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们;
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    a). 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
    b). 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
    c). 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要
    创建非核心线程立刻运行这个任务;
    d). 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行;
  4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

多线程与并发系列推荐

Java多线程并发03——什么是线程上下文,线程是如何调度的

Java多线程并发02——线程的生命周期与常用方法,你都掌握了吗

Java多线程并发01——线程的创建与终止,你会几种方式

posted @ 2020-03-19 21:41  xwayway  阅读(1266)  评论(0编辑  收藏  举报