• 扫盲大队-线程池


    一、是什么
    在聊线程池之前,想厘清并发和并行的概念。
    并发(Concurrency)的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序(或线程)之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
    从微观角度来讲:所有的并发处理都有排队等候,唤醒,执行等这样的步骤,在微观上他们都是序列被处理的,如果是同一时刻到达的请求(或线程)也会根据优先级的不同,而先后进入队列排队等候执行。
    从宏观角度来讲:多个几乎同时到达的请求(或线程)在宏观上看就像是同时在被处理。
     
    并行(Parallelism)指两个或两个以上事件(或线程)在同一时刻发生,是真正意义上的不同事件或线程在同一时刻,在不同CPU资源上(多核),同时执行。并行不存在像并发那样竞争,等待的概念。
     
    线程池是在初始化一个多线程应用程序过程中创建一个线程集合,然后在需要执行新的任务时重用这些线程而不是新建一个线程(提高线程复用,减少性能开销)。线程池中线程的数量通常完全取决于可用内存数量和应用程序的需求。线程池中的每个线程都有被分配一个任务,一旦任务已经完成了,线程回到池子中然后等待下一次分配任务。
     
    二、有什么用
    本质上来讲,我们使用线程池主要就是为了减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务;节约应用内存(线程开的越多,消耗的内存也就越大)。
    使用线程池,1、改进一个应用程序的响应时间。由于线程池中的线程已经准备好且等待被分配任务,应用程序可以直接拿来使用而不用新建一个线程。2、节省为每个短生存周期任务创建一个完整的线程的开销并可以在任务完成后回收资源。3、根据当前在系统中运行的进程来优化线程时间片。4、允许我们开启多个任务而不用为每个线程设置属性。5、允许我们为正在执行的任务的程序参数传递一个包含状态信息的对象引用。6、可以用来解决处理一个特定请求最大线程数量限制问题。
     
    三、怎么用
    ThreadPoolExecutor是Java线程池的核心类,通过传入该类的构造函数参数类配置线程池。
     
    ThreadPoolExecutor构造函数的核心参数如下:
    int corePoolSize:线程池核心线程数
    int maximumPoolSize:线程池最大线程数,如果当前任务数量超过这个数量,则多出来的任务放到队列任务中等待处理
    long keepAliveTime:线程池总非核心线程超时时长,线程超时后被销毁
    TimeUnit unit:超时时长单位
    BlockingQueue<Runnable> workQueue:任务队列,存放等待执行的任务对象。当所有核心线程都被占用时,新添加任务会存放到任务队列,如果任务队列满了,新建非核心线程从任务队列中获取任务并执行
    ThreadFactory threadFactory:创建线程工厂类
    RejectedExecutionHandler handler:拒绝处理任务时的策略。
     
    ThreadPoolExecutor的核心方法:
    execute():ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。
    submit():用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。
    shutdown():不再接受新任务,当前正在执行任务完成后关闭线程池
    shutdownNow():不再接受新任务,毁天灭地直接立刻马上关闭线程池的
    setCorePoolSize:设置核心池大小
    setMaximumPoolSize:设置线程池最大能创建的线程数目大小
    getQueue() 、getPoolSize() 、getActiveCount()、getCompletedTaskCount()等获取与线程池相关属性的方法
     
    拒绝处理任务策略:
    ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
    ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
    ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
    ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
     
    线程池任务执行策略:
    如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
    如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
    如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
    如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
     
    任务队列workQueue类型:
    SynchronousQueue:队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果没有闲置线程,则新建一个线程来处理这个任务,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大。
    LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize。
    ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误。
    DelayQueue:队列内元素必须实现Delayed接口,传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。
     
    常用的线程池:
    1、newCachedThreadPool
    工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
    需要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。
     
    2、newFixedThreadPool
    创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
    FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
     
    3、newSingleThreadExecutor
    创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
     
    4、newScheduleThreadPool
    创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
     
    四、深入研究方向
    1、Future
     
    五、面试点
    1、使用线程池的风险
    死锁
    当一组进程或线程中的每一个都在等待一个只有该组中另一个进程才能引起的事件时,这组进程或线程死锁。死锁的最简单情形是:线程 A 持有对象 X 的独占锁,并且在等待对象 Y 的锁,而线程 B 持有对象 Y 的独占锁,却在等待对象 X 的锁。除非有某种方法来打破对锁的等待(Java 锁定不支持这种方法),否则死锁的线程将永远等下去。
    线程池还引入了另一种死锁可能,所有池线程都在执行已阻塞的等待队列中另一任务的执行结果的任务,但这一任务却因为没有未被占用的线程而不能运行。当线程池被用来实现涉及许多交互对象的模拟,被模拟的对象可以相互发送查询,这些查询接下来作为排队的任务执行,查询对象又同步等待着响应时,会发生这种情况。
     
    资源不足
    线程消耗包括内存和其它系统资源在内的大量资源。除了 Thread 对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。除此以外,JVM 可能会为每个 Java 线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后,虽然线程之间切换的调度开销很小,但如果有很多线程,环境切换也可能严重地影响程序的性能。
    如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。
     
    并发错误
    线程池和其它排队机制依靠使用 wait() 和 notify() 方法,如果编码不正确,那么可能丢失通知,导致线程保持空闲状态,尽管队列中有工作要处理。
     
    线程泄漏
    各种类型的线程池中一个严重的风险是线程泄漏,当从池中除去一个线程以执行一项任务,而在任务完成后该线程却没有返回池时,会发生这种情况。发生线程泄漏的一种情形出现在任务抛出一个 RuntimeException 或一个 Error 时。如果池类没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减少一个。当这种情况发生的次数足够多时,线程池最终就为空,而且系统将停止,因为没有可用的线程来处理任务。
     
    针对线程池可能出现的风险,可以用一些准则进行有效规避:不要对那些同步等待其它任务结果的任务排队来规避死锁;谨慎为时间可能很长的操作使用合用的线程;根据实际情况合理设置线程池的各个参数配置。
     
    2、线程池的大小设置
    如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
    如果是IO密集型任务,参考值可以设置为2*NCPU
    最终还是需要根据实际情况,针对系统负载、资源利用率等指标来进行调整。
     
    3、Java线程池获取任务时的精妙处理
    假如我们来设计线程池,可能会有一个任务分派线程,当发现有线程空闲时,就从任务缓存队列中取一个任务交给空闲线程执行。但在ThreadPoolExecutor并没有采用这样的方式,因为这样会要额外地对任务分派线程进行管理,无形地会增加难度和复杂度,这里直接让执行完任务的线程去任务缓存队列里面取任务来执行。

  • 相关阅读:
    Struts2结合Ajax实现登录
    Java读取Properties文件
    职责链模式
    javaScript初学者易错点
    2019 DevOps 必备面试题——DevOps 理念篇
    如何成为一名优秀的敏捷团队负责人
    为什么企业敏捷团队会失败
    伪装的敏捷,我好累
    CODING 告诉你如何建立一个 Scrum 团队
    十倍程序员的传说
  • 原文地址:https://www.cnblogs.com/qhj348770376/p/9419092.html
Copyright © 2020-2023  润新知