java并发程序设计总结六:线程池

来源:互联网 发布:人工智能带来的便利 编辑:程序博客网 时间:2024/05/19 23:09
多线程的软件设计方法确实可以最大限度的发挥现代多核处理器的计算能力,但是如果不加控制和管理的随意使用创建线程,对系统的性能反而会产生不利的影响。


不适用线程池的线程优化

可以使用如下方式来创建线程,他会在run结束之后,自动回收线程资源:
new Thread(new Runnable(){    public void run(){        // do something...    }}.start();
在线程比较少的情况下使用这种方式到是没有什么问题,但是在真实环境中,通常需要创建很多的线程,而这样反而会耗尽CPU和内存资源


线程池的概念

使用过数据库开发的话,我们应该都知道数据库连接池,为了避免每次数据库查询都重新建立和销毁数据库连接,我们可以在数据库连接池中维护一些数据库连接connection并让他们长期保持在激活状态。这样一来,每当系统需要使用数据库时,并不是创建一个新的连接,而是直接从连接池中取出一个连接对象;而每当用完的时候,不是去关闭连接,而是将它还给数据库连接池。通过这种方式,可以节省不少的创建和销毁对象的时间。线程池和数据库连接池的原理是一样的


JDK对线程池的支持

为了能够更好的控制多线程,JDK提供了一套Executor框架,存在于java.util.concurrent包中,是jdk并发包的核心类。其中,ThreadPoolExecutor/ExecutorService/ScheduleExecutorService都可以表示一个线程池,存放线程对象;Executors类则扮演者线程池工厂的角色,通过Executors的一系列方法可以取得特定功能的线程池。,主要方法如下:
public static ExecutorService newFixedThreadPool(int cnt);返回一个固定线程数量的线程池,当有新任务提交时,如果在线程池中有空闲线程,则立即执行,否则该任务就会被暂存到一个任务队列中,待有空闲线程时,便处理在任务队列中的任务public static ExecutorService newSingleThreadExecutor();返回一个单例的(只有一个线程的)线程池,若多余一个任务被提交到线程池中,则多出来的任务也会被暂存到一个任务队列中,待线程空闲时,以先进先出的顺序执行队列中的任务public static ExecutorService newCacheThreadPool();返回一个可根据实际情况调整线程数量的线程池,当提交的任务多余当前线程池中的线程数量时,线程池会创建新的线程来提交任务,待任务完成后,新创建的线程将会返回到线程池中进行复用;线程池中的线程数量会动态发生改变:每次会缓存上一次新增的线程到线程池中public static ScheduledExceutorService newSingleThreadScheduledExecutor();返回一个单例的ScheduledExecutorService线程池,该线程池接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能public static ScheduledExecutorService newScheduledThreadPool(int cnt);返回一个可以指定线程数量的ScheduledExecutorService线程池

线程池的简单使用

package org.blog.controller;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * @ClassName: Main1 * @Description: 线程池的简单使用实例code ExecutorService * @author Chengxi * @Date: 2017-10-23下午6:45:59 * *  */public class Main1 {    public static class tpoll implements Runnable{        public void run(){            System.out.println("runing...");        }    }    public static void main(String[] args){        tpoll poll = new tpoll();        //创建一个大小为10的线程池        ExecutorService exec = Executors.newFixedThreadPool(10);        for(int i=0; i<10; i++){            //提交线程任务            exec.submit(poll);        }        //关闭线程池        exec.shutdown();    }}
在最后我们使用了shutdown关闭了线程池,如果省略这行代码,会发现程序将一直处于运行状态,因为线程池中的线程会一直处于激活状态


计划任务:ScheduledExecutorService线程池

ScheduledExecutorService线程池扩展了ExecutorService线程池,它可以根据时间需要对线程进行调度。它的主要方法签名如下:
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);该方法会在给定时间之后对任务进行一次调度(不会进行周期性调度)public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit);第一次任务在经过initialDelay时间之后开始执行,第二次任务在第一次任务执行完成后立即执行,每次的执行时间为Math.max(任务本身的执行时间,period);也就是说除了第一次任务需要延迟initialDelay时间外,其余的任务在前面的任务完成之后立即执行public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);第一次执行任务在经过initialDelay+delay时间之后开始执行,之后的每一次任务都会在前面的任务完成之后的delay延迟之后开始执行(即每次任务执行都会有个延迟)
下面通过一个实例code来解析这三个方法(主要是分辨后面两个方法的区别)
package org.blog.controller;import java.util.concurrent.Executors;import java.util.concurrent.ScheduledExecutorService;import java.util.concurrent.TimeUnit;/** * @ClassName: Main2 * @Description: ScheduledExecutorService的使用实例code * @author Chengxi * @Date: 2017-10-23下午7:47:15 * *  */public class Main2 {    public static class schedulethread implements Runnable{        public void run(){            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }            System.out.println("进行任务调度中");        }    }    public static void main(String[] args) throws InterruptedException{        //创建一个ScheduledExecutorService线程池(单例模式)        ScheduledExecutorService schedule = Executors.newSingleThreadScheduledExecutor();        schedulethread test = new schedulethread();        //调用schedule方法指定时候后执行一次任务//      schedule.schedule(test, 3, TimeUnit.SECONDS);//      Thread.sleep(3000);//      schedule.shutdown();        /**         * schedule打印结果为:         *      执行任务调度中         * 随后程序结束!         *  总结:schedule只会在指定延迟后调用一次         */        //执行scheduleAtFixRate方法来执行任务//      schedule.scheduleAtFixedRate(test,0,1,TimeUnit.SECONDS);        /**         * scheduleAtFixedRate打印结果为:         *      每隔一秒打印一次“执行任务调度中”         * 程序一直不会结束!         *  总结:scheduleAtFixedRate除了第一次任务会进行initialdelay延迟外,后面的任务都是紧接着执行的         */        //执行scheduleWithFixDelay方法来执行任务//      schedule.scheduleWithFixedDelay(test, 0, 1, TimeUnit.SECONDS);        /**         * scheduleWithFixedDelay打印结果为:         *      每隔两秒打印一次“执行任务调度中”         * 程序一直不会结束!         *  总结:scheduleWithFixedDelay除了第一次任务会进行initialdelay+period延迟外,后面的任务都是period延迟后执行         */    }}


核心线程池的内部实现

前面所说的几个创建线程池的方法,其实无论是newFixedThreadPool还是newSingleThreadExecutor还是newCachedThreadPool,他们的内部实现都是通过ThreadPoolExecutor构造器,其方法声明如下:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {    return new ThreadPoolExecutor(nThreads, nThreads,                                      0L, TimeUnit.MILLISECONDS,                                      new LinkedBlockingQueue<Runnable>(),                                      threadFactory);}public static ExecutorService newSingleThreadExecutor() {    return new FinalizableDelegatedExecutorService            (new ThreadPoolExecutor(1, 1,                                    0L, TimeUnit.MILLISECONDS,                                    new LinkedBlockingQueue<Runnable>()));}public static ExecutorService newCachedThreadPool() {    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                      60L, TimeUnit.SECONDS,                                      new SynchronousQueue<Runnable>());}
对于ThreadPoolExecutor类来说,他有很多的重载构造器,如下是它的包含所有的参数的构造器:
public ThreadPoolExecutor(int corePoolSize,                              int maximumPoolSize,                              long keepAliveTime,                              TimeUnit unit,                              BlockingQueue<Runnable> workQueue,                              ThreadFactory threadFactory,                              RejectedExecutionHandler handler);corePoolSize:指定了线程池中的线程数量maximumPoolSize:指定了线程池中的最大线程数量(对于cached可扩增的线程池来说该参数有用)keepAliveTime:指定了线程池中线程数量超过corePoolSize时,多余的空闲线程的存活时间unit:指定存活时间的时间单位workQueue:任务队列,存放被提交但是还未被执行的任务threadFactory:线程工厂,用于创建线程,一般使用默认的线程工厂即可handler:拒绝策略,当等待任务超过maximunPoolSize或者说太多了来不及处理时使用指定的策略来拒绝任务
对于ThreadPoolExecutor线程池的构造器,其中的workQueue和handler需要进行详细的说明
workQueue是一个BlockingQueue接口的对象,仅仅用于存放Runnable对象,根据队列的功能分类,可以分为如下几种任务队列:1.直接提交的队列SynchronousQueue:synchronousQueue没有容量,如果提交任务时没有空闲的线程,则会尝试重新创建新的线程,如果线程数量已经到达maximumPoolSize,则执行拒绝策略2.有界的任务队列ArrayBlockingQueue:该队列的构造函数必须带有一个容量参数,表示队列的最大容量:public ArrayBlockingQueue(int cnt);3.无界的任务队列LinkedBlockingQueue:该队列相比有界的而言,不存在任务入队失败的情况,除非系统资源耗尽。也可以通过该队列的一个有参构造函数来指定队列的最大容量,使之也变成有界队列public LinkedBlockingQueue();public LinkedBlockingQueue(int cnt);4.优先任务队列PriorityBlockingQueue:带有执行优先级的队列,可以控制任务的执行先后顺序。无论是ArrayBlockingQueue还是LinkedBlockingQueue,都是按照先进先出算法处理任务的


拒绝策略

在jdk中内置提供了四种拒绝策略:1.AbortPolicy:该策略会直接抛出异常,组织系统正常工作2.CallerRunsPolicy:只要线程池未关闭,该策略就会直接在调用者线程中,运行当前打算被丢弃的任务。该策略不会丢弃任务,不过任务提交线程的性能会急剧下降3.DiscardOldestPolicy:该策略会丢弃最老的一个请求DiscardPolicy:该策略会默默的丢弃无法处理的任务,不会进行任何处理
以上内置的四种策略都实现了RejectedExecutionHandler接口,我们也可以通过实现该接口来实现自己的拒绝策略。该接口的定义如下:
public interface RejectedExecutionHandler{    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);}其中r表示当前请求执行的任务,executor表示当前的线程池


线程工厂ThreadFactory

线程工厂主要是用于创建线程池中的线程对象,ThreadFactory是一个接口,它只有一个方法,用来创建线程,当线程池需要新建线程时,就会调用这个方法:
Thread newThread(Runnable r);
我们也可以通过实现ThreadFactory接口来实现自定义创建线程对象


扩展线程池

ThreadPoolExecutor是一个可扩展的线程池,它提供了三个方法用于对线程池进行控制
线程准备执行时调用protected void beforeExecute(Thread t, Runnable r);线程执行完毕后调用protected void afterExecute(Runnable r, Throwable t);线程池调用shutdown关闭后调用protected void terminated();


线程池异常隐藏

在这里需要注意的是,当我们调用线程池的submit方法来提交任务时,如果当前任务有错误,最终执行的结果中是看不到错误的,只会让该结果不显示出来,可以说线程池将错误吃了。对于这种情况,我们可以使用两种办法解决:1.使用execute代替submit提交任务;2.简单修改一下submit方式提交:
Future res = exec.submit(run);res.get();
通过上面两种方法之一修改提交,最终输出的结果会打印出错误时的堆栈异常信息