线程池学习笔记(一)
一、线程池的必要性
- 每个线程都需要一定的栈内存空间。在最近的64位JVM中,默认的栈大小是1024KB。如果服务器收到大量请求,或者handleRequest方法执行很慢,服务器可能因为创建了大量线程而崩溃。例如有1000个并行的请求,创建出来的1000个线程需要使用1GB的JVM内存作为线程栈空间。另外,每个线程代码执行过程中创建的对象,还可能会在堆上创建对象。这样的情况恶化下去,将会超出JVM堆内存,并产生大量的垃圾回收操作,最终引发内存溢出(OutOfMemoryErrors)
-
这些线程不仅仅会消耗内存,它们还会使用其他有限的资源,例如文件句柄、数据库连接等。不可控的创建线程,还可能引发其他类型的错误和崩溃。因此,避免资源耗尽的一个重要方式,就是避免不可控的数据结构。
二、配置线程池的好处
-
服务器应用程序中经常出现的情况是:单个任务处理的时间很短而请求的数目却是巨大的。如果不使用线程池就会一个请求创建一个线程。为每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源要比花在处理实际的用户请求的时间和资源更多。除了创建和销毁线程的开销之外,活动的线程也消耗系统资源。在一个 JVM 里创建太多的线程可能会导致系统由于过度消耗内存而用完内存或“切换过度”。
-
线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。
三、线程池的使用
1、为什么要使用线程池
- 减少创建和销毁线程的次数,每隔工作线程都可以被重复利用,可执行多个任务
- 可根据系统的承受能力,调整线程池中工作线程的数量,防止因消耗过多的内存而把服务器累趴
2、线程池的工作原理
- 每次提交任务时,如果线程数还没达到coreSize就创建新线程并绑定该任务。所以第coreSize次提交任务后线程总数必达到coreSize,不会重用之前的空闲线程。
-
线程数达到coreSize后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用take()阻塞地从工作队列里拉活来干。
-
如果队列是个有界队列,又如果线程池里的线程不能及时将任务取走,工作队列可能会满掉,插入任务就会失败,此时线程池就会紧急的再创建新的临时线程来补救。
-
临时线程使用poll(keepAliveTime,timeUnit)来从工作队列拉活,如果时候到了仍然两手空空没拉到活,表明它太闲了,就会被解雇掉。
-
如果core线程数+临时线程数 >maxSize,则不能再创建新的临时线程了,转头执行RejectExecutionHanlder。默认的AbortPolicy抛RejectedExecutionException异常,其他选择包括静默放弃当前任务(Discard),放弃工作队列里最老的任务(DisacardOldest),或由主线程来直接执行(CallerRuns),或你自己发挥想象力写的一个。
3、线程池的类
3.1 newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池
保证所有任务的执行顺序按照任务的提交顺序执行。
-
-
-
SingleThreadExecutor 就像是线程数量为1的FixedThreadPool (指定数量的线程池). 如果希望在 子线程中连续运行 任何流程(长期存活的任务) ,很有用处.例如: 监听进入的Socket 连接的任务.
-
如果向SingleThreadExecutor 提交了多个任务, 那么这些任务将排队, 每个任务都会在下一任务开始前,结束运行. 所有的任务将使用相同的线程.
-
用SingleThreadExecutor 来运行这些线程, 以确保任意时刻在任何线程中都只有唯一的任务在运行.
-
-
实现方式:
public class TestSingleThreadExecutor { public static void main(String[] args) { ExecutorService pool = Executors. newSingleThreadExecutor(); Thread t1 = new MyThread(); Thread t2 = new MyThread(); //将线程放入池中进行执行 pool.execute(t1); 或者 pool.submit(t1); pool.execute(t2); 或者 pool.submit(t2); //关闭线程池 pool.shutdown(); } }
结论:最多只有一个线程在同时执行
Execute()和submit()的区别:
- 接收的参数不一样
- submit有返回值,而execute没有
比如说我有很多个做validation的task,我希望所有的task执行完,然后每个task告诉我它的执行结果,是成功还是失败,如果是失败,原因是什么。然后我就可以把所有
失败的原因综合起来发给调用者。 submit方法最终会调用execute方法来进行操作,只是他提供了一个Future来托管返回值的处理而已
3. submit方便Exception处理
如果你在你的task里会抛出checked或者unchecked exception,而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。比如说,我有很多更新各种数据的task,我希望如果其中一个task失败,其它的task就不需要执行了。那我就需要catch Future.get抛出的异常,然后终止其它task的执行
(参考内容:http://blog.csdn.net/yuzhiboyi/article/details/7775266)
3.2 newFixedThreadPool(固定大小线程池)
(1)创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
(2)FixedPool默认用了一条无界的工作队列 LinkedBlockingQueue(可传整型参数设置队列长度),coreSize的线程做不完的任务不断堆积到无限长的Queue中。如果不想搞一条无限长的Queue,避免任务无限等待显得像假死,同时占用太多内存,可能会把它换成一条有界的ArrayBlockingQueue,那就要同时关注一下这条队列满了之后的场景,选择正确的rejectHanlder。
(3)newFixedThreadPool(int nThreads)源码 newBoundedFixedThreadPool() public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS, s
new LinkedBlockingQueue<Runnable>());
}
所以可以自己new一个ThreadPoolExecutor,来达到自己的参数可控的程度,例如,可以将LinkedBlockingQueue(要 new Node(runnable),无疑会产生更多对象) 换成 ArrayBlockingQueue(每插入一个Runnable就直接放到内部的数组里)
ThreadPoolExecutor源码:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
(方法体里面的具体代码没有贴出来,感兴趣的可以参考http://blog.csdn.net/xieyuooo/article/details/8718741)
}
- LinkedBlockingQueue和ArrayBlockingQueue区别参考: http://super-robin.iteye.com/blog/1997423
-
corePoolSize:核心运行的poolSize,也就是当超过这个范围的时候,就需要将新的Thread放入到等待队列中了;
-
maximumPoolSize:一般你用不到,当大于了这个值就会将Thread由一个丢弃处理机制来处理,但是当你发生:newFixedThreadPool的时候,corePoolSize和maximumPoolSize是一样的,而corePoolSize是先执行的,所以他会先被放入等待队列,而不会执行到下面的丢弃处理中,看了后面的代码你就知道了。
- workQueue:等待队列,当达到corePoolSize的时候,就向该等待队列放入线程信息(默认为一个LinkedBlockingQueue),运行中的队列属性为:workers,为一个HashSet;内部被包装了一层,后面会看到这部分代码。
- keepAliveTime:默认都是0,当线程没有任务处理后,保持多长时间,cachedPoolSize是默认60s,不推荐使用。
- threadFactory:是构造Thread的方法,你可以自己去包装和传递,主要实现newThread方法即可;
- handler:也就是参数maximumPoolSize达到后丢弃处理的方法,java提供了5种丢弃处理的方法,当然你也可以自己弄,主要是要实现接口:RejectedExecutionHandler中的方法:public void rejectedExecution(Runnabler, ThreadPoolExecutor e)
java默认的是使用:AbortPolicy,他的作用是当出现这中情况的时候会抛出一个异常;其余的还包含:
1、CallerRunsPolicy:如果发现线程池还在运行,就直接运行这个线程
2、DiscardOldestPolicy:在线程池的等待队列中,将头取出一个抛弃,然后将当前线程放去。
3、DiscardPolicy:什么也不做
4、AbortPolicy:java默认,抛出一个异常:RejectedExecutionException。
3.3 newCachedThreadPool(无界线程池,可以进行自动线程回收)
-
-
-
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
-
CachedPool则把coreSize设成0,然后选用了一种特殊的Queue -- SynchronousQueue (该QUEUE中,每个插入操作必须等待另一个线程的对应移除操作) ,只要当前没有空闲线程,Queue就会立刻报插入失败,让线程池增加新的临时线程,默认的KeepAliveTime是1分钟,而且maxSize是整型的最大值,也就是说只要有干不完的活,都会无限增增加线程数,直到高峰过去线程数才会回落。
-
coreSize(0)、 maxSize(整形最大值)、rejectHandler、keepAliveTime(60s)、SynchronousQueue(高并发下性能比LinkedBlockingQueue/ArrayBlockingQueue低一大截)
-
-
3.4 newScheduledThreadPool
-
-
- 在定义队列的时候使用的是DelayedWorkQueue (该QUEUE中,每个插入操作必须等待另一个线程的对应移除操作。)
- 在执行时使用ScheduledThreadPoolExecutor . scheduleAtFixedRate(等待时间跟任务是否执行完没关系)
-
ScheduledThreadPoolExecutor .scheduleWithFixedDelay(里面的间隔时间是等任务执行完的等待时间)