共计 5124 个字符,预计需要花费 13 分钟才能阅读完成。
银行就是一个线程池
image-20201126113618828
银行实际上就是一个 Java 线程池。
一次周末去银行办业务,人多排号,排的久了忽然发现银行实际上和 Java 的线程池如此相似,能够说截然不同。于是我就开展了联想。
- 银行是个线程池
- 周末,银行七个窗口只来了两个值班的员工,那么这两个员工咱们就叫做 外围员工数
- 那天因为不晓得什么起因,来银行办业务的人特地多,两个员工很快就忙不过来了,那么新来的人怎么办呢?只能排个号在休息区的椅子上坐着等,这一排椅子咱们就叫做 期待队列
- 很快,休息区也坐满了人,这个时候银行的解决业务的效率显著跟不上了,咋办呢?能够叫人会来加班(创立新的线程),因为银行只有 7 个窗口,所以最终只能有 7 个员工。最多七个柜台就叫做 最大员工数
-
有了新的员工,人多了,解决业务的速度也上来了,会呈现两种状况
- 尽管银行曾经满负载了,然而人还是源源不断的来银行办业务,这个时候所有的工作人员都忙着,期待区也坐满了人,那么新的人怎么办呢?工作人员能够抉择让新的员工明儿,或者一会儿再来,这个让新的人什么时候来的做法,就是一种 回绝策略的抉择。
- 除了下面的状况还会有另一种状况,5 集体来加班之后,效率晋升,很快就把所有的业务做完了,而后银行就没有人办业务了,那么这多进去的五个人要始终在银行待着吗?必定不是,他们会等一会儿而后回家,这个等一会儿咱们能够叫做 闲暇等待时间
以上银行的流程和线程池的流程齐全是一样的,咱们只须要将
- 银行工作人员换成线程
- 办业务的人换成工作对象(Runnable)
- 休息区换成期待队列
……
上述的步骤就和线程池的原理,基本参数截然不同了。
说完了线程池的基本原理,接下来就是线程池常见的面试问题。
什么是线程池?有什么益处?
谈到线程池就会想到池化技术,其中最外围的思维就是把贵重的资源放到一个池子中;每次应用都从外面获取,用完之后又放回池子供其他人应用,有点吃大锅饭的意思。
Java 线程池有以下长处:
- 线程是稀缺资源,不能频繁的创立。
- 解耦作用;线程的创立于执行齐全离开,不便保护。
- 该当将其放入一个池子中,能够给其余工作进行复用。
创立线程池的形式
- 通过 Executors 类
- 通过 ThreadPoolExecutor 类
在 Java 中,咱们 能够通过 Executors 类创立线程池,常见的 API 有:
- Executors.newCachedThreadPool():有限线程池。
- Executors.newFixedThreadPool(nThreads):创立固定大小的线程池。
- Executors.newSingleThreadExecutor():创立单个线程的线程池。
- Executors.newScheduledThreadPool()
- Executors.newWorkStealingPool(int) java8 新增,应用目前机器上可用的处理器作为它的并行级别
以上的这些创立线程池的办法,实际上 JDK 曾经给咱们写好的,能够拿来即用的。然而只有咱们查看上述办法的源码就会发现:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
以上办法实际上都是利用 ThreadPoolExecutor 类实现的。
所以第二种创立线程形式是本人通过 new ThreadPoolExecutor 来进行创立。
Executors 有那么多创立线程池的办法,开发中用哪个比拟好?
答案:一个都不必。
从《阿里巴巴 Java 开发手册》中能够看到
image-20200728104421665
如何通过 ThreadPoolExecutor 自定义线程池?即线程池有哪些重要的参数?
在上一个问题中,咱们提到了创立线程池要通过 new ThreadPoolExecutor 的形式,那么,如何创立呢?在创立的时候,又须要哪些参数呢?
咱们间接看一下 ThreadPoolExecutor 的构造方法源码,如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
稀稀拉拉都是参数,那么这些参数都什么呢?
image-20200728104546690
大抵的流程就是
- 创立线程池之后,有工作提交给线程池,会先由 外围线程执行
- 如果工作继续减少,corePoolSize 用完并且工作队列满了,这个时候线程池会减少线程的数量,增大到最大线程数
- 这个时候如果工作持续减少,那么因为线程数量曾经达到最大线程数,期待队列也曾经满了,这个时候线程池实际上是没有能力执行新的工作的,就会采纳回绝策略
- 如果任务量降落,就会有很多线程是不须要的,鸿鹄之志,而只有这些线程闲暇的工夫超过闲暇线程工夫,就会被销毁,直到残余线程数为 corePoolSize。
通过以上参数能够就能够灵便的设置一个线程池了,示例代码如下:
/**
* 获取 cpu 外围数
*/
private static int corePoolSize = Runtime.getRuntime().availableProcessors();
/**
* corePoolSize 用于指定外围线程数量
* maximumPoolSize 指定最大线程数
* keepAliveTime 和 TimeUnit 指定线程闲暇后的最大存活工夫
*/
public static ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, corePoolSize+1, 10l, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(1000));
线程池底层工作原理?
对于线程池的工作原理和执行流程,通过两张图来进行展现
image-20201126094135666
image-20201126094144097
- 在创立了线程池后,期待提交过去的工作申请。
- 当调用 execute()办法增加一个申请工作时,线程池会做如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创立马上创立线程运行这个工作。
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个工作 放入队列。
- 如果这个时候队列满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创立非核心线程立即运行这个工作。
- 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池 会启动饱和回绝策略来执行。
- 当一个线程实现工作时,它会从队列中取下一个工作来执行。
- 当一个线程无事可做超过肯定的工夫(keepAlilveTime)时,线程池会判断:
- 如果以后运行的线程数大于 corePoolSize,那么这个线程就被停掉。
- 所以线程池的所有工作实现后它 最终会膨胀到 corePoolSize 的大小。
谈谈线程池的饱和策略,也叫做回绝策略。
所谓饱和策略就是:当期待队列曾经排满,再也发不下新的工作的时候,这时,线程池的最大线程数也到了最大值,意味着线程池没有能力继续执行新工作了,这个时候再有新工作提交到线程池,如何进行解决,就是饱和(回绝)策略
image-20201126094213570
如何合理配置一个线程池
通常咱们是须要依据这批工作执行的性质来确定的。
- IO 密集型工作:因为线程并不是始终在运行,所以能够尽可能的多配置线程,比方 CPU 个数 * 2
-
- IO 密集型,即该工作须要大量的 IO,即大量的阻塞。
- 在单线程上运行 IO 密集型的工作会导致节约大量的 CPU 运算能力节约在期待。
- 所以 IO 密集型工作中应用多线程能够大大的减速程序运行,即便在单核 CPU 上,这种减速次要就是利用了被节约掉的阻塞工夫。
- CPU 密集型工作(大量简单的运算)该当调配较少的线程,比方 CPU 个数相当的大小。CPU 密集的意思是该工作须要大量的运算,而没有阻塞,CPU 始终全速运行。
当然这些都是经验值,最好的形式还是依据理论状况测试得出最佳配置。
如何敞开线程池
敞开线程池的办法有两个:shutdown()/shutdownNow()
。
shutdown()
执行后进行承受新工作,会把队列的工作执行结束。shutdownNow()
也是进行承受新工作,但会中断所有的工作,将线程池状态变为 stop。
敞开线程池的代码:
long start = System.currentTimeMillis();
for (int i = 0; i <= 5; i++) {pool.execute(new Job());
}
pool.shutdown();
while (!pool.awaitTermination(1, TimeUnit.SECONDS)) {LOGGER.info("线程还在执行。。。");
}
long end = System.currentTimeMillis();
LOGGER.info("一共解决了【{}】", (end - start));
pool.awaitTermination(1, TimeUnit.SECONDS)
会每隔一秒钟查看一次是否执行结束(状态为 TERMINATED
),当从 while 循环退出时就表明线程池曾经齐全终止了。
❤️ 帅气的你又来看了我
如果你感觉这篇内容对你挺有有帮忙的话:
- 点赞反对下吧,让更多的人也能看到这篇内容(珍藏不点赞,都是耍流氓 -_-)
- 欢送在留言区与我分享你的想法,也欢送你在留言区记录你的思考过程。
- 感觉不错的话,也能够关注 编程鹿 的集体公众号看更多文章和解说视频(感激大家的激励与反对????????????)