java多线程学习之线程池赏析

1了解概念:什么是线程池

Thread pool线程池,顾名思义就是由若干个线程“Thread类型对象”所组成的一个"池",但是切勿只认为Thread pool(线程池)内部就仅仅只有Thread类型对象(线程),这样理解是不够准确的。因为Thread pool内还有"任务队列",任务队列:Queue  tasksWorkQueue它是一个可以存放Runnable类型实例化对象的一个Queue(队列性集合)(注意:Queue这种数据结构具有"先进先出FIFO"的特性,即所谓的排队性),进入了Queue tasksWorkQueue中的task(任务:Runnable类型实例),会被java线程池自带的调度算法,按照FIFO(先进先出)的原则排队等待,当该Runnable实例(任务)出队列后:意味着由thread pool(java线程池)为该Runnable型实例(任务)分派(调度)到了可利用的闲置的有档期的thread(线程池内的当前可调度使用的一个Thread类型对象,且该Thread类型对象没有被其他的Runnable类型实例嫁接寄生到它上),并让该Runnable类型对象(任务)嫁接性寄生到该被调度到的(空闲)(有档期)闲置的Thread类型实例上,使得该task(任务:Runnable类型实例)的run()方法得以有机会被运行!(Runnable类型对象必须寄生到Thread类型对象上才能得以运行!)(注意:当该Thread 类型对象运行完了该任务(Runnable类型对象)的run()方法后,该Thread类型对象又会变成(重置成闲置的状态)闲置的,有档期的thread并返回到线程池thread pool中重新供线程池调度使用,并具有了可能再接纳新的任务(Runnable类型实例)的能力。(权威)

—————————————————————— 

2线程池的内部构造

一个线程池ThreadPool类型对象,都会用其内部的两个集合组件:一个是Thread[] threads(存放线程的地方)(线程池,线程集合,线程数组),一个是Queue workQueue(存放任务的地方)(任务队列)+任务(Runnable类型对象)(workQueue中的Runnable类型对象(元素))与线程(Thread类型对象)(threads[i],threads中的Thread类型对象)之间的绑定(寄生关系,嫁接关系),及其对二者之间的调度关系的算法程序! 

——————————————————————

3线程池的工作理念

线程池中的线程(Thread类型实例)可以运行执行工作队列(任务队列)(Queue)中的任务(Runnable型实例),且仅仅有线程池中具有闲置状态的线程(thread)才资格和能力(被线程池的自定义算法调度)可能接到在Queue工作队列中排队等待的第一名Runnable类型实例(任务)(这时该第一名任务出任务队列).当该thread运行完毕了交给它(分派给它)的那个Runnable类型实例任务后,该thread会再次被重置成"有档期的闲置状态",并重返线程池,供线程池再次随机调度使用,以用来接纳新的第一名任务(原第一名任务已经出了工作队列,执行完毕后就没了,消失了,被内存释放了!) 

——————————————————————

4使用线程池的好处 

在使用线程池之前我们必须要关注到thread的创建,控制,与thread之间的通信问题.使用了thread pool(线程池)之后,程序员只要专注实现要执行的任务(Runable类型实现类的实现)的构造即可(专心实现核心业务关注点程序).使用Executor框架,能让程序员把精力和焦点放在多线程程序要实现的业务逻辑功能上而不是写出一滩围绕线程的如何创建和管理的(与线程thread本身的工作机制有关的)一系列的非核心业务关注点的代码,让你的心思与精力完全放在核心业务关注点上来.

————————————————————————

5线程池(Thread pool)与吞吐量问题 (测试结果才是说明问题的直接证据)

 thread可能会被Block住的事实意味着我们必须要有比CPU更多的thread在pool里面(这是Thread pool中的thread会被block的原因之一),由此得出的推论:多个Cpu密集的线程处理方案要比一个CPU处理多线程的吞吐量要好,即一个cpu密集的多thread处理可以得到最佳的吞吐量。例如每一个thread有50%的时间被block住,你会希望每个Cpu要处理两个thread,如果每个thread有66%被阻断的时间(执行总时间的66%)换句话说每个thread在完成它的任务所需要的总时间的33%是有效时间,其余为该线程被阻断的时间,那么你会希望每个CPU能够总共处理三个thread,如此类推,以达到最佳的程序性能吞吐量。当然,你不太可能让程序模型设计到如此程度的详细细节,且任何模型一旦计算到随机出现的尖峰流量更是难以估计.最后,你需要进行一些测试来测定thread pool的适当大小.但如果CPU资源缺乏时,调节thread的数目(但还是要保持CPU的利用率)会增加应用程序的吞吐量.

————————————————————————

6什么情况下不要使用线程池:

从程序功能的需求与设计的目的来讲:
如果你的程序是用来进行批处理,或只是提供单一回答或报告,是否用多线程(多个单独分离的thread)或者使用线程池(thread pool)就一点都不重要.因为这种情况下使用线程者并不关心某个thread(线程)是否能比其他的thread(线程)先完成,即没有线程排队的概念要求.注意:这并不代表你可以期待创建数千个thread而不会有事:thread是需要内存的,而使用越多的内存就会对系统性能产生更大的冲击.此外,当操作系统管理数千个thread而不是只有几个thread的时候是需要成本的.另外,如果你的程序设计很漂亮地分离出了多个单独的thread且你只在乎所有这些thread的最终执行结果,而不在乎他们的排队与调度序列的时候,这时候也是不需要使用线程池(thread pool)的.
 

从程序的性能方面和吞吐量方面来讲:
thread pool 同样在可用的CPU资源足够来处理程序所需的所有工作任务时是不必要使用线程池的即这种情况下使用线程池是多此一举,因为线程池的使用目的就是为了处理现有Cpu数目少而thread数目多的情况,增加程序的吞吐量的。事实上,如果你的cpu数目较多且数据处理,计算速度超快,且足以应付多个单独线程(不再一个线程池中的多个独立线程)的处理那么在这种情况下你还画蛇添足的使用线程池是不明智地,这可能反而会影响系统的性能.举个例子:如果你的系统有八个CPU且你的线程池(thread pool)中只有四个thread(这样的情况下CPU们肯定吃不饱),task(任务,runnable类型实例化对象)甚至出现了在还有四个CPU闲置的情况下,该任务(task:Runnable类型实例)还要等待线程池提供(调度)有可以让该任务(Runnable类型实例)寄生到的thread(Thread类型对象,线程)被调度,这样使用线程池(thread pool)就太浪费CPU了,也达不到程序的处理任务的吞吐量的需求了.所以使用线程池(thread pool)的时候你需要调节线程池内thread的数量(Thread类型对象的数量)总数以让这些thread们不会淹没掉系统(拖累系统,过多过少都不好,过多则吃内存,过少则CPU吃不饱即线程池内没有足够数量的Thread类型对象被CPU调度来接手至所需要运行的任务(runnable类型实例)上),但是不管何种情况下你总不会希望你的thread数量比CPU数目还少吧,这样CPU会被饿死(闲置,浪费了计算能力与系统资源)(权威) 

——————————————————————

7如何使用线程池 

你要想使用Thread pool你就必须得做两件事:

第一:创建用于存放Runnable类型实例(任务)的任务队列Queue workQueue 它通常用BlockingQueue workQueue来实现!

第二:创建pool本身:即一个用于存放Thread类型实例(线程)的一个集合 Collection pool.
__________________________________________________________________________________________________
线程池在JAVA语言中的一个成熟的封装实现类是java.util.concurrent.ThreadPoolExecutor这个类,它实现了ExecutorService这个接口(是个线程池理念中的成熟接口),它是ExecutorService这个接口的一个成熟的实现类.

定义如下:

package java.util.concurrent;

public class ThreadPoolExecutor implements ExecutorService {

    public ThreadPoolExecutor(int corePoolSize,

                              int maximumPoolSize,

                              long keepAliveTime,

                              TimeUnit unit,

                              BlockingQueue workQueue);

    public ThreadPoolExecutor(int corePoolSize,

                              int maximumPoolSize,

                              long keepAliveTime,

                              TimeUnit unit,

                              BlockingQueue workQueue,

                              ThreadFactory threadFactory);

    public ThreadPoolExecutor(int corePoolSize,

                              int maximumPoolSize,

                              long keepAliveTime,

                              TimeUnit unit,

                              BlockingQueue workQueue,

                              RejectedExecutionHandler handler);

    public ThreadPoolExecutor(int corePoolSize,

                              int maximumPoolSize,

                              long keepAliveTime,

                              TimeUnit unit,

                              BlockingQueue workQueue,

                              ThreadFactory threadFactory,

                              RejectedExecutionHandler handler);


public boolean prestartCoreThread( );

    public int prestartAllCoreThreads( );

    public void setMaximumPoolSize(int maximumPoolSize);

    public int getMaximumPoolSize( );

    public void setCorePoolSize(int corePoolSize);

    public int getCorePoolSize( );

    public int getPoolSize( );

    public int getLargestPoolSize( );

    public int getActiveCount( );

    public BlockingQueue getQueue( );

    public long getTaskCount( );

    public long getCompletedTaskCount( );


}


这个类的使用说明:
corePoolSize:指定线程池thread pool中 最精瑞线程(thread)数量,最核心的线程数量,应保持最少的线程(Thread类型实例对象)数量,核心处理业务线程数(应必须(满足)达到的最基本的水平)

maximumPoolSize:指定线程池thread pool中 可保持的最多线程(thread)数量,最大线程数量,应保持最满时的线程(Thread类型实例对象)数量(最大极限)

以上的这两个参数会使得线程池中现有的Thread类型实例化对象的数目会在[corePoolSize maximumPoolSize]区间内变化.


若在ThreadPoolExecutor 的构造函数中指定corePoolSize的取值和maximumPoolSize的取值相同,则该线程池(这里指ThreadPoolExecutor 类型实例对象)的pool中的线程数量始终会保持不变,且就是核心处理业务线程数!

若如果对corePoolSize的取值和maximumPoolSize的取值不相同,那么线程池(这里指ThreadPoolExecutor 类型实例对象)会动态调整期内部pool中的线程(Thread类型实例)的数量,使得该数量值落在corePoolSize的取值和maximumPoolSize的取值的区间之内.注意:线程池中的目前(当前)的线程数量用ThreadPoolExecutor 类型实例对象的getPoolSize()这个方法可以返回得到!

关于BlockingQueue workQueue,它是用来存放正在等待需要寄生到线程池中的线程(thread)上的Runnable类型实例(任务)的一个数据结构(队列),因此它workQueue叫做“任务队列”或“工作队列”注意:一旦一个工作队列BlockingQueue workQueue被一个线程池所使用,(传给了一个线程池(本例中是一个ThreadPoolExecutor 类型实例对象))那么这个 BlockingQueue workQueue就应该完全由线程池自行进行管理,你不要手动的管理它,应该让它完全托管!(无人值守地由线程池自动管理),你不要企图自己手动地显式调用有关处理该workQueue的workQueue自己的任何成员方法,即不要直接发送消息给workQueue,特别是往里面添加数据元素,若你要往这个任务队列中添加任务(Runnable类型实例)你也只能(官方推荐)使用线程池(ThreadPoolExecutor实例)的execute(Runnable xxx);方法来添加任务!(这样使用不犯毛病),切记你千万不要直接地运行(显式调用)workQueue自己的method,不然使用了该workQueue的线程池(ThreadPoolExecutor实例)内部的调度运作会放生混乱!
___________________________________________________________________________________________________________________________________________________
那么上文提到的workQueue(存放任务Runnable实例的地方)和线程池(这里指ThreadPoolExecutor类型实例)中的pool(存放thread的地方)是如何交互的呢?pool中的thread又是怎样接到分派给它的Runnable类型实例(任务)的呢?下文回答这两个问题!

首先你要明确线程池(ThreadPoolExecutor实例)的构造是用指定的corePoolSize=min与 maximumPoolSize=max这两个构造参数构造出来的,min指定了pool中最少要保持的线程thread的数量,max指定了pool中最多应保持的thread数量,当一个Runnable类型实例(任务)要交由线程池去处理(希望委托给线程池中pool区域内的thread(Thread类型对象)去处理)时(通过ThreadPoolExecutor实例的execute(Runnable xxx)方法来实现这一步骤),会发生以下五种情况中的一种。


第一:
如果pool中目前所创建的线程数量

第二:如果pool中目前所创建的线程数量为n且min


第三:如果pool中目前所创建的线程数量为n且min

第四:如果pool中目前所创建的线程数量为n且min

第五:如果pool中目前所创建的线程数量为n且min


总结:
当你在线程池机制中,你设置的任务队列(工作队列)BlockingQueue workQueue的容量大小为0时,这意味着相当于没有任务队列(工作队列)这么个东西,或者说这种设定下任务队列这个东西起不到任何作用了,它就是个摆设了,没有意义了,那么就会出现没有"等待中的任务排队"这个"排队"的现象,这种形势下:对于要分配给线程池处理的任务(Runnable类型实例)而言,它要么会被线程池立即接受并运行(执行任务)(即当pool中有闲置的空闲thread时会立即分配给该新到来的任务,并让其寄生执行),要么该新到来的任务(Runnable类型实例)会立即遭到线程池的拒绝处理将它抛弃,(这是因为第一:pool中没有闲置的thread可调度且n==max,因为所有的thread都在忙都在执行其他任务没空处理新任务,即pool中所有线程都是忙状态,而且n已经到达了max值,pool不可能再为新到来的Runnable实例创建新的线程thread了,第二:在上述情况下又没有了任务队列这个东西可用(即任务队列名存实亡),那么新到来的暂时不可马上被分配到thread的任务就无地可去了,被逼到了死路,因为没有了任务队列它就不能够排队等待了),因此这个任务Runnable实例就没有地方生存了,所以它只能自生自灭!(如果你为了能使Runnable类型实例任务无条件得以运行(不被拒绝),那么你就会想到企图将pool中的线程数量的最大极限值不加设定即max值趋于无穷大,这种情况下,你为了防止任务被拒绝新建的Thread类型对象数量会不断攀升以致爆膨,由于创建大量的线程使得系统维护它们的开销巨大,这就使得本应有的线程池性能特性之一(控制线程的无节制创建从而节省系统资源)这一特征荡然无存!),这就跟你起初想使用线程池帮助你优化系统的理念背道而驰了。

其次:当你设置的任务队列(工作队列)BlockingQueue workQueue的容量大小为不加限制的时候(容量无极限),那么有会出现什么现象呢?
答案:当任务队列的容量不加限制,会导致Runnable类型对象加入到workQueue中一定会成功,使得处理不过来的任务放到任务队列中去排队一定会成功,当待处理的任务爆膨的时候(这里指当pool中线程全忙没有空闲的闲置thread时),这些处理不过来的任务会排队(线程池中pool的线程全忙时新到来的没有被分配到线程的Runnable类型实例就会排队即被放到等待队列当中,当新的任务(Runnable型实例)越多时,排的队就越长,(要放到workQueue中的任务就要越多),这代表了pool永远不会创建比min还多的线程(因为处理不过来的任务会自动地排队,你就排去吧,没人搭理你,队伍排的长长的,而且pool不会单独创建新的线程去处理那些排不上队的Runnable类型任务,因为这种情况下根本不存在排不上队的任务了.篇外话:线程池的设计中pool只会为排不上队即放不进workQueue中去的Runnable类型实例任务创建新的线程Thread,以使得n值向max值趋近!这里的n指的是pool中的当前线程总数,max指的是初始化线程池时指定的线程池pool中限定的最多(大)线程总数,最大容量)(换句话说在workQueue的容量不做限定的这种情况下根本不存在排不了队的任务,即任务若要被放进任务队列中则一定会成功,因为任务队列容量无上限有的是地方,队伍排得越长,后续的排队的任务就越难被执行,因为前面排队的人实在太多了(且后排在后面的任务其优先被执行权不够(就越靠后)(这是由队列的FIFO的特性所决定的),就像排队搓澡一样,前面排的人越多,后面要搓澡的人等的时间就得越长)).


__________________________________________________________________________________________________________________________________

经验之谈:使用线程池的最佳策略:如下

使用线程池时,在初始化阶段要设定BlockingQueue workQueue(任务排队队列)的容量大小使其为一个有限的固定值,假设此值为P(即工作队列的最大容量值,即workQueue中所能够存放Runnable类型对象的最多数目要设为一个固定值p),其次应让pool中的总线程数量设定在一个闭区间内,即core(核心线程数量)或者说是应保持的最小线程数量min,与最大线程数量(pool中最多允许保存的thread数量)max,使得min<=n<=max的条件始终成立即可.这种情况下当有任务(Runnable类型实例)要委托各线程池的pool来调度thread来处理它时,pool首先会选择的算法策略是不断创建新的thread直到pool中当前有的线程总数n==min值为止,此时若还有到来的新的未处理的Runnable类型任务时,这些未得到处理的任务就要选择进人workQueue(任务队列),到任务队列中去排队,直到在任务队列中排队的任务(Runnable类型对象)的数量达到了p值为止(workQueue容量,超过这个值后,新来的Runnable类型任务就不能够再入队列了,因为workQueue已经满了,放不下去了),若继续还有新到来的Runnable类型对象(任务)到来,那么后来的这些排不了队的任务(放进workQueue中的任务)就会仍然被pool所接受,只不过pool要为这些排不了队的额外任务,再创建一批新的thread(Thread类型对象)(即新的线程),来处理接受这些新的额外的任务,直到n==max为止,pool不可能在创建新的thread了.(pool已到达能够创建的线程数量上限),若在n==max的情况下,还有新的更多的Runnable类型任务需要交付给线程池来处理时,线程池将拒绝处理这些过分多余的任务,使得这些最后多出来处理不了的任务被丢弃.让其自生自灭.
注意:使用固定容量值的workQueue+固定容量的pool这一策略的线程池的好处是:如果要执行的任务数比可分配与可调度的闲置thread数目要大的时候(即任务到来比处理的速度要快时),“过多的处理不过来的任务”就要进workQueue排队等待(按照fifo原则排队等待pool调度稍后有可闲置状态的thread去接受并处理它),当pool中的线程数量已到达上限max,且workQueue中的Runnable类型任务数量已到达p时,且pool中再无闲置thread(所有的thread全忙)时,那么继续到来的任务就能被拒绝处理,被抛弃,这样可以减少程序的负担,即减少线程池的可处理负担,提高了多线程程序的吞吐量。切记:没有魔术(梦之规则)(黄金规则)规则可以用来决定最佳容量大小的workQueue与pool容量的规则取值,可以遵循的道理与原则是:使用与CPU数量相同的thread数目,一般不会犯毛病,对于高要求与更复杂的多线程并发程序的设计,挑选pool容量值(线程池中线程数量的设置)的大小要由测试不同的值来确定线程池工作地最佳性能配置值!
_______________________________________________________________________________________________


本章总结:

在这一章中我们先探索讨论executor接口,它将多线程程序设计的细节对开发者隐藏了起来,它可以用来处理Runnable类型的任务,它是处理多Runnable类型对象(任务)的理想工具.
executor接口所提供的模型,能够让你所对于要处理的多个任务,描述并规理成串行化的多个任务的概念.使得你对多任务的串行化处理有了个理想的实现方案与工具.程序员可以只关注程序的逻辑需求,而不用烦恼于关于java线程(thread)如何被创建与使用(调度)的诸多细节.就能写出高效有用的多线程并发程序(或者说多任务处理程序).

java.util.concurrent.ThreadPoolExecutor这个类是对executor接口的良好实现,使用这个工具类可以在实现“线程池”效应(简化多线程程序的设计,加强程序的吞吐量使得多个任务被串行化并发处理,这里的串行化指得是任务等待处理是采用"排队等待的策略“,排队等待处理模式:遵循着FIFO原则排队)的好处外,可用其内的pool这个机制动态调节"池"中的线程数量,从而减少多线程对CPU的竞争,并能够让CPU密集的多线程程序更快地完成个别任务(Runnable类型任务).

java线程池被认为被使用的最重要的原因是:重复利用已经被创建出来的thread要比不断地新创建thread在效率与资源开销上都要好得多!另一个原因是:你会发现其实线程池的好处关键在于:当CPU资源竞争较少的时候(比如说需要并发执行的thread数量较少时),每个需要处理的任务(Runnable类型实例的run()方法)完成的平均时间要比其它方式(非线程池方式这里指的是盲目地创建多个独立的处理任务用的线程的方式)要少!

有效地使用java线程池的关键在于选择正确地pool容量(pool中可持有的thread总数),与workQueue实现所使用的Queue模型(Queue的模型有多种比如说:LinkedBlockingQueue,ArrayBlockingQueue等,还有就是限定容量的Queue与不限定容量的Queue等)你要选用哪种Queue模型完全是要根据你的应用需求,比如说未限定容量的Queue不会拒绝任务的入队列操作,它会使得任务(任务请求)得到累积的效果,而限定了容量的Queue模型当队列已满时,会导致新的更多的待处理任务遭到入队列拒绝,从而间接导致这样的任务遭到线程池拒绝处理并抛弃!要想用好java线程池你的花些功夫,但是它的回报是:简化多线程并发程序的逻辑设计与创建实现,增加了多任务并发处理程序的吞吐量.


——————————————————————————————————————

使用的案例代码如下:

package javathreads.examples.ch10.example1;

import java.util.concurrent.*;

import javathreads.examples.ch10.*;

public class ThreadPoolTest {

    public static void main(String[] args) {

        int nTasks = Integer.parseInt(args[0]);

        long n = Long.parseLong(args[1]);

        int tpSize = Integer.parseInt(args[2]);

        ThreadPoolExecutor tpe = new ThreadPoolExecutor(

            tpSize, tpSize, 50000L, TimeUnit.MILLISECONDS,

            new LinkedBlockingQueue( ));

        Task[] tasks = new Task[nTasks];

        for (int i = 0; i < nTasks; i++) {

            tasks[i] = new Task(n, "Task " + i);

            tpe.execute(tasks[i]);

        }

        tpe.shutdown( );

    }

}


In this example, we're using the tasks to calculate Fibonacci numbers as we do in Chapter 9. Once the pool is constructed, we simply add the tasks to it (using the execute() method). When we're done, we gracefully shut down the pool; the existing tasks run to completion, and then all the existing threads exit. As you can see, using the thread pool is quite simple, but the behavior of the pool can be complex depending on the arguments used to construct it. We'll look into that in the next section.

note that task is instance of Task,The class Task in this  example is one of the implementations for Runnable (Runnable is a interface of java)
________________________________________________________________________________________________________________________________________________
 

你可能感兴趣的:(多线程,java,学习,开发语言)