【JavaSE】线程池的概念及使用

线程池

Java线程池是运用最多的并发框架,线程池的优点如下:
1.降低资源消耗:通过重复利用已创建的线程降低线程的创建和销毁带来的消耗。
2.提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
3.提高线程的可管理性:由线程池统一管理调度进行线程分配和监控。

一、为什么要用线程池?
线程池解决的核心问题就是资源管理问题。在并发环境下,系统不能够确定在任意时刻中,有多少任务需要执行,有多少资源需要投入。这种不确定性将带来以下若干问题:

  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
  2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。
    为解决资源分配这个问题,线程池采用了“池化”(Pooling)思想。池化,顾名思义,就是将资源统一在一起管理的一种思想。
    线程池的主要思想是:在进程开始时创建一定数量的线程,并加到池中以等待工作。当服务器收到请求时,它会唤醒池内的一个线程(如果有可用线程),并将需要服务的请求传递给它。一旦线程完成了服务,它会返回到池中再等待工作。如果池内没有可用线程,那么服务器会等待,直到有空线程为止。
    线程池具有以下优点:
  4. 用现有线程服务请求比等待创建一个线程更快。
  5. 线程池限制了任何时候可用线程的数量。这对那些不能支持大量并发线程的系统非常重要。
  6. 将要执行任务从创建任务的机制中分离出来,允许我们采用不同策略运行任务。例如,任务可以被安排在某一个时间延迟后执行,或定期执行。
    二、线程池的核心参数
    Java中的线程池核心实现类是ThreadPoolExecutor,ThreadPoolExecutor实现的顶层接口是Executor,顶层接口Executor提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供Runnable对象,将任务的运行逻辑提交到执行器(Executor)中,由Executor框架完成线程的调配和任务的执行部分。ThreadPoolExecutor,主要构造方法:
    public ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)
  • corePoolSize:核心线程大小,当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使有其他空闲线程可以处理任务也会创新线程,等到工作的线程数大于核心线程数时就不会在创建了。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前把核心线程都创造好,并启动

  • maximumPoolSize:线程池允许创建的最大线程数。如果队列满了,并且以创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。如果我们使用了无界队列,那么所有的任务会加入队列,这个参数就没有什么效果了

  • keepAliveTime:线程池的工作线程空闲后,保持存活的时间。如果没有任务处理了,有些线程会空闲,空闲的时间超过了这个值,会被回收掉。如果任务很多,并且每个任务的执行时间比较短,避免线程重复创建和回收,可以调大这个时间,提高线程的利用率。

  • unit:keepAliveTIme的时间单位,可以选择的单位有天、小时、分钟、毫秒、微妙、千分之一毫秒和纳秒。类型是一个枚举java.util.concurrent.TimeUnit,这个枚举也经常使用,有兴趣的可以看一下其源码

  • workQueue:工作队列,用于缓存待处理任务的阻塞队列,常见的有4种,后面有介绍

  • threadFactory:线程池中创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字

  • handler:饱和策略,当线程池无法处理新来的任务了,那么需要提供一种策略处理提交的新任务,默认有4种策略
    example:
    public class Demo1 {
    static ThreadPoolExecutor executor = new ThreadPoolExecutor(3,
    5,
    10,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue(10),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
    int j = i;
    String taskName = “任务” + j;
    executor.execute(() -> {
    //模拟任务内部处理耗时
    try {
    TimeUnit.SECONDS.sleep(j);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + taskName + “处理完毕”);
    });
    }
    //关闭线程池
    executor.shutdown();
    }
    }

三、任务调度流程
首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

四、线程池中常见5种工作队列
任务太多的时候,工作队列用于暂时缓存待处理的任务,JDK中常见的5种阻塞队列:

  1. ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按照先进先出原则对元素进行排序
  2. LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按照先进先出排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool使用了这个队列。
  3. SynchronousQueue :一个不存储元素的阻塞队列,每个插入操作必须等到另外一个线程调用移除操作,否则插入操作一直处理阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用这个队列
  4. PriorityBlockingQueue:一个具备优先级的无限阻塞队列,进入队列的元素按照优先级会进行排序
  5. DelayQueue:没有大小限制的队列,只有当其指定的延迟时间到了,才可以从队列中获取到该元素,所有往队列中插入数据的操做(生产者)永远不会被阻塞,而只有获取数据的操做(消费者)才会被阻塞。常见的例子是使用一个DelayQueue来管理一个超时未响应的链接队列。
    五、四种常见饱和策略
    AbortPolicy:直接抛出异常
    CallerRunsPolicy:在当前调用者的线程中运行任务,即随丢来的任务,由他自己去处理
    DiscardOldestPolicy:丢弃队列中最老的一个任务,即丢弃队列头部的一个任务,然后执行当前传入的任务
    DiscardPolicy:不处理,直接丢弃掉,方法内部为空

六、Executors类
Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。常用的方法有

  • newSingleThreadExecutor
    创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。

  • newFixedThreadPool
    创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,在提交新任务,任务将会进入等待队列中等待。

  • newCachedThreadPool
    创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒处于等待任务到来)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。

  • newScheduledThreadPool
    创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

线程池的处理流程
1.首先判断核心线程池里的线程是否都在执行任务。若不是,则创建一个新的工作线程来执行任务,如果核心线程都在执行任务,转到下一个流程。
2.线程池判断工作队列是否已满,如果队列没有满,则交给工作队列处理,若已满则进入下一个流程。
3.线程池判断线程池的线程是否都已处于工作状态,若没有,则创建下一个工作线程执行任务,如果已满,则交给饱和策略处理任务。

线程池的使用

线程池构造方法:

public ThreadPoolExecutor(int corePoolSize,//线程池的基本大小
                              int maximumPoolSize,//线程池最大数量
                              long keepAliveTime,//线程活动保持时间
                              TimeUnit unit,//线程活动保持时间单位
                              BlockingQueue<Runnable> workQueue,//阻塞队列
                              RejectedExecutionHandler handler//饱和策略)

普通方法

void execute(Runnable command) 
在将来某个时候执行给定的任务。 

<T> Future<T> submit(Callable<T> task) 
提交值返回任务以执行,并返回代表任务待处理结果的Future。  

Future<?> submit(Runnable task) 
提交一个可运行的任务执行,并返回一个表示该任务的未来。  

<T> Future<T> submit(Runnable task, T result) 
提交一个可运行的任务执行,并返回一个表示该任务的未来。  

1.执行excute方法

public class TestPool {
    public static void main(String[] args) {
        Runnable myrun=new MyRunn();//创建线程实现类
                                                      //核心线程数    最大线程数           线程活动保持时间
        ThreadPoolExecutor pool=new ThreadPoolExecutor(3,5,1000,
                //线程活动保持时间的单位   饱和策略
                TimeUnit.MILLISECONDS,new LinkedBlockingDeque<Runnable>());
        for(int i=0;i<5;i++){
            //用excute()方法提交不需要返回值的任务,不能判断任务是否被线程池执行成功
            pool.execute(myrun);//线程池对象调用excute()方法启动线程,不需调用start方法

        }
    }
}
class MyRunn implements  Runnable{

    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName() + "+" + i);
        }
    }
}

2.sublmit方法

public class TestPoolSubmit{
    public static void main(String[] args) {
        Callable callable=new MyCall();//创建线程实现类

                                                      //核心线程数    最大线程数           线程活动保持时间
        ThreadPoolExecutor pool=new ThreadPoolExecutor(3,5,1000,
                //线程活动保持时间的单位   饱和策略
                TimeUnit.MILLISECONDS,new LinkedBlockingDeque<Runnable>());
        for(int i=0;i<5;i++){
            Future<String> future = pool.submit(callable);//利用Future对象接收
            try {
                //通过get(time)方法获取返回值,若指定时间为获取到则返回
                //get()方法会等到任务执行完为止。
                String str= future.get(1000,TimeUnit.MILLISECONDS);
            System.out.println(str);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

class MyCall implements Callable {


    @Override
    public Object call() throws Exception {
        for(int i=0;i<50;i++){
            System.out.println(Thread.currentThread().getName()+"、"+i);
        }
        return Thread.currentThread().getName()+"任务执行完毕";
    }
}

下面是一些常用的静态工厂,生成一些常用的线程池:

1.newSingleThreadExecutor

创建一个单线程的线程池,这个线程池只有一个线程在工作,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

2.newFixedTheadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,知道线程达到线程池的最大大小。线程池的大小一旦达到最大值就保持不变,如果某一个线程因为执行异常结束,那么线程池会补充一个新线程。

3.newCachedThreadPool

创建一个可缓存的线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(1分钟不执行)的线程,当任务书增加是,此线程池又可以智能的添加新线程来处理任务,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统能够创建线程的大小。

4.newScheduledThreadPool

创建一个大小无限的线程池,此线程池支持定时以及周期性的执行任务的需求。

关闭线程池

可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程, 然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的 区别:

shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执 行任务的列表。

shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程

合理配置线程池

首先需从以下方面分析任务:
任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
任务的优先级:高、中和低。
任务的执行时间:长、中和短。
任务的依赖性:是否依赖其他系统资源,如数据库连接。

CPU密集型任务应配置尽可能小的线程,如配置Ncpu+1个线程的线程池。由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*Ncpu。混合型的任 务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太 大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。
注:可以通过 Runtime.getRuntime().availableProcessors() 方法获得当前设备的CPU个数
优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 来处理。它可以让优先级高的任务先执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值