秋招面经二(百度、京东)
百度
一面(2020-7-15)
1、ThreadPool的执行策略
1.1 执行流程
- 如果当前线程池中的线程数目小于 corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
- 如果当前线程池中的线程数目 >= corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;
- 如果当前线程池中的线程数目达到 maximumPoolSize,则会采取任务拒绝策略进行处理;
- 如果线程池中的线程数量大于 corePoolSize 时,如果某线程空闲时间超过 keepAliveTime,线程将被终止,直至线程池中的线程数目不大于 corePoolSize ;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过 keepAliveTime ,线程也会被终止。
1.2 排队策略
在前面我们多次提到了任务缓存队列,即 workQueue,它用来存放等待执行的任务。
workQueue 的类型为 BlockingQueue
,通常可以取下面三种类型:
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
1.3 拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到 maximumPoolSize,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
1ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 2ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 3ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) 4ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
1.4 线程池关闭
ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是 shutdown() 和 shutdownNow(),其中:
- shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
- shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
1.5 线程池容量大小
ThreadPoolExecutor 提供了动态调整线程池容量大小的方法:setCorePoolSize() 和 setMaximumPoolSize(),
- setCorePoolSize:设置核心池大小
- setMaximumPoolSize:设置线程池最大能创建的线程数目大小
当上述参数从小变大时,ThreadPoolExecutor 进行线程赋值,还可能立即创建新的线程来执行任务。
2、关于红黑树
红黑树关键的两个操作:
- recolor(重新染色)
- rotation(旋转)
具体的过程如下所示:
- 将
新插入的节点标记为红色
- 如果 X 是根结点(root),则标记为黑色
- 如果 X 的 parent 不是黑色,同时 X 也不是 root:
3.1
如果 X 的 uncle (叔叔) 是红色
- 3.1.1 将 parent 和 uncle 标记为黑色
- 3.1.2 将 grand parent (祖父) 标记为红色
- 3.1.3 让 X 节点的颜色与 X 祖父的颜色相同,然后重复步骤 2、3
如下图所示:
过程跟着上面的公式走:
- 将新插入的 X 节点标记为红色
- 发现 X 的 parent (P) 同样为红色,这违反了红黑树的第三条规则「不能有两个连续相邻的红色节点」
- 发现 X 的 uncle (U) 同样为红色
- 将 P 和 U 标记为黑色
- 将 X 和 X 的 grand parent (G) 标记为相同的颜色,即红色,继续重复公式 2、3
- 发现
G 是根结点,标记为黑色
- 结束
如果 X 的 uncle 是黑色的情况
- 如果 X 的 parent 不是黑色,同时 X 也不是 root:
3.2 如果 X 的 uncle (叔叔) 是黑色,我们要分四种情况处理
- 3.2.1 左左 (P 是 G 的左孩子,并且 X 是 P 的左孩子)
- 3.2.2 左右 (P 是 G 的左孩子,并且 X 是 P 的右孩子)
- 3.2.3 右右 (和 3.2.1 镜像过来,恰好相反)
- 3.2.4 右左 (和 3.2.2 镜像过来,恰好相反)
当出现 uncle 是黑色的时候我们第一步要考虑的是旋转
- 左左情况:这种情况很简单,想象这是一根绳子,手提起 P 节点,然后变色即可
- 左右情况:左旋: 使 X 的父节点 P 被 X 取代,同时父节点 P 成为 X 的左孩子,然后再应用 左左情况
- 右右情况:与左左情况一样,想象成一根绳子
- 右左情况:右旋: 使 X 的父节点 P 被 X 取代,同时父节点 P 成为 X 的右孩子,然后再应用右右情况
3、数据库
3.1 不同的数据库引擎把数据存在哪里?
.frm文件:存储数据表的框架结构,文件名与表名相同,每个表对应一个同名frm文件,与操作系统和存储引擎无关,即不管MySQL运行在何种操作系统上,使用何种存储引擎,都有这个文件。
(1)myisam数据库表文件:
.MYD文件:即MY Data,表数据文件
.MYI文件:即MY Index,索引文件
.log文件:日志文件
(2)InnoDB采用表空间(tablespace)来管理数据,存储表数据和索引。InnoDB数据库文件(即InnoDB文件集,ib-file set):
ibdata1、ibdata2等:系统表空间文件,存储InnoDB系统信息和用户数据库表数据和索引,所有表共用
.ibd文件*:单表表空间文件,每个表使用一个表空间文件(file per table),存放用户数据库表数据和索引
日志文件: ib_logfile1、ib_logfile2
(3)汇总:
MySQL如果使用MyISAM存储引擎,数据库文件类型就包括.frm、.MYD、.MYI,
MySQL如果使用InnoDB存储引擎,数据库文件类型就包括.frm、ibdata1、.ibd,存放位置有两个,
3.2为什么B+树比其他的树好
因为B+树的深度低,数据存储在硬盘上,当我们每次加载数据的时候,都需要将数据从硬盘上加载到内存,所以每次的比较,都是一次加载数据到内存的过程,B+树的高度低,就可以减少比较的次数,使得加载数据到内存中的次数减少,所以开销小。速度快
3.3 myisam和innodb的区别
InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;
InnoDB是聚集索引,使用B+Tree作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按B+Tree组织的一个索引结构),必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
也就是说:InnoDB的B+树主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值;而MyISAM的B+树主键索引和辅助索引的叶子节点都是数据文件的地址指针。
InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快(注意不能加有任何WHERE条件);
Innodb不支持全文索引,而MyISAM支持全文索引,在涉及全文索引领域的查询效率上MyISAM速度更快高;PS:5.7以后的InnoDB支持全文索引了
MyISAM表格可以被压缩后进行查询操作
InnoDB支持表、行(默认)级锁,而MyISAM支持表级锁, InnoDB的行锁是实现在索引上的,而不是锁在物理行记录上。潜台词是,如果访问没有命中索引,也无法使用行锁,将要退化为表锁。
InnoDB表必须有主键(用户没有指定的话会自己找或生产一个主键),而Myisam可以没有
Innodb存储文件有frm、ibd,而Myisam是frm、MYD、MYI
Innodb:frm是表定义文件,ibd是数据文件 Myisam:frm是表定义文件,myd是数据文件,myi是索引文件
二面(2020-7-29)
1、kafka重复消费
1.1、异步提交offset时的情况
首先我们将消息的offset提交方式修改为手动提交,在每一次的消息消费完成之后,我们使用异步的方式commitAsync(),提交这个消息的offset,这个时候,我们如果提交失败,将会有重试的机制。现在考虑一种情况:A线程异步提交offset为x,B线程异步提交offset为x+y,在A消费完消息提交offset的时候,发生了异常,需要重试,而B线程提交offset:x+y成功了,那么此时的offset应该为x+y,在此时A线程的提交重试成功,那么offset又被修改为x。
在下一次进行消费拉取的消息的时候就会从x的offset的位置开始拉取,此时就形成了重复消费的现象。
解决方法
:我们可以设置一个递增的序号来维护异步提交的顺序,每次位移提交的是偶之后就增加序号相对应的值。在遇到位移提交失败需要重试的时候,可以检查所提交的位移和序号的值的大小,如果前者小于后者,则说明有更大的位移被提交了,不需要再进行本次重试,如果两者相同则说明可以进行重试提交。
2、项目帖子拉黑搜索?
只有管理员
才可以有拉黑搜索的功能,并且拉黑搜索即为将数据库中的帖子状态设为2
,从ES中删除帖子
3、登录的时候密码使用http是明文传输,怎么解决?
解决http的数据安全问题:
(1)选择拦截过滤器。
在请求的时候对请求方法进行一次拦截处理
。比如非正常访问的方法已经注入插入可执行语句参数验证等,在拦截中进行一次安全校验,保证请求不是非法请求
。
(2)数据加密。
我们知道目前大部分APP接口都是通过Http协议进行调用的容易被抓包拦截。
我们可以对客户端和服务端都对数据传输的时候进行一个加密处理
。常用的MD5 AES等。
譬如:上一个项目做法比较简单 对用户与帐号密码进行加密作为一个authcode,每次请求必须携带
。这个方法与下边说的方法3组合运用
客户端:
1、设置一个key(和服务器端相同) 2、根据上述key对请求进行某种加密(加密必须是可逆的,以便服务器端解密) 3、发送请求给服务器
服务器端:
1、设置一个key 2、根据上述的key对请求进行解密(校验成功就是「信任」的客户端发来的数据,否则拒绝响应) 3、处理业务逻辑并产生结果 4、将结果反馈给客户端
(3)签名
根据用户名或者用户id,结合用户的ip或者设备号,生成一个token。在请求后台,后台获取http的head中的token,校验是否合法(和数据库或者Redis中记录的是否一致,在登录或者初始化的时候,存入数据库/redis)
在使用Base64方式的编码后,Token字符串还是有20多位,有的时候还是嫌它长了。由于GUID本身就有128bit,在要求有良好的可读性的前提下,很难进一步改进了。那我们如何产生更短的字符串呢?还有一种方式就是较少Token的长度,不用GUID,而采用一定长度的随机数,例如64bit,再用Base64编码表示:
var rnd = new Random(); var tokenData = userIp+userId; rnd.NextBytes(tokenData); var token = Convert.ToBase64String(tokenData).TrimEnd('=');
由于这里只用了64bit,此时得到的字符串为Onh0h95n7nw的形式,长度要短一半。这样就方便携带多了。但是这种方式是没有唯一性保证的。不过用来作为身份认证的方式还是可以的(如网盘的提取码)。
(4)使用第三方框架与技术整合支持比如spring的框架中的oauth模块。
4、ES越来越慢
(1)可能和存储时的索引有关
在ES中,我们需要对每种type创建映射关系,对于字符串类型的映射关系,每个字段对应的有index属性,index可选的参数值有3个:
- analyzed:分析器将所有字符转为消息,并将字符串分解为单词。当期望
每个单词
完整匹配时,可以使用这种选项。比如:我们搜索单词“big”或“data”,都可以匹配到“big data“文档。 - not_analyzed:分析过程被忽略,
整个字符串
被当做单独的词条进行索引。当进行精准匹配时,请使用这个选项。比如:我们搜索单词“big”或“data”,都无法匹配
到“big data“文档,只有搜索”big data“的时候,才会搜索到”big data“对应的文档。也就是说,在这种情况下,适合的是精准匹配,不会对输入内容进行拆分单词的操作。 - no:这种选项是不对文档字段进行索引。比如当我们存储帖子和评论的时候,我们需要将其分开存储,这个时候我们不应该对评论开启索引选项,因为在真正的搜索时,大家更加期望的是对帖子内容进行搜索而不是对评论进行搜索。
在上面的搜索配置选项中,通过开启不一样的index选项,可以达到不同的目标,但是代价也是更加耗时和占用存储空间。如果我们全都使用默认的analyzed选项的话,那么我们在搜索的时候,自然会使得搜索越来越慢。
(2)使用再打分
elasticsearch中自带的打分系统已经被Lucene进行了深度优化,但是如果我们使用自己的脚本打分方案,则会使得整体的性能显著降低。在这种情况下,我们可以先使用elasticsearch中自带的打分算法,进行初步打分,然后取一定数量的结果,来使用我们自己的脚本进行再打分
。
(3)查询
在查询的时候,所有的记录都是存在磁盘中的,如果内存过小,我们无法将所有的数据存放在内存中的Filesystem cache
中时,索引将走磁盘io的方式,这个时候我们的速度将会降下来。
优化方案:
- 创建索引的时候
不要把所有的字段都存在ES中
,仅仅存放需要被我们进行搜索的字段,这样可以减少缓存。 - 采用
数据预热
的方法,将一些热门搜索的数据(比如大V发送的帖子博客)提前存放到 Filesystem cache 中,这样可以提高整体的搜索效率,与此同时,我们还可以专门设计一个数据预热的子系统,每隔固定的时间将新的热门数据进行缓存。 冷热分离
:将冷门数据和热门数据分开创建索引。这样就可以,这样可以确保热数据在被预热之后,尽量都让他们留在filesystem os cache
里,别让冷数据给冲刷掉。
5、代码题输出什么
6、为什么不用filter来实现记录日志的功能,而要使用aop
fliter在前面就已经拦截下来了,无法完成我们需要的功能。
7、Linux中用什么命令来检索所有文件中有“abc”的文件
使用grep
8、分页查询limit语句
select * from table limit 10*10 10;
9、如何将int转为long
10、前缀树算法实现
kmp
11、项目中有哪些表
comment、discuss_post、login_ticket、message、user
12、项目中使用的es的版本,客户端使用的版本
es版本号:6.4.3
13、项目中使用的多线程地方
ThreadLocal区分不同的登陆用户
quartz定时任务,定时刷新帖子分数
14、kafka的副本选举
当分区的leader节点发生故障的时候,其中一个follower节点就会成为新的leader节点,这样就会导致集群的负载不均衡
,从而影响整体的健壮性和稳定性。当原来的leader节点恢复之后重新加入集群时,它只能成为一个新的follower节点而不再对外提供服务。
为了能够有效治理负载失衡的情况,kafka引入优先副本
的概念。优先副本就是指在AR集合列表中的第一个副本
。理想情况狂下,优先副本就是该分区的leader副本。kafka要确保所有主题的优先副本在kafka集群中均匀分布,这样就保证了所有分区的leader均衡分布。如果leader分布过于集中,就会造成集群负载不均衡。
在kafka中,使用优先副本的选举方式。
三面(2020-8-13)
1、Redis里面的如果判断过期时间的底层实现
redis 过期策略是:定期删除+惰性删除。
所谓定期删除,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
假设 redis 里放了 10w 个 key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的灾难。实际上 redis 是每隔 100ms 随机抽取一些key 来检查和删除的。
但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个 key 的时候,redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。
获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。
答案是:走内存淘汰机制。
2、OOM,死锁等基本的排查工具。
死锁可以使用jstack命令来查看对应进程的运行情况。
3、一个项目oom了,从哪些方面开始排查
(1)登录Linux,看一下CPU的负载情况
使用top
或者uptime
命令查看CPU的负载情况。
(2)使用jstack进入对应负载较高线程中,查看线程内存的使用情况
3、RPC
4、duboo
5、在kafka里面,如果第一个消息发送过程中,由于网络延迟,较慢,第二个消息先到达,如何保证顺序性
场景:当acks=0的时候,我们可以有最大的吞吐量,即:生产者发送完消息就不管了,无论这条消息有没有被写入到kafka中。
方法一:如果acks设为非0,``max.in.flight.requests.per.connection(生产者客户端与Node之间的连接最多缓存的请求数量)参数设为大于1的值,就会出现下面这种情况:如果第一批次消息写入失败,而第二批次写入成功,那么生产者会重试发送第一批次的消息,此时如果第一批次的消息写入成功,那么这两个批次就出现了错序。一般而言,
在需要保消息顺序的场合建议把参数max.in.flight.requests.per.connection配置为1,而不是把acks配置为0`,不过这样会影响整体的吞吐量。
方法二:使用幂等的特性。
京东
一面(2020-7-16)
1、ES底层搜索算法
反向搜索,将存在index库中的所有document记录,按照分词进行重建。把所有的分词作为索引,然后每个索引对应的值,是拥有这个词语的所有的文本的id编号。所以我们可以根据输入的单词来快速获取拥有我们搜索的关键词的所有记录。然后按照命中率进行一个分数计算,给我们呈现出来的是一个排好序的记录形式。
2、Redis分布式部署,分布式存储问题
2.1 传统的分布式部署算法
比如现在有一个数据,使用redis存储时,会现将其进行hash计算,然后根据计算的hash值进行取模
,根据取模的结果将数据缓存到与结果值对应的redis中。如果增加或者减少一个Redis,那么我们所有Redis中的数据存储位置都会有相应的改变。所有就有很大的缺点。
2.2 hash一致性算法
这个算法先将数据映射到hash空间中,然后将cache(redis)也映射到hash空间中。算法的数据存储是按照hash空间环 顺时针 存储(将数据存储到顺时针遇到的第一个cache中),如下图所示:
【优势】:比如我们现在移除或添加一台cache,那么数据还是按照顺时针存储到数据库中,移除后并不会对object4造成影响,它继续顺时针移动到cacheC中。再比如添加一个Cache,依然不会造成影响,依然按照顺时针移动到相应的cache中。
【缺点】:如果Redis在环上的分布不均匀则会对某些Redis产生巨大的压力。如下所示的分布将会大大增加A节点的压力。
【改进】可以使用虚拟节点的方式来解决,比如一个cache有两个虚拟节点,当数据命中到节点上时,将数据存储到相应的真实的cache中。使用虚拟节点来解决分布均匀问题:
3、敏感词过滤算法中的AC算法
3.1 DFA
3 .2 KMP算法
在敏感词过滤的时候,我们使用的是一种回溯算法,但是这种算法的使用效率不高,主要是由于一个敏感词匹配失败之后,会使得整体的匹配初始值从头再开始。在网上有一种算法叫做KMP算法,可以避免回溯问题。
KMP算法的主要特征是构建一个部分匹配表,部分匹配表是根据已经匹配的字符串的最大前后缀来计算得到的。在每次匹配失败之后的回溯过程中,我们使用到了部分匹配表中的数值,来增加我们回溯的起点的索引值,从而提高整体的效率。
"部分匹配"的实质是,有时候,字符串头部和尾部会有重复。比如,"ABCDAB"之中有两个"AB",那么它的"部分匹配值"就是2("AB"的长度)。搜索词移动的时候,第一个"AB"向后移动4位(字符串长度-部分匹配值),就可以来到第二个"AB"的位置。
4、hashmap安全问题有哪些?
- 如果多个线程同时使用put方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程的 put 的数据被覆盖。
- 多个线程同时检测到元素个数超过数组大小* loadFactor ,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失。
5、threadLocal
threadLocal中主要是用来做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。
5.1 在spring中的使用
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring的事务主要是ThreadLocal和AOP去做实现的。
5.2 ThreadLocal的底层实现
首先给一个使用场景
ThreadLocal<String> localName = new ThreadLocal(); localName.set("张三"); String name = localName.get(); localName.remove();
看一下set源码,即为ThreadLocal的set方法的源码
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
获取当前线程,然后调用ThreadLocal.getMap方法
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
public class Thread implements Runnable { /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; }
所以,我们返回的ThreadLocalMap即为当前线程中的一个变量
,所以我们在ThreadLocal中,可以将不同的线程自己的数据隔离开。
然后来看一下ThreadLocalMap
这个类
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * ` null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } /** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; }
ThreadMap中使用的是Enrty
数组,没有继承Map接口,而是使用了Entry,同时Entry的key是ThreadLocal。ThreadLocalMap的大概结构如下所示
然后再看看set方法
private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. 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<?> k = e.get(); if (k ` key) { e.value = value; return; } if (k ` null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
所以ThreadLocal的set方法,主要是操作底层的ThreadLocalMap来完成的。根据hash值来定位,然后依次往后遍历,如果有相同的key,则直接刷新覆盖。如果有空值,则在这个地方进行创建值,如果有值但是不为key,则继续向后遍历。
然后再来看一下ThreadLocal的get方法:
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
依旧继续操作底层的ThreadLocalMap使用get方法。我们继续向下看ThreadLocalMap的getEntry方法
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() ` key) return e; else return getEntryAfterMiss(key, i, e); }
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k ` key) return e; if (k ` null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
循环查找的方法与上面的set方法一样。
由此,我们就结束了整个ThreadLocal的底层get和set方法。
5.3 ThreadLocal存在的问题
(1)对象存储问题
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
(2)那么是不是说ThreadLocal的实例以及其值存放在栈上呢?
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
5.4 内存泄漏问题
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
弱引用:
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。
(1)解决方法
在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。
(2)那为什么ThreadLocalMap的key要设计成弱引用?
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
6、jvm工作流程
二面(2020-7-20)
1、 volatile的三个特性
- 可见性
- 禁止重排序
- 不保证原子性
2、平时用一些什么数据结构,用到实战中使用这些数据结构做了什么?
比如hashmap。
3、 面向对象的理解,三大特性
为什么要面向对象
多态、封装、继承
4、使用mybatis如何将插入的记录的主键,返回给我们的项目中,有一个小技巧。
在我们写Usermapper的xml文件时候,将user对象的id
设置为主键,与数据库中进行对应。当我们在向数据库中插入的user的时候,数据库底层会自动的生成主键,然后mybatis可以从数据库中获取到这个主键信息,然后将数据库中的主键信息再填回到我们的user对象的主键id中,这个时候我们的user对象就有主键id的值了.
<!--在数据库插入user之后,mybatis会从数据库中索取一个id,然后赋给user的id--> <insert id="insertUser" parameterType="User" keyProperty="id"> insert into user (<include refid="insertFields"></include>) values(#{username},#{password},#{salt},#{email},#{type},#{status},#{activationCode},#{headerUrl},#{createTime}) </insert>
5、为什么hashmap底层要实现hashcode和equals方法,之间有什么关系,jdk1.7和1.8实现区别
散列码应该更加关注速度,而不是唯一性,所以散列码不必是独一无二的,但是通过hashcode()和equals()方法,我们必须能够完全确定对象的身份。
jdk1.7和1.8的主要区别在于1.8之后引入了红黑树