Java中的线程池

来源:互联网 发布:无法更改mac地址 编辑:程序博客网 时间:2024/06/07 04:37

线程池是使用场景最多的并发框架,几乎所有需要异步和并发执行任务的程序都可以使用线程池。类似于数据库链接池,使用线程池能为程序带来如下好处:

1、降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2、提高相应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
3、提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控。

线程池的实现原理

与数据库连接池类似,线程池也一句话概括:通过将事先创建好的线程存放起来,在需要的时候直接拿过来使用就可以了。但是,为了提高线程池的性能,实际的线程池要比这复杂得多。当接受到一个新任务到线程池的时候,线程池会有如下处理流程:

1.线程池首先判断核心线程池中线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果都在执行任务,也就是没有空闲线程的话就进入下个流程
2.线程池继续判断工作队列是否已经满了。如果工作队列没有满,则把新提交的任务放入该工作队列中,如果工作队列已经满了,则进入下个流程
3.线程池判断线程池中的线程是否都处于工作状态。如果不是,则创建一个新的线程执行提交的任务,如果是,则执行饱和策略。

执行流程:

这里写图片描述

在Java中实现的线程池的核心类是ThreadPoolExecutor,该类的execute方法的执行流程就是上面的过程。注意三个关键字:核心线程池工作队列饱和策略

细化到ThreadPoolExecutor执行execute方法的过程,对上面的过程补充如下:

  • 核心线程池对应corePoolSize变量的值,如果运行的线程小于corePoolSize,则创建新的线程执行任务(这个过程需要获取全局锁);
  • 如果运行的线程大于corePoolSize,则将任务加入BlockingQueue(对应工作队列,阻塞队列);
  • 如果无法加入则创建新的线程执行任务,这个步骤中,如果创建新线程后当前运行的线程数大于maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExcution()方法。

ThreadPoolExecutor为了避免执行新提交的任务获取全局锁,ThreadPoolExecutor在创建后会执行一个预热过程,所谓预热就是让当前运行的线程数大于等于corePoolSize。这样,后面新提交的任务都将直接加入到BlockingQueue。而这个过程是不需要获取全局锁的,自然就能提高线程池的性能。

源码分析根据上面的分析,在查看ThreadPoolExecutor源码来加深理解

public void execute(Runnable command) {        if (command == null)            throw new NullPointerException();        int c = ctl.get();        //如果当前正在运行的线程数小于corePoolSize,则创建新的线程        //执行当前任务        if (workerCountOf(c) < corePoolSize) {            if (addWorker(command, true))                return;            c = ctl.get();        }        //如果当前运行的线程数大于等于corePoolSize或者线程创建失败        //则把当前任务放入工作队列        if (isRunning(c) && workQueue.offer(command)) {            int recheck = ctl.get();            //判断之前是否已经添加过线程执行该任务(因为可能之前)            //创建的线程已经死亡了)或者线程池是否已经关闭。如果            //两个答案都是肯定的,那么选择拒绝执行任务            if (! isRunning(recheck) && remove(command))                reject(command);            else if (workerCountOf(recheck) == 0)                addWorker(null, false);        }        //如果线程池任务无法加入到工作队列(说明工作队列满了)        //创建一个线程执行任务。如果新创建后当前运行的线程数大于        //maximumPoolSize则拒绝执行任务        else if (!addWorker(command, false))            reject(command);    }

如果线程池能够创建线程执行任务,那么将调用addWorker方法,将线程池创建的线程封装为Worker,Worker在执行完任务后还会循环获取队列中任务来执行。看看addWorker方法的源码:

 private boolean addWorker(Runnable firstTask, boolean core){        //省略部分代码        boolean workerStarted = false;        boolean workerAdded = false;        Worker w = null;        try {            //这里就将提交的任务封装成为Worker了            w = new Worker(firstTask);            final Thread t = w.thread;            if (t != null) {                //使用加锁的方式原子添加工作线程                final ReentrantLock mainLock = this.mainLock;                mainLock.lock();                try {                    //在获得锁期间再次检查线程池的运行状态:如果                    //线程池已经关闭或者任务为空则抛出异常                    int rs = runStateOf(ctl.get());                    if (rs < SHUTDOWN ||                        (rs == SHUTDOWN && firstTask == null)) {                        if (t.isAlive())                             throw new IllegalThreadStateException();                        //加入Worker数组                        workers.add(w);                        int s = workers.size();                        if (s > largestPoolSize)                            largestPoolSize = s;                        workerAdded = true;                    }                } finally {                    mainLock.unlock();                }                if (workerAdded) {                    //如果添加成功则启动线程执行任务                    t.start();                    workerStarted = true;                }            }        } finally {            if (! workerStarted)                addWorkerFailed(w);        }        return workerStarted;    }

之后我们看看执行t.start()后会发生的事,因为Worker本身实现了Runnable,所以start后将调用Worker的run方法,源码如下:

 public void run() {           runWorker(this);       }       final void runWorker(Worker w) {        Thread wt = Thread.currentThread();        Runnable task = w.firstTask;        w.firstTask = null;        w.unlock(); // allow interrupts        boolean completedAbruptly = true;        try {            while (task != null || (task = getTask()) != null) {                w.lock();                try {                    beforeExecute(wt, task);                    Throwable thrown = null;                    task.run();                    afterExecute(task, thrown);                } finally {                    task = null;                    w.completedTasks++;                    w.unlock();                }            }            completedAbruptly = false;        } finally {            processWorkerExit(w, completedAbruptly);        }   }

以上源码其实就干了一件事:创建的线程在执行完提交的任务后会反复从BlockingQueue中获取任务来执行。

使用线程池

线程池的创建,使用ThreadPoolExecutor来创建:
构造方法:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler)

参数说明:

corePoolSize: 线程池的基本大小

提交一个任务到线程池时,线程池会创建一个线程来执行任务。即使线程池中有空闲的线程可以执行该任务,线程池也会创建线程。等到需要执行的任务数大于线程池的基本大小时就不会再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程就会提前创建并启动所有的基本线程。

maximumPoolSize:线程池维护线程的最大数量

线程池允许创建的最大线程数。如果队列满了,并且创建的线程数小于最大线程数,则线程池会创建新的线程执行任务。如果使用了无界任务队列,则这个参数就没什么用。

keepAliveTime: 线程池维护线程所允许的空闲时间

线程池的工作线程空闲后,保持的存活时间。如果任务多,可以增大时间提高线程的利用率。

unit: 线程池维护线程所允许的空闲时间的单位

单位有天 小时 分钟 毫秒 微秒 千分之一毫秒 千分之一微妙

workQueue: 线程池所使用的任务队列

用来保存等待执行任务的阻塞队列。可以选择的有:

ArrayBlockingQueue
LinkedBlockingQueue(吞吐量高于ArrayBlockingQueue,静态工厂Executors.newFixedThreadPool()默认使用它)
SynchronousQueue(吞吐量高于LinkedBlockingQueue,静态工厂Executors.newCachedThreadPool()默认使用它)
PriorityBlockingQueue 无界阻塞队列,使用它则会让maximumPoolSize参数无效

handler: 线程池对拒绝任务的处理策略(饱和策略)

当队列和线程池都满了,说明线程池处于饱和状态。那么必须采用一种策略处理提交的新任务,这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛异常。在jdk 1.5中主要提供了如下几种策略。

  • AbortPolicy 直接抛出异常
  • DiscardOldestPolicy 丢弃队列中最近的一个任务,并执行当前任务
  • CallerRunsPolicy 只用调用者所在线程来运行任务
  • DiscardPolicy 不处理,丢弃掉
    当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志,持久化存储不能处理的任务等等。

向线程池提交任务

使用两个方法来为线程池提交任务:execute() submit()

execute方法用于提交不需要返回值的任务,所以无法判断是否被线程池成功。实例代码:

threadspool.execute(new Runnable{@Overridepublic void run(){   doSomething();}});

submit方法用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过它可以判断执行是否成功

future有个阻塞式的get()方法,通过它可以获取线程的返回值,它还有个重载方法get(long timeout,TimeUnit unit)方法会阻塞一段时间后返回,但这个时候有可能任务没有执行完。

关闭线程池

调用线程池的shutdown或者shutdownNow方法来关闭线程池。它们的原理是遍历线程池的工作线程,然后逐个调用interrupt方法来中断线程,所以没有响应中断的任务永远无法停止。它们两者还是有点区别,shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。shutdown只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。

线程池的监控

通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用

  • taskCount:线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。
  • largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
    *getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不减。
  • getActiveCount:获取活动的线程数。

通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,让我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。

建议使用有界队列
有界队列能增加系统的稳定性和预警能力。尤其是依赖与数据库操作的任务,因为等待数据库返回结果比较耗时,此时建议使用容量大一点的队列,线程数也建议设大,这样才能更好利用CPU。

现在我们编码来联系线程池:

 public  static void main(String[] str){      //创建一个顺序存储的阻塞队列,并指定大小为10        BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(10);        //创建线程池的饱和策略,AbortPolicy抛异常        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();        //创建线程池,线程池基本大小3 最大线程数为5 线程最大空闲时间10分钟        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,5,10, TimeUnit.MINUTES,blockingQueue,handler);        /*         *我们给线程池的基本是3,现在我们给线程池提交四个任务,并不断的查询线程数。         */        //使用execution方法提交任务        threadPoolExecutor.execute(new Runnable() {            @Override            public void run() {                System.out.println("当前线程ID"+Thread.currentThread().getId());                System.out.println("我是线程1号");                try {                    Thread.sleep(1000000);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        //获取活动线程数        System.out.println("启动一个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount());        threadPoolExecutor.execute(new Runnable() {            @Override            public void run() {                System.out.println("当前线程ID"+Thread.currentThread().getId());                System.out.println("我是线程2号");                try {                    Thread.sleep(1000000);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        //获取活动线程数        System.out.println("启动二个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount());        threadPoolExecutor.execute(new Runnable() {            @Override            public void run() {                System.out.println("当前线程ID"+Thread.currentThread().getId());                System.out.println("我是线程3号");                try {                    Thread.sleep(1000000);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        //获取活动线程数        System.out.println("启动三个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount());        /*         * 第四个任务应该是不会被执行的,而被存储在任务队列中。等待其他任务被执行完。         */        //提交四号线程后。此线程应该存储在队列中        threadPoolExecutor.execute(new Runnable() {            @Override            public void run() {                System.out.println("当前线程ID"+Thread.currentThread().getId());                System.out.println("我是线程4号");                try {                    Thread.sleep(1000000);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        });        //获取活动线程数        System.out.println("启动四个线程后当前活动线程数量:"+threadPoolExecutor.getActiveCount());    }

运行结果:

启动一个线程后当前活动线程数量:1启动二个线程后当前活动线程数量:2当前线程ID11当前线程ID12启动三个线程后当前活动线程数量:3启动四个线程后当前活动线程数量:3当前线程ID10我是线程1号我是线程3号我是线程2号

可以看到。在任务队列没有满的情况下,线程池只会创建基本数量的线程3个。对于这三个线程没有空闲的时候。第四个线程则会被存储到任务队列,等待空闲线程。

现在我们对上述代码做如下修改:

将任务队列容量设置为1

BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(1);

对线程池再提交一个任务,即在任务队列为1情况下提交5个任务

此时的打印为:

启动一个线程后当前活动线程数量:1当前线程ID10我是线程1号启动二个线程后当前活动线程数量:2当前线程ID11我是线程2号启动三个线程后当前活动线程数量:3启动四个线程后当前活动线程数量:3当前线程ID12我是线程3号启动五个线程后当前活动线程数量:4当前线程ID13我是线程5号

可以看的到,当线程池的基本线程数量满了以后,继续提交的线程会保持在任务队列,上代码中四号任务被保存在任务队列等待基本线程来执行它。此时继续提交线程给线程池,由于我们的任务队列已经满了(只初始化了1,并只保存了四号任务),此时对于第5号线程则会创建工作线程来执行它。如果继续不断的添加任务给线程池,但任务数超过参数二的时候,则会执行饱和策略。

参考 《Java并发编程的艺术》

0 1
原创粉丝点击