关于java中的线程池,我一开始以为就是为了不频繁的建立和销毁线程吧,先建立必定量的线程,而后再进行复用。可是要具体说一下如何作到的,本身又说不出一个一二三来了,这大概就是本身的学习习惯流于表面,不常常深刻的结果吧。因此这里决定系统的学习一下线程池的相关知识。java
本身稍微总结了一下,学习一些新的知识或者技术的时候,大概均可以分为这么几个点:多线程
一、为何会有这项技术,用原来的方法有什么问题。并发
二、这项新技术具体是怎么解决这个问题的(这时可能就要涉及到一些具体的知识点和编码了)框架
三、是否是使用这项技术问题就能够获得完美解决了,有没有什么不一样的方案?各自的优缺点是什么?(这是对一些具体的技术来讲的,可是线程池是一个比较大的概念,可能不涉及这一点,但相应的线程池中有许多不一样的种类,来应对不一样的场景)less
下面的内容是本身读过《实战java 高并发程序设计》以后加上本身的理解写的笔记,若是有错漏之处,请你们在评论区指出。dom
一、正如前面的所说,频繁的建立和销毁时间消耗了太多的资源,占用了太多的时间jvm
二、线程也是须要占用必定内存的,同时存在不少个线程的话,内存很快就溢出了,即便没有溢出,大量的线程回收也会对GC形成很大的压力,延长GC的停顿时间ide
这里能够举个例子来讲明一下,好比你去银行办理业务时,首先得拿号排队吧,而后叫你去哪一个窗口你就得去哪一个窗口,在我看来,这就是一个很典型的线程池的例子。高并发
咱们能够想象一下,若是不按这种模式,会是什么样子……工具
你来到了银行的业务大厅,业务经理问你要办理什么业务,你说我想开个帐户,因而经理拿起手机打通了职工大楼的电话,“让负责开帐户的的那个小组派我的过来”(new 了一个开帐户的对象),业务员快马加鞭的赶了过来而后帮你处理完了任务,只听经理又说到“这里没你事儿了,回去吧”,因而你又回到了职工大楼。
而后又来了一个客户……
因而你把上述的过程又执行了一遍,那么业务员在路上的时间可能比处理业务的时间还要长了。
更糟的是,若是有200个线程同时存在,而且每个客户的业务处理时间都很是的长,那么业务大厅就可能同时存在200个客户和业务员了,大厅挤得都快遇上春运了。
ps : 上面这个小例子举得并非很好,因此你们不要跟实际的知识点对号入座。好比说这里有10个业务员,那么10个业务员其实是不能同时进行服务的,由于你的电脑没有10个cpu,只能是cpu不断的在线程之间进行切换,只要它换的够快,就能够给每个客户一种他一直都在为我服务的感受。
认识线程池
线程池的轮子咱们已经不用本身造了,在jdk5版本以后,引入了Executor框架,用于管理线程。
Executor 框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable 等
先放一张Executor框架的部分类图(下面这个类图就是用idea自带的工具作的,很是方便,有时间再写一下它的用法):
其中虚线箭头指的是实现,实线箭头指的是继承
而本文中咱们须要了解的就是这个ThreadPoolExecutor 和 下面这个Executors了。
ThreadPoolExecutor:
从网上找了一个小例子,就是给一个集合添加2000个元素,咱们分红两个测试,一个测试是添加一次元素就建立一个线程,另一个测试是先建立好线程池,而后再添加。
不使用线程池版本:
//每一次添加操做都开一个线程 public static void getTimeWithThread() { System.out.println("使用多线程测试 start"); final List<Integer> list = new LinkedList<>(); Random random = new Random(); long start = System.currentTimeMillis();
Runnable target = new Runnable() { @Override public void run() { list.add(random.nextInt()); } }; for (int i = 0; i < 20000 ; i++) { Thread thread = new Thread(target); thread.start(); try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } long end = System.currentTimeMillis(); long time = end - start; System.out.println("最终list的大小为:" + list.size()); System.out.println("使用多线程测试 end, 用时:" + time + "ms\n"); }
例子比较简单,咱们须要建立线程,这里用的是实现Runnable接口的方式,而后为了保证子线程执行完成以后主线程(main线程)才执行,咱们这里使用了join方法。
那么整个for循环的意思就是,我开启一个线程,而后你的main线程得等我执行完以后才能开启下一个线程继续执行。
使用线程池版本:
//使用线程池进行集合添加元素的操做 public static void getTimeWithThreadPool() { System.out.println("使用线程池测试 start"); final List<Integer> list = new LinkedList<>(); Random random = new Random(); long start = System.currentTimeMillis();
Runnable target = new Runnable() { @Override public void run() { list.add(random.nextInt()); } }; ThreadPoolExecutor tp = new ThreadPoolExecutor(100, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(20000)); for (int i = 0; i < 20000 ; i++) { tp.execute(target); } tp.shutdown(); try { tp.awaitTermination(1,TimeUnit.DAYS); } catch (InterruptedException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); long time = end - start; System.out.println("最终list的大小为:" + list.size()); System.out.println("使用线程池测试 end, 用时:" + time + "ms\n"); }
使用线程池就比较简单了,咱们只要先建立好线程池,而后向它提交任务就好了,具体的线程该怎么操做,怎么管理都不用咱们来操心。
execute() : 它是顶层接口Executor的一个方法(也是惟一一个),其实跟普通的建立线程执行run方法没有太大的区别
shutdown(): 顾名思义是关闭线程池,它会将已经提交但尚未执行的任务执行完成以后再关闭线程池(什么是提交?咱们后面再说)
至于ThreadPoolExecutor里面那一大堆参数,咱们慢慢再来看。
最后的测试结果:
不用线程池的话,个人机器跑出来大概要8000ms左右(人家网上的例子测出来只要2000ms左右,这差距,是我电脑垃圾,仍是jvm没设置好啊,以后再来看这个问题),使用线程池的话是180ms左右。
能够看出来线程池相对于单纯的使用线程来讲的话做用是至关大的。
ps:这里本身另外测试了一组,不使用线程直接添加,发现时间会快不少,这个问题其实还不是很是明白,多个线程一块儿执行难道不是执行的更快吗?暂时尚未得出结论,等更进一步的理解以后再写一篇文章来进行分析。
ThreadPoolExecutor里面那些参数都是干吗用的?
/** * Creates a new {@code ThreadPoolExecutor} with the given initial * parameters. * * @param corePoolSize the number of threads to keep in the pool, even * if they are idle, unless {@code allowCoreThreadTimeOut} is set * @param maximumPoolSize the maximum number of threads to allow in the * pool * @param keepAliveTime when the number of threads is greater than * the core, this is the maximum time that excess idle threads * will wait for new tasks before terminating. * @param unit the time unit for the {@code keepAliveTime} argument * @param workQueue the queue to use for holding tasks before they are * executed. This queue will hold only the {@code Runnable} * tasks submitted by the {@code execute} method. * @param threadFactory the factory to use when the executor * creates a new thread * @param handler the handler to use when execution is blocked * because the thread bounds and queue capacities are reached * @throws IllegalArgumentException if one of the following holds:<br> * {@code corePoolSize < 0}<br> * {@code keepAliveTime < 0}<br> * {@code maximumPoolSize <= 0}<br> * {@code maximumPoolSize < corePoolSize} * @throws NullPointerException if {@code workQueue} * or {@code threadFactory} or {@code handler} is null */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
corePoolSize : 指定了线程池的线程数量
maximumPoolSize : 指定了线程池中的最大线程数量
keepAliveTime : 超过了corePoolSize时多余线程的存活时间
unit : KeepAliveTime的时间单位
workQueue : 任务队列,被提交但还没有被执行的任务
threadFactory: 线程工厂,用于建立线程
handler : 拒绝策略,当任务太多来不及处理的时候,如何拒绝任务
corePoolSize 和 maximumPoolSize(这里假设corePoolSize是5,maximumPoolSize是10 )
线程池的工做原理是你来一个线程,我就帮你在线程池中新建立一个线程,建立了5个线程以后,再来一个线程,我就不是在第一时间去建立一个新的线程,而是把它加入到一个等待队列中去,等线程池中有了空余的线程再从队列中拿一个出来进行 处理,等待队列的容量是咱们一开始设置好的,若是等待队列也满了的话再去建立新的线程。
当线程池也满了,等待队列也满了(线程池数量达到了maximumPoolSize)的时候就拒绝执行线程的任务,这就涉及到了拒绝的策略。
而通过一段时间以后发现,业务没有那么繁忙了,就不须要一直维持着10个线程,能够清除掉一部分,以避免占据多余的空间。
keepAliveTime 和 unit
有了上面的结束,这个参数就比较好理解了,上面说线程满了再通过一段时间以后就会被清除掉一部分线程,这个通过的时间就是有keepAliveTime 和 unit决定的
好比 keepAliveTime = 1 ,unit = TimeUnit.Days ,那么就是通过一天以后再去清理线程池。
workQueue :
咱们前面也提到了当线程的数量超过了coreSize以后会添加到一个等待队列中去,这个队列就是workQueue。workQueue 采用的是一个实现了BlockingQueue的接口的对象
work也分为不一样的几种,采起不一样的策略
pubic ArrayBlockingQueue( int capacity )
首先是最容易想到的,就是给等待队列设置一个容量,超过这个容量以后再建立新的线程。
handler(拒绝策略):
当线程池和等待队列都满了以后,线程池就会拒绝执行新的任务了,那么该怎么拒绝呢,直接就说你走吧,哥们儿hold不住了吗?显然没这么简单。。
AbortPlicy 策略 : 直接抛出异常,阻止系统正常工做
CallerRunsPolicy策略 : 只要线程池没有关闭,就在调用者线程之中执行这个任务。好比说是主线程提交的这个任务,那我就直接在主线程之中执行这个任务。
DiscardOledestPolicy策略:该策略会丢掉最老的一个请求,也就是即将被执行的那个请求。并尝试再次发起请求。
DiscardPolicy策略:直接丢,不作任何处理
以上策略都是经过实现RejectedExecutionHandler接口实现的,若是上述策略还没法知足你的话,那么你也能够本身实现这个接口。
Executors:
介绍了基本的线程池以后就能够介绍一些jdk为咱们写好的一些线程池了。
它由Executors类生成的。有如下几种:
以newFixedThreadPool为例展现一下它的使用方法。
package thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 展现Executors的简单用法 */ public class Lesson15_ThreadPool02 { public static void main(String[] args) { Runnable task = new Runnable() { @Override public void run() { System.out.println(System.currentTimeMillis() + ": Thread Id : " + Thread.currentThread().getId()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }; ExecutorService ex = Executors.newFixedThreadPool(5); for (int i = 0; i < 10 ; i++) { ex.submit(task); } ex.shutdown(); } }
1541580732981: Thread Id : 13 1541580732981: Thread Id : 14 1541580732981: Thread Id : 11 1541580732981: Thread Id : 12 1541580732981: Thread Id : 15 1541580733981: Thread Id : 13 1541580733981: Thread Id : 15 1541580733981: Thread Id : 12 1541580733981: Thread Id : 11 1541580733981: Thread Id : 14
关于定时线程池的两个方法的区别:
举个例子,好比说,初始延时是1秒,period是5秒,任务实际的执行时间是2秒,那么第一个任务开始执行的时间是1秒,再第二个任务执行的时间是6秒,你看跟任务的实际执行时间并无什么关系。
可是这里会有一个显而易见的问题,按照上面的说法,若是个人任务执行时间是10秒怎么办,远比period要大,那么此时会等待上一个任务执行完成以后当即执行下一个任务,
你也能够理解成period变成了8秒
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
以schedeleAtFixedRate为例,简单写一下代码的用法:
package thread; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * 这里演示定时线程池的功能 */ public class Lesson15_ThreadPool03 { public static void main(String[] args) { Runnable task = new Runnable() { @Override public void run() { System.out.println(System.currentTimeMillis()/1000 + ": Thread Id : " + Thread.currentThread().getId()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }; System.out.println(System.currentTimeMillis()/1000); ScheduledExecutorService ses = Executors.newScheduledThreadPool(5); ses.scheduleAtFixedRate(task,1,3,TimeUnit.SECONDS); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } ses.shutdown(); } }
1541584928
1541584929: Thread Id : 11
1541584932: Thread Id : 11
1541584935: Thread Id : 12
Process finished with exit code 0
从28开始执行定时线程池的任务,1秒钟(初始延时)以后开始执行第一个任务,以后每过三秒钟执行下一个任务
这里若是不关闭线程池的话,任务会一直执行下去。
线程池的分析暂时先到这里,还有一部份内容,例如扩展线程池,如何决定线程池的线程数量,fork/join框架等。等认真读过下一部分以后再继续把线程池部分的笔记凑齐。
package thread;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 这里演示定时线程池的功能
*/
public class Lesson15_ThreadPool03 {
public static void main(String[] args) {
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(System.currentTimeMillis()/1000 +
": Thread Id : " + Thread.currentThread().getId());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
System.out.println(System.currentTimeMillis()/1000);
ScheduledExecutorService ses = Executors.newScheduledThreadPool(5);
ses.scheduleAtFixedRate(task,1,5,TimeUnit.SECONDS);}}