线程池

为什么要使用线程池

1.可以减少线程创建和销毁带来的性能消耗
2.提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
3.提高线程的可管理性

线程池的实现原理

线程池的作用是有效的降低频繁创建销毁线程所带来的额外开销。一般来说,线程池都是采 用预创建的技术,在应用启动之初便预先创建一定数目的线程。应用在运行的过程中,需要时 可以从这些线程所组成的线程池里申请分配一个空闲的线程,来执行一定的任务。任务完成 后,并不是将线程销毁,而是将它返还给线程池,由线程池自行管理。
如果线程池中大多线程处于空闲状态,为了节省系统资源,线程池就会动态销毁其中一部分空闲的线程

线程池的结构

1.Workqueue:任务队列,用于存放待执行任务
2.Worker:工作类,一个worker代表启动了一个线程,它启动后会循环执行workQueue里 面的所有任务

线程池的执行流程

1.一个任务提交,如果线程池大小没达到corePoolSize,则每次都创建一个worker也就是一 个线程来立即执行。
2.如果达到corePoolSize,则把多余的任务放到workQueue,等待已创建的worker来循环 执行
3.如果队列workQueue都放满了还没有执行,则在maximumPoolSize下面创建新的worker 来循环执行workQueue
4.如果启动到maximumPoolSize还有任务进来,线程池已达到满负载,此时就执行任务拒 绝RejectedExecutionHandler
代码:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    //1.当前池中线程比核心数少,新建一个线程执行任务
    if (workerCountOf(c) < corePoolSize) {   
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //2.核心池已满,但任务队列未满,添加到队列中
    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);
    }
    //3.核心池已满,队列已满,试着创建一个新线程
    else if (!addWorker(command, false))
        reject(command);    //如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务
}
线程池的主要应用场景

1.需要大量的线程来完成任务,且完成任务的时间比较短,如WEB服务器完成网页请求这 样的任务。因为单个任务小,而任务数量巨大。
2.对性能要求苛刻的应用,比如要求服务器迅速响应客户请求

ThreadPoolExecutor类

参数

1.keepAliveTime:代表的就是线程空闲后多久后销毁,线程的销毁是通过worker的 getTask()来实现的。一般来说,Worker会循环获取getTask(),如果getTask()返回null则 工作线程worker终结。

  1. corePoolSize(线程池的基本大小):核心线程数,核心线程会一直存活,即使没有任务需 要处理。当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线 程来处理任务,而不是直接交给现有的线程处理。
  2. maximumPoolSize(线程池最大大小):当线程数大于或等于核心线程数,且任务队列已 满时,线程池会创建新的线程,直到线程数量达到maximumPoolSize。如果线程数已等于 maximumPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而 抛出异常。
  3. queueCapacity(任务队列容量):从maxPoolSize的描述上可以看出,任务队列的容量会影 响到线程的变化,因此任务队列的长度也需要恰当的设置。
  4. timeunit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性。
  5. RunnableTaskQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会 对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
    i. ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
    ii. LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大 小,则默认为Integer.MAX_VALUE;
    iii. SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建 一个线程来执行新来的任务。
  6. threadFactory:线程工厂,主要用来创建线程;
  7. RejectedExecutionHandler:表示当拒绝处理任务时的策略,有以下四种取值:
    i. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异 常
    ii. ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    iii. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    iv. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
方法:
  1. execute():向线程池提交一个任务,交由线程池去执行。
  2. submit():来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行 的结果,内部调用的execute()方法,只不过它利用了Future来获取任务执行结果。
  3. shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才 终止,但再也不会接受新的任务
  4. shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队 列,返回尚未执行的任务
线程池的调优

1.调整线程池的大小-线程池的最佳大小取决于可用处理器的数目以及工作队列中的任务的性质
2.CPU密集型任务,线程数目少一些,例如CPU+1个
3.I/O密集型任务,线程多一些,例如2*CPU个
4.混合型任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个I/O密集型任务,只要两个任务执行时间相差不是特别大,那么分解后执行的吞吐量要比串行的高,如果任务执行时间相差太大就不用分解,用Runtime.getRuntime().availableProcessors()获取当前设备CPU数目

使用线程池的注意事项

(1) 线程池的大小。 多线程应用并非线程越多越好, 需要根据系统运行的软硬件环境以及应用本身的特点决定线程池的大小。 一般来说, 如果代码结构合理的话, 线程数目与 CPU数量相适合即可。 如果线程运行时可能出现阻塞现象, 可相应增加池的大小; 如有必要可采用自适应算法来动态调整线程池的大小, 以提高 CPU 的有效利用率和系统的整体性能。
(2) 并发错误。 多线程应用要特别注意并发错误, 要从逻辑上保证程序的正确性, 注意避免死锁现象的发生。
(3) 线程泄漏。 这是线程池应用中一个严重的问题, 当任务执行完毕而线程没能返回池中就会发生线程泄漏现象。

常用的线程池

线程池_第1张图片
image.png

你可能感兴趣的:(线程池)