发布于2021-06-14 11:19 阅读(846) 评论(0) 点赞(13) 收藏(2)
在之前的文章中,已经发布了常见的面试题,这里我花了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。
本文主要描述的是java多线程中提供的一些常用框架threadlocal、线程池。线程池是面试的一大热点问题,非常有必要掌握线程池的原理和相关源码;Threadlocal在某些特定的场合下作用非常大,掌握其核心内容和使用方法也非常有必要。如果对java多线程感兴趣的同学可以看下公众号里多线程系列的文章,也许会对你有些帮助。
ThreadLocal有了解么?ThreadlocalMap的key 和value是什么?怎么保证内存不会泄露?
Threadlocal是线程本地变量类,该类的作用是可以在一个线程的保存变量,该变量只有该线程可以访问,这样可以在一定程度上避免使用锁。另一个作用是避免同一个线程上下文传递。Threadlocal的值是通过Thread类中的ThreadLocalMap进行存储。ThreadLocalMap的每个entry对象是一个弱引用对象,key是threadLocal对象,value是要存入的值。我们看下ThreadLocalMap的定义:
- static class ThreadLocalMap {
-
- //静态内部类,每个entry对象key都是一个弱引用对象
-
- static class Entry extends WeakReference<ThreadLocal<?>> {
-
- Object value;
-
- //key 是ThreadLocal 对象,value是具体的值
-
- Entry(ThreadLocal<?> k, Object v) {
-
- super(k);
-
- value = v;
-
- }
-
- }
-
- private static final int INITIAL_CAPACITY = 16;//初始值 16
-
- private Entry[] table;//entry对象
-
- private int size = 0;//entry的大小
-
- private int threshold;//扩容的阈值
-
- }
需要了解的是,ThreadLocalMap在解决hash冲突的时候,使用的是线性探测的方式来解决的。
- private void set(ThreadLocal<?> key, Object value) {
-
- Entry[] tab = table;
-
- int len = tab.length;
-
- int i = key.threadLocalHashCode & (len-1);
-
- for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {//一直寻找下一个空位
-
- //...
-
- }
-
- }
Threadlocal为什么会发生内存泄漏?在这里,先提一下java的四种引用及其回收机制:强、软、弱、虚。强引用是不会被垃圾回收器回收的,软引用是没有内存的时候会被回收,弱引用和虚引用在发生垃圾回收的时候被回收。所以如果我们不希望软、弱被回收的话,我们只要使用一个强引用指向软、弱引用。
下图是threadlocal的在堆的存储图,如果threadlocal对象有强引用的话,那么key不会被回收,当失去强引用连接时,entry中的key就会被清除,但是value是一个强引用,所以value不会被清除,这样就有可能导致内存泄漏了。
如何避免内存泄漏,实际上是在我们每次使用完threadlocal的时候,调用remove方法进行删除就行。现在看下remove方法的实现:
- private void remove(ThreadLocal<?> key) {
-
- Entry[] tab = table;
-
- int len = tab.length;
-
- int i = key.threadLocalHashCode & (len-1);
-
- for (Entry e = tab[i];
-
- e != null;
-
- e = tab[i = nextIndex(i, len)]) {
-
- if (e.get() == key) {
-
- e.clear();//找到清除
-
- expungeStaleEntry(i);//同时清除过期的key,key为null的值
-
- return;
-
- }
-
- }
-
- }
Java的线程池是怎么实现的?其原理是什么?线程池怎么设计核心线程数,拒绝策略怎么选择?怎么优雅关闭一个线程池?
在java语言中,Executors提供了四种线程池的创建方式,newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor。除了周期任务的线程池会在某些定时任务上使用,其他的线程池一般是不建议在实际中使用。而实际上,我们更应该关注线程池的底层创建ThreadPoolExecutor。我们看下构造函数:
- public ThreadPoolExecutor(int corePoolSize,//核心线程数
- int maximumPoolSize,//最大线程数
- long keepAliveTime,//最大线程活跃时间
- TimeUnit unit,//最大线程的时间单位
- BlockingQueue<Runnable> workQueue,//工作队列
- ThreadFactory threadFactory,//线程创建工作
- RejectedExecutionHandler handler)//拒绝策略
这几个参数中,核心的参数要记住:核心线程数,最大线程数,阻塞队列,拒绝策略。
我们看下线程池的实现原理:
这是线程池的经典原理图:
核心的执行代码如下:
- public void execute(Runnable command) {
-
- if (command == null)//判空
-
- throw new NullPointerException();
-
- int c = ctl.get();//获取当前线程数量
-
- if (workerCountOf(c) < corePoolSize) {//如果数量小于核心线程数
-
- if (addWorker(command, true))//创建线程并执行任务,有可能添加失败
-
- return;
-
- c = ctl.get();//更新当前线程数
-
- }
-
- if (isRunning(c) && workQueue.offer(command)) {//超过核心数且正在运行,添加到阻塞队列中
-
- int recheck = ctl.get();//再次获取线程数
-
- if (! isRunning(recheck) && remove(command))//如果线程已经关闭并且从阻塞队列中移除
-
- reject(command);//执行拒绝策略
-
- else if (workerCountOf(recheck) == 0)//如果当前线程数为0
-
- addWorker(null, false);//创建线程
-
- }
-
- else if (!addWorker(command, false))//如果创建最大线程数并执行失败
-
- reject(command);//执行拒绝策略
-
- }
线程池设计核心线程数的原则上主要分io密集型和cpu密集型。如果是io密集型,就说明cpu需要等待,那么多提供一点线程数量去执行其他任务一定是能增加效率的。如果是cpu密集型,那就意味着cpu不可能闲置,如果线程的数量过高,反而因为线程上下文来回切换导致效率低下,这个时候线程数就不宜设置过高。了解上面的原则之后,实际中,我们也不会说具体到多少做个试验,一般就是参考其他作者的试验结果,如下其他书籍试验的结果(以供参考,原文来自:https://www.cnblogs.com/651434092qq/p/14240406.html ):
Java并发实战的书中给出的计算公式:
核心线程数 = CPU 核心数 * (1 + IO 耗时/ CPU 耗时)
(io耗时和cpu耗时使用工具统计)
Java虚拟机并发编程书中提供的计算公司:
核心线程数 = CPU 核心数 / (1 - 阻塞系数)
(其中计算密集型阻塞系数为 0,IO 密集型阻塞系数接近 1,一般认为在 0.8 ~ 0.9 之间)
拒绝策略的选择也很关键,默认提供了以下几种策略:
CallerRunsPolicy - 使用调用线程直接运行任务。一般不允许失败的情况下使用。但是,如果并发量很大的话,我们就有可能导致执行线程阻塞,一般不建议。
AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。
DiscardPolicy - 直接丢弃
DiscardOldestPolicy - 丢弃阻塞队列中最老的一个任务,并将新任务加入
一般建议使用抛异常,并且自己将异常进行处理,如果不允许失败的话,那就重新执行或者保存起来下次执行,如果有必要,建议自行实现拒绝策略,在实战中,我们就出现过因为拒绝策略导致整个环境阻塞严重的问题。
线程池的优雅关闭也是一个很重要的内容,尤其是当我们的系统不能中断服务的时候,这个时候就要确保执行的线程已经完成,新的任务不在提交到该线程池中。我们先看下线程池的几个状态:
- private static final int RUNNING = -1 << COUNT_BITS;//运行状态,接收任务的添加并且处理队列中的任务
-
- private static final int SHUTDOWN = 0 << COUNT_BITS;//关闭状态,不接受新的任务,但是队列中的任务会处理
-
- private static final int STOP = 1 << COUNT_BITS;//停止状态,不接受新任务,不处理队列中的任务,并且中断当前运行的任务
-
- private static final int TIDYING = 2 << COUNT_BITS;//清理状态,所有的任务都不在执行,运行的线程已经为0,执行关闭钩子的方法
-
- private static final int TERMINATED = 3 << COUNT_BITS;//结束状态,关闭钩子的方法已经执行完,线程池真正关闭完了
线程池提供的几个关闭方法:
Shutdown方法: 将线程池的状态从running->shutdown
shutdownNow:将线程池的状态从(running/shutdown->stop)
awaitTermination:等待线程池的状态变成terminated状态
线程池的优雅关闭代码如下:
- public void close{
-
- threadPool.shutdown(); // 关闭
-
- try {
-
- if (!threadPool.awaitTermination(30, TimeUnit.SECONDS)) {// 等待30s
-
- threadPool.shutdownNow();// 调用 shutdownNow 取消正在执行的任务
-
- if (!threadPool.awaitTermination(30, TimeUnit.SECONDS))// 再次等待30s
-
- log.error("线程池关闭异常,有任务在一直执行")
-
- }
-
- } catch (InterruptedException ex) {
-
- threadPool.shutdownNow();// 捕获异常,调用shutdownNow强行关闭
-
- }
-
- }
在关闭中,我们要设置一个关闭等待时间,如果时间内没有关闭,我们一般会认为线程这个时候是阻塞住了,那么就强行关闭。
线程池是本文的核心内容,要了解线程的关键参数,原理,有必要的话,看下底层的实现代码。线程池参数的设计和关闭是实战过程中需要关注的;Threadlocal在某些特定的场景下使用,比如分库分表,这些线程本地变量能够带来很大的方便。Threadlocal在使用上不难,要关注一下内存泄漏的问题,要知道,在java语言中,内存泄漏并不常见。
本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞或者转发一下哈,谢谢。
想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈
原文链接:https://blog.csdn.net/rqc112233/article/details/117729828
作者:怎么没有鱼儿上钩呢
链接:http://www.javaheidong.com/blog/article/222771/103e099cba9ac9cadd58/
来源:java黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 java黑洞网 All Rights Reserved 版权所有,并保留所有权利。京ICP备18063182号-2
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!