计算机系统应用教程网站

网站首页 > 技术文章 正文

Java基础——Java多线程(什么是线程池?)

btikc 2024-10-22 10:32:29 技术文章 14 ℃ 0 评论

1 基本概括

2 主要介绍

2.1 线程池的概念

线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。

线程池内部结构 :

1.线程池管理器:负责线程创建、销毁、添加任务等;

2.工作线程: 线程池创建的正在工作的线程;

3.任务队列( BlockingQueue ):线程满了之后,可以放到任务队列中,起到一定的缓冲;

4.任务:要求实现统一的接口,方便处理和执行;

2.2 线程池的优点

  1. 可以将任务的提交和执行策略解耦,便于统一管理任务执行策略,好维护,比如延时执行,设置等 待时间,超时自动失败等。

2.提高性能,用已创建的线程执行任务,减少创建和销毁线程的开销。

3.约束最大线程并发数,防止无止境创建线程造成性能变差以及程序死掉。

4.活跃线程数、最大线程数等参数可配置,方便进行性能调优。

2.3 线程池的状态

线程池的5种状态:Running、ShutDown、Stop、Tidying、Terminated。

线程池各个状态切换框架图:

1.RUNNING

状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。

状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!

2.SHUTDOWN

状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。

状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。

3.STOP

状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。

状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。

4.TIDYING

状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。

状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。

当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。

5.TERMINATED

状态说明:线程池彻底终止,就变成TERMINATED状态。

状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。

2.4 线程池的使用场景

使用的场景:

1、需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

2、对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

3、接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,并出现"OutOfMemory"的错误。

不使用线程池的场景:

1、如果需要使一个任务具有特定优先级

2、如果具有可能会长时间运行(并因此阻塞其他任务)的任务

3、如果需要将线程放置到单线程单元中(线程池中的线程均处于多线程单元中)

2.5 线程池的使用步骤

2.6 ThreadPoolExecutor类

2.4.1 参数说明

corePoolSize:表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到 corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了 
corePoolSize,则不再重新创建线程。如果调用了prestartCoreThread()或者 prestartAllCoreThreads(),线程池创建的时候所有的核心线程都会被创建并且启动。
maximumPoolSize:表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过 maximumPoolSize 的话,就会创建新的线程来执行任务。
keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了 
    corePoolSize,并且线程空闲时间超过了 
    keepAliveTime 的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。
    unit:时间单位。为 keepAliveTime 指定时间单位。workQueue:阻塞队列。用于保存任务的阻塞队列,关于阻塞队列可以看这篇文章。可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。
    threadFactory:创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。
    handler:饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种:
AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;CallerRunsPolicy:只用调用者所在的线程来执行任务;DiscardPolicy:不处理直接丢弃掉任务;
DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务


2.4.2 工作队列workQueue

该线程池中的任务队列:维护着等待执行的Runnable对象

当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

常用的workQueue类型:

  1. SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现<线程数达到了maximumPoolSize而不能新建线程>的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大
  2. LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize
  3. ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误
  4. DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

2.4.3 ThreadPoolExecutor的策略

当一个任务被添加进线程池时:

  1. 线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
  2. 线程数量达到了corePools,则将任务移入队列等待
  3. 队列已满,新建线程(非核心线程)执行任务
  4. 队列已满,总线程数又达到了maximumPoolSize,就会由 RejectedExecutionHandler抛出异常

2.4.4 可创建的线程池

1.可缓存线程池: CachedThreadPool

a.没有最大线程数限制,想创建多少就创建多少

b.有空闲线程就用空闲线程,没有就创建新线程

c.复用空闲线程能减少一部分重复创建销毁的开销

2. 定长线程池:FixedThreadPool

a.用来控制线程最大并发数。

b.如果没有空闲线程,剩下的任务会在队列等待有空闲线程执行。

3.支持定时和周期反复执行的定长线程池:ScheduledThreadPool

4.单线程的线程池:SingleThreadExecutor

线程池里只有一个线程,按照队列的出入规则来串行执行任务,例如先进先出。

2.4.5 拒绝策略

一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,这种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况。ThreadPoolExecutor自带的拒绝策略如下:

1、AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作;

2、CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行;

3、DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;

4、DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失;

2.4.6 关键方法分析

1 private boolean addWorker(Runnable firstTask, boolean core)

此方法用于新增线程池的Worker对象。由于一些条件的限制,此方法并不总是能执行成功。具体的执行情况如下:

1 如果当前线程池处于STOP、TIDYING和TERMINATED中的任一状态,不执行新增Worker操作,返回false;

2 以下描述的是线程池处于SHUTDOWN状态的情况:

参数firstTask不为null,也就是说有新任务的提交,不执行新增Worker操作,返回false;

参数firstTask为null,并且当前任务队列为空,不执行新增Worker操作,返回false;

3 以下描述的是线程池处于RUNNING状态的情况:

线程池中Worker的数量已到达能够容纳的最大值CAPACITY,不执行新增Worker操作,返回false;

准备新增的为核心Worker,并且线程池中Worker的数量已到达最大值corePoolSize,不执行新增Worker操作,返回false;

准备新增的为非核心Worker,并且线程池中Worker的数量已到达最大值 maximumPoolSize,不执行新增Worker操作,返回false。

成功将新建的Worker放入Worker集合后,会启动Worker对象内部的线程,此方法执行是否成功以这个线程是否启动为准,若线程启动的标志为false,则说明新增Worker的操作失败,要做新增Worker失败处理。

addWorkerFailed方法用于处理新增Worker失败的情况,会执行下面三步操作:

新增Worker逻辑已经创建了一个Worker对象,那么此方法就要负责从Worker集合中移除这个Worke 对象;

将线程池Worker集合大小减1,通过CAS进行操作,自旋直到减成功;

调用tryTerminate方法,尝试停止线程池,能否停止成功以tryTerminate的执行为准(尝试性的停止)。

2 private boolean addWorker(Runnable firstTask, boolean core)

getTask是个比较重要的方法,它负责从任务队列中获取任务。

getTask方法有两种返回数据:

1 null

2 返回null,则预示着执行此方法的Worker将会被从Worker集合中移除;

3 队列任务

getTask() == null

出现getTask() == null的情况如下:

  1. 线程池中Worker的总数大于maximumPoolSize;
  2. 线程池已停止(STOP、TIDYIING和TERMINATEDR任意一种);
  3. 线程池已关闭(SHUTDOWN),并且任务队列为空;
  4. 线程池Worker的总数已经超过corePoolSize或者allowCoreThreadTimeOut设置为true,上一次获取任务已经超时,当前的Worker不是最后一个或者是最后一个Worker并且任务队列为空。

3 public void shutdown()

外部通过调用shutdown方法来关闭线程池,此方法执行的步骤为:

第一步:检查是否有关闭权限; 第二步:将线程池状态转移为SHUTDOWN(循环通过CAS更新,直至成功);

第三步:中断空闲的Worker; 第四步:调用onShutdown方法; 第五步:调用tryTerminate尝试终止线程池。

4 public List shutdownNow()

外部通过调用shutdownNow方法来立即关闭线程池,此方法执行的步骤为:

第一步:检查是否有关闭权限;

第二步:将线程池状态转移为STOP(循环通过CAS更新,直至成功);

第三步:中断所有的Worker;

第四步:将任务队列中的任务转移出来;

第五步:调用tryTerminate尝试终止线程池。

第六步:返回转移出来的任务列表。

3 简单用例

3.1 ThreadFactory对线程池中创建的线程进行记录与命名

public class ThreadPool {
    private static ExecutorService pool;
    public static void main( String[] args )
    {
        //自定义线程工厂
        pool = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5),
                new ThreadFactory() {
            public Thread newThread(Runnable r) {
                System.out.println("线程"+r.hashCode()+"创建");
                //线程命名
                Thread th = new Thread(r,"threadPool"+r.hashCode());
                return th;
            }
        }, new ThreadPoolExecutor.CallerRunsPolicy());
          
        for(int i=0;i<10;i++) {
            pool.execute(new ThreadTask());
        }    
    }
}

public class ThreadTask implements Runnable{    
    public void run() {
        //输出执行线程的名称
        System.out.println("ThreadName:"+Thread.currentThread().getName());
    }
}


3.2 创建可缓存线程池CachedThreadPool

//开始下载
private void startDownload(final ProgressBar progressBar, final int i) {
        mCachedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                int p = 0;
                progressBar.setMax(10);//每个下载任务10秒
                while (p < 10) {
                    p++;
                    progressBar.setProgress(p);
                    Bundle bundle = new Bundle();
                    Message message = new Message();
                    bundle.putInt("p", p);
                    //把当前线程的名字用handler让textview显示出来
                    bundle.putString("ThreadName", Thread.currentThread().getName());
                    message.what = i;
                    message.setData(bundle);
                    mHandler.sendMessage(message);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
     });
 }


4 线程会常见的问题


1 使用线程池比手动创建线程好在哪里?
2 线程池的各参数的含义?
3 线程池有哪 4 种拒绝策略?
4 有哪 6 种常见的线程池?什么是 Java8 的 ForkJoinPool?
5 线程池常用的阻塞队列有哪些?
6 为什么不应该自动创建线程池?
7 合适的线程数量是多少?CPU 核心数和线程数的关系?
8 如何根据实际需要,定制自己的线程池?
9 如何正确关闭线程池?shutdown 和 shutdownNow 的区别?
10  线程池实现“线程复用”的原理?

常见出现的问题会在后面的文章讨论,想一起讨论学习的朋友可以点下关注,会持续更新,文章有帮助的话可以收藏转发,有什么补充可以在下面评论,谢谢!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表