Java中的线程池

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

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{
@Override
public 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
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Java线程池的参数包括以下7个: 1. corePoolSize:线程池的基本大小,即在没有任务需要执行的时候线程池的大小。 2. maximumPoolSize:线程池最大的大小,即线程池允许的最大线程数。 3. keepAliveTime:线程池线程空闲后,保持存活的时间。 4. unit:keepAliveTime的时间单位。 5. workQueue:任务队列,用于保存等待执行的任务的阻塞队列。 6. threadFactory:线程工厂,用于创建新线程。 7. handler:拒绝策略,用于当任务队列已满,且线程池线程数达到maximumPoolSize时,如何拒绝新任务的策略。 下面是一个示例代码,展示了如何使用Java线程池参数: ```java import java.util.concurrent.*; public class ThreadPoolDemo { public static void main(String[] args) { // 创建一个线程池 ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // corePoolSize 4, // maximumPoolSize 60, // keepAliveTime TimeUnit.SECONDS, // unit new ArrayBlockingQueue<Runnable>(4), // workQueue Executors.defaultThreadFactory(), // threadFactory new ThreadPoolExecutor.AbortPolicy() // handler ); // 提交任务 for (int i = 0; i < 10; i++) { executor.execute(new Task(i)); } // 关闭线程池 executor.shutdown(); } static class Task implements Runnable { private int num; public Task(int num) { this.num = num; } @Override public void run() { System.out.println("正在执行task " + num); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("task " + num + "执行完毕"); } } } ```
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值