JAVA 线程池

一、ThreadPoolExecutor的使用

1.ThreadPoolExecutor的完整构造方法是:

ThreadPoolExecutor ( int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue< Runnable >  workQueue, RejectedExecutionHandler hander)

2.Executors中的几个工厂方法

ThreadPoolExecutor是Executors类的底层实现

①newFixedThreadPool ( int nThreads)——固定大小线程池

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

可以看到corePoolSize和maximunPoolSize的大小是一样的(其实使用无界Queue的话maximumPoolSize参数是没有意义的),keepAliveTime和unit的设置表明该实现不想keepAlive,最后的BlockingQueue选择了LinkedBlockingQueue,该Queue有一个特点就是它是无界的;

②newSingleThreadExecutor()——单线程

public static ExecutorService newSingleThreadExecutor() {
        return new ThreadPoolExecutor(1, 1,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

③newCachedThreadPool()——无界线程池,可以进行自动线程回收

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

在BlockingQueue的选择上使用SynchronousQueue,在该Queue中,每个插入操作必须等待另一个线程的对应移除操作。比如,我先添加一个元素,接下来如果想继续添加则会阻塞,直到另一个线程取走一个元素,反之亦然(也就是缓冲区为1的生产者消费者模式)。

3.corePoolSize、workQueue、maximumPoolSize的优先级

当一个任务通过execute(Runnable)方法欲添加到线程池时: 
如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。 

如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。 

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。 

如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。

也就是:处理任务的优先级为: 

核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。 

当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

二、BlockingQueue<Runnable> workQueue

1.BlockingQueue的作用

所有的BlockingQueue都可以用于传输和保持提交的任务,可以使用此队列对池大小进行交互。

如果运行的线程小于corePoolSize,则Executor始终首选添加新的线程,而不进行排队,也就是当当前运行的线程小于corePoolSize时,则任务根本不会存放,添加到Queue中,而是直接创建一个新的thread;

如果运行的线程等于或多余corePoolSize,则Executor首选将请求加入Queue,而不是添加新的thread;

如果无法将请求加入Queue,则创建新线程,除非创建此线程超出maximumPoolSize,在这种情况下,任务将被拒绝。

2.排队的三种策略

①直接提交,工作队列的默认选项是SynchronousQueue,它将任务交给线程而不保持它们,如果不存在可用于立即执行的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界maximumPoolSize以避免拒绝新提交的任务,当命令以超过队列所能处理的平均连续到达时,此策略允许无界线程具有增长的可能性。

②无界队列,使用无界队列如LinkedBlockingQueue将导致在所有corePoolSize线程都忙时新任务在队列中等待,这样创建的线程数就不会超过corePoolSize,因此maximumPoolSize的值也就无效了。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列。

③有界队列,当使用有限的maximumPoolSize时,有界队列有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。  

3.几个例子

①直接提交策略SynchronousQueue,由于该Queue本身的特性,在某次添加元素后必须等待其他线程取走后才能继续添加。

new ThreadPoolExecutor(2, 3, 30, TimeUnit.SECONDS, 
			new SynchronousQueue<Runnable>());

当核心线程已经有2个正在运行.

--此时继续来了一个任务(A),根据前面介绍的“如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。”,所以A被添加到queue中。
--又来了一个任务(B),且核心2个线程还没有忙完,OK,接下来首先尝试1中描述,但是由于使用的SynchronousQueue,所以一定无法加入进去。
--此时便满足了上面提到的“如果无法将请求加入队列,则创建新的线程,除非创建此线程超出maximumPoolSize,在这种情况下,任务将被拒绝。”,所以必然会新建一个线程来运行这个任务。
--暂时还可以,但是如果这三个任务都还没完成,连续来了两个任务,第一个添加入queue中,后一个呢?queue中无法插入,而线程数达到了maximumPoolSize,所以只好执行异常策略了。
所以在使用SynchronousQueue通常要求maximumPoolSize是无界的,这样就可以避免上述情况发生(如果希望限制就直接使用有界队列)。对于使用SynchronousQueue的作用jdk中写的很清楚:此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。
什么意思?如果你的任务A1,A2有内部关联,A1需要先运行,那么先提交A1,再提交A2,当使用SynchronousQueue我们可以保证,A1必定先被执行,在A1么有被执行前,A2不可能添加入queue中。

②使用无界队列策略,即LinkedBlockingQueue

拿newFixedThreadPool来说,如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。

--如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。

--如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。

--OK,此时任务变加入队列之中了,那什么时候才会添加新线程呢?如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

这里就很有意思了,可能会出现无法加入队列吗?不像SynchronousQueue那样有其自身的特点,对于无界队列来说,总是可以加入的(资源耗尽,当然另当别论)。换句说,永远也不会触发产生新的线程!corePoolSize大小的线程数会一直运行,忙完当前的,就从队列中拿任务开始运行。所以要防止任务疯长,比如任务运行的实行比较长,而添加任务的速度远远超过处理任务的时间,而且还不断增加,如果任务内存大一些,不一会儿就爆了,呵呵。

③有界队列,即ArrayBlockingQueue

new ThreadPoolExecutor(
             2, 4, 30, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2));
假设,所有的任务都永远无法执行完。

对于首先来的A,B来说直接运行,接下来,如果来了C,D,他们会被放到queu中,如果接下来再来E,F,则增加线程运行E,F。但是如果再来任务,队列无法再接受了,线程数也到达最大的限制了,所以就会使用拒绝策略来处理。

三、用途和用法

为什么要用线程池:

减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务;
可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)

网络请求通常有两种形式:第一种,请求不是很频繁,而且每次连接后会保持相当一段时间来读数据或者写数据,最后断开,如文件下载,网络流媒体等。另一种形式是请求频繁,但是连接上以后读/写很少量的数据就断开连接。考虑到服务的并发问题,如果每个请求来到以后服务都为它启动一个线程,那么这对服务的资源可能会造成很大的浪费,特别是第二种情况。因为通常情况下,创建线程是需要一定的耗时的,设这个时间为T1,而连接后读/写服务的时间为T2,当T1>>T2时,我们就应当考虑一种策略或者机制来控制,使得服务对于第二种请求方式也能在较低的功耗下完成。

通常,我们可以用线程池来解决这个问题,首先,在服务启动的时候,我们可以启动好几个线程,并用一个容器(如线程池)来管理这些线程。当请求到来时,可以从池中去一个线程出来,执行任务(通常是对请求的响应),当任务结束后,再将这个线程放入池中备用;如果请求到来而池中没有空闲的线程,该请求需要排队等候。最后,当服务关闭时销毁该池即可。


参考文章:

http://dongxuan.iteye.com/blog/901689

http://blog.csdn.net/v1v1wang/article/details/5607279

http://sunnylocus.iteye.com/blog/223327




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