作为一个好几年没打开IDE的人来填下自己挖的坑,回答下上篇文章的题目,这应该也算能充分说明一旦真正的掌握了,其实是很难忘的,当然,另一方面貌似也说明这些年了Java语言层面进步是有限的。
这些题目我是完全根据Java编程进阶的思路来设计的,很多同学可能觉得背背答案就能去应对一些面试,先不说为什么这个是很难靠背的,事实上只要面试官稍有点水平,通常在题目的问法上就会有很多的变化,很容易判断出面试者的情况,而且面试官应该是根据面试者本身的背景来问问题,而不是千篇一律,所以我觉得发这些题目,最希望的是大家可以认真的去学习和掌握背后的知识点,这样才能以不变应万变。
基于BIO实现的Server端,当建立了100个连接时,会有多少个线程?如果基于NIO,又会是多少个线程? 为什么?
答:BIO由于不是NIO那样的事件机制,在连接的IO读取上,无论是否真的有读/写发生,都需要阻塞住当前的线程,对于基于BIO实现的Server端,通常的实现方法都是用一个线程去accept连接,当连接建立后,将这个连接的IO读写放到一个专门的处理线程,所以当建立100个连接时,通常会产生1个Accept线程 + 100个处理线程。
NIO通过事件来触发,这样就可以实现在有需要读/写的时候才处理,不用阻塞当前线程,NIO在处理IO的读写时,当从网卡缓冲区读或写入缓冲区时,这个过程是串行的,所以用太多线程处理IO事件其实也没什么意义,连接事件由于通常处理比较快,用1个线程去处理就可以,IO事件呢,通常会采用cpu core数+1或cpu core数 * 2,这个的原因是IO线程通常除了从缓冲区读写外,还会做些比较轻量的例如解析协议头等,这些是可以并发的,为什么不只用1个线程处理,是因为当并发的IO事件非常多时,1个线程的效率不足以发挥出多core的CPU的能力,从而导致这个地方成为瓶颈,这种在分布式cache类型的场景里会比较明显,按照这个,也就更容易理解为什么在基于Netty等写程序时,不要在IO线程里直接做过多动作,而应该把这些动作转移到另外的线程池里去处理,就是为了能保持好IO事件能被高效处理。
从上面可以看出,对于大多数需要建立大量连接,但并发读写并不会同时的场景而言,NIO的优势是非常明显的。
这种关于BIO、NIO的问法的变化空间是非常大的,还可以进一步拓展问问AIO和BIO、NIO的根本不同。
通常来说基于NIO实现的Server端,会用多少个线程去处理IO事件,为什么?
答:见1里面的回答。
一个典型的客户端集群->LB->服务端集群这样的结构中,如客户端采用连接池,长连接的方式,这种设计你觉得可能会出现什么问题?如果客户端采用的是单个长连接的方式呢?如果有问题,你觉得应该怎么解决?
答:这题比较开放,会有各种回答,这里讲下当年我自己在这里碰到的一个很大的坑,血泪教训,也是我认为这样的结构里最大的风险。
客户端采用连接池,长连接,通过LB去连接后端的服务端集群,在这样的结构下,由于客户端看到的其实只有LB提供出来的vip,会导致的一个严重问题是服务端集群出现不均衡的现象,尤其是在服务端集群发布重启等情况下,最恶劣的情况下搞不好会导致服务端集群压根就没法启动了。
客户端采用单个长连接,其实也会碰到同样的问题。
当年,最早的时候我们的系统就是采用这样的方式,导致出现过严重故障,服务端发布的时候启动不了,因为发布的分批导致了压力压在了少数的机器上,容量不够就崩了,那次处理的时候只好先把vip disable掉,服务端集群全部发布好了,再把vip enable,才勉强扛过去了。
像这种问题,解决起来很麻烦,例如让长连接到达一定条件下就断开下,但这样长连接的作用就降低了,比较根本的解决方法是在这样的场景里把中间的LB去掉,换成类似通过服务注册/发现的机制来解决。
有些同学回答风险是LB的连接会爆掉,这个大家可能小看了LB设备的能力,在一定规模的场景下是完全没问题的,毕竟到达阿里这样规模的企业也很少。
CGLIB和Java的动态代理相比,具体有什么不同?
答:我自己也不是很懂,简单点讲是CGLIB可以代理类,这非常有助于像Spring AOP增强这样的场景的实现。
在基于Netty实现FrameDecoder时,下面两种代码的表现会有什么不同?
第一种
private void callDecode(...) {
List<Object> results = new ArrayList<Object>();
while (cumulation.readable()) {
int oldReaderIndex = cumulation.readerIndex();
Object frame = decode(context, channel, cumulation);
if (frame == null) {
if (oldReaderIndex == cumulation.readerIndex())
break;
else
continue;
}
else if (oldReaderIndex == cumulation.readerIndex()) {
throw new IllegalStateException( ".....");
}
results.add(frame);
}
if(results.size() > 0)
fireMessageReceived(context, remoteAddress, results);
}
第二种
private void callDecode(...) {
int oldReaderIndex = cumulation.readerIndex();
Object frame = decode(context, channel, cumulation);
if (frame != null)
fireMessageReceived(context, remoteAddress, frame);
}
答:第一种在并发量非常大时会有很大的优势,原因是当并发量非常大时,一次流事件里可能带了多个可处理的对象,之前也说了通常来说基于NIO的模型都是IO线程池 + 业务处理线程池的模式,怎么充分的让IO线程更加高效的并发决定了server的处理能力,第一种的处理方式可以有效减少IO线程池和业务处理线程池的上下文切换,从而提高IO线程的处理效率。
用Executors.newCachedThreadPool创建的线程池,在运行的过程中有可能产生的风险是?
答:这题比较简单,主要是在考察对自带的这些线程池API的掌握能力,有没有在用的时候仔细的去了解,newCachedThreadPool最大的风险就是可能会创建超多的线程,导致最后不能创建线程。
这道题稍微拓展开下可以顺带问问创建100个线程会耗费多少资源,一个Java进程能创建多少线程池是受什么限制?
new ThreadPoolExecutor(10,100,10,TimeUnit.MILLISECONDS,new LinkedBlockingQueue(10));一个这样创建的线程池,当已经有10个任务在运行时,第11个任务提交到此线程池执行的时候会发生什么,为什么?
答:之所以问这个题,是我自己以前刚学ThreadPoolExecutor的时候就进了这个坑,正常逻辑好像会觉得是当线程数还没到达max,就应该一直创建线程来处理并发的任务,但事实上ThreadPoolExecutor的实现却是当coreSize满了后,会先往Queue里面塞,只有Queue塞满了,max又还没到,才会去创建线程来处理,所以这道题当第11个任务提交时,会放到Queue里,所以对于用到的API,千万别自以为然,还是去翻翻它具体的实现比较好。
这道题拓展的更难一点可以是问问如果来设计一个类似ThreadPoolExecutor的类,大概怎么设计?
实现一个自定义的ThreadFactory的作用通常是?
答:通常的作用是给线程取名字,便于以后查问题,很多查过问题的同学应该都会发现,看到jstack出来后一堆看不出名字意义的线程是多么的崩溃。
除了用Object.wait和Object.notifyAll来实现线程间的交互外,你还会常用哪些来实现?
答:这题主要看对线程交互的掌握程度,方法非常的多,j.u.c里的不管是BlockingQueue的实现,还是各种类似CountDownLatch、CyclicBarrier,都可以用来实现线程的交互。
为什么ConcurrentHashMap可以在高并发的情况下比HashMap更为高效?
答:主要是ConcurrentHashMap在实现时采用的拆分锁,以及巧妙的使用final、volatile,网上有很多相关的解读的文章,这里就不展开了。
AtomicInteger、AtomicBoolean这些类之所以在高并发时高效,共同的原因是?
答:CAS,CAS是硬件级的原语,可以借助此实现Lock-free算法,网上解读的文章同样非常的多,这里也不展开了。
请合理的使用Queue来实现一个高并发的生产/消费的场景,给些核心的代码片段。
答:这道题主要是想看看对于各种Queue实现的掌握情况,例如通常可能会借助LinkedBlockingQueue来实现简单的生产/消费,那么像ArrayBlockingQueue、LinkedBlockingQueue的区别是什么,或者你自己实现一个Queue你会怎么做?
请实现让10个任务同时并发启动,给些代码片段。
答:借助CyclicBarrier实现,之所以让给代码片段,是看对代码的熟练程度,写代码写的多的话,是完全可以做到手写一段简单的编译不会出错,可运行的代码的。
同样,这种题目可以进一步的问,CyclicBarrier是怎么实现的。
在Java程序运行阶段,可以用什么命令行工具来查看当前Java程序的一些启动参数值,例如Heap Size等。
答:jinfo -flags,这个主要是看对Java一些查问题的工具的掌握情况,别的能做到类似效果的工具其实也都ok的。
用什么命令行工具可以查看运行的Java程序的GC状况,请具体写出命令行格式。
答:通常可以用jstat -gcutil [pid] [频率,例如多少毫秒一次] [多少次]来看目前的gc情况,如果已经打开了gc log,可以直接查看gc日志。
这种问题,稍微拓展下就可以看gc log通常怎么打开,具体的命令行参数,一段gc log的解读等。
用什么工具,可以在Java程序运行的情况下跟踪某个方法的执行时间,请求参数信息等,并请解释下工具实现的原理。
答:btrace,Arthas,主要借助JVM attach agent,ASM以及Instrumentation来动态的替换字节码,从而实现动态的对程序运行情况的跟踪。
这题拓展开,可以问会有什么限制,这个可以进一步了解对原理的掌握程度,也可以请实际的讲一个借助这些工具排查的case,来看看实践情况。
当一个Java程序接收请求,很长时间都没响应的话,通常你会怎么去排查这种问题?
答:这题非常开放,原因会很多,通常来说,需要先确认下请求是不是已经过来了,如果确认请求过来了的话,需要梳理下Java程序接收请求的处理过程,然后jstack看看对应的线程池的情况,看看是不是哪个环节卡住了。
同样,这种题展开的问法就是问讲一个实际的case。
Java进程突然消失了,你会怎么去排查这种问题?
答:这题也非常开放,通常来说,先去看看java进程的日志,例如有没有hs_err_[pid].log,如果有,看看日志里的内容,相应的来处理;另外可以看看有没有core dump,如果有,用gdb查查看;还可以用dmesg,看看是不是什么原因被os kill了;还有看运维系统的一些操作日志。
同样,这种题展开的问法就是问讲一个实际的case。
以下这段代码思路,你觉得在运行时可能会产生的风险是,应该如何改进?
public List<User> getUsers(String[] userIds){
// 从数据库查找符合userIds的user记录
// 将返回的记录组装为User对象,放入List并返回
}
答:很多同学回复了各种风险,挺好的,我自己回答的话,这题最大的风险是没有限制userIds的个数,可能会导致从数据库里查找大量的数据,并拼装为User对象,一方面可能会使得数据库扛不住,另一方面也有可能导致Java这边OOM了,类似的这样的代码曾经导致过非常严重的故障。
之所以问这个问题,是想提示大家在写代码的过程中要比较好的进行防御性编程。
以下两种代码,在运行时有什么不同?为什么?
第一种
private static final boolean isLoggerDebugEnabled = log.isDebugEnabled();
public void xx(User user){
if(isLoggerDebugEnabled){
log.debug("enter xx method, user id is: " + user.getId());
}
}
第二种
public void xx(User user){
log.debug("enter xx method, user id is: " + user.getId());
}
答:如果log的debug级别没开,第一种不会出现字符串拼接,第二种会出现,造成一些young区的内存浪费,所以第一种写法是更加好的,原因是第一种在Java运行时的编译过程中会直接优化掉,整段代码会彻底拿掉。
这题可以拓展开的问问会有哪些编译的优化技巧。
Java程序为什么通常在刚启动的时候会执行的比较慢,而处理了一些请求后会变快,AOT能带来什么帮助?
答:因为刚启动的时候Java还处于解释执行阶段,处理了一些请求后随着C1、C2编译的介入,会优化为机器码,并且借助各种运行时数据的高级优化(例如上面20题的那种),使得Java程序逐渐进入一个高速运行的状态,这也是Java这门语言很大的优势,使得程序员间的差距一定程度缩小了,以及不会出现太烂的Java程序。
AOT带来的帮助是在启动前就将一些代码直接编译为机器码,从而在启动瞬间就可以直接跳过解释执行,进入比较高效的执行。
这个话题确实有点大,后面我邀请下专业的JVM同学来写一篇,顺带给大家讲讲阿里的场景里是怎么尽可能去解决启动瞬间慢的这个问题的,包括大家也可以去了解下Azul的ReadyNow!,阿里提的JEP JWarmup。
Parallel GC、CMS GC、ZGC、Azul Pauseless GC最主要的不同是?背后的原理也请简单描述下?
答:这题比上题还大,先简单回答下,后面专题来写吧。
Parallel GC的Young区采用的是Mark-Copy算法,Old区采用的是Mark-Sweep-Compact来实现,Parallel执行,所以决定了Parallel GC在执行YGC、FGC时都会Stop-The-World,但完成GC的速度也会比较快。
CMS GC的Young区采用的也是Mark-Copy,Old区采用的是Concurrent Mark-Sweep,所以决定了CMS GC在对old区回收时造成的STW时间会更短,避免对应用产生太大的时延影响。
G1 GC采用了Garbage First算法,比较复杂,实现的好呢,理论上是会比CMS GC可以更高效,同时对应用的影响也很小。
ZGC、Azul Pauseless GC采用的算法很不一样,尤其是Pauseless GC,其中的很重要的一个技巧是通过增加Read Barrier来更好的识别对GC而言最关键的references变化的情况。
这题总的来说偏向于去问对GC非常熟的同学,这种题目拓展是非常大的,一方面可以是算法,另一方面可以是更具体的实现,例如GC是怎么实现STW的。
请写一段程序,让其运行时的表现为触发5次ygc,然后3次fgc,然后3次ygc,然后1次fgc,请给出代码以及启动参数。
答:这个我不写了,很早以前的文章里好像也有写过,需要基于对Java内存管理的分代、GC触发机制来设计相应的代码,这种题目变化就更多样了,一方面可以调整gc的触发形式,另一方面可以通过调整启动参数,gc的形式,来看是不是真的彻底掌握gc的知识点。
Go的Coroutine和Java的线程机制最主要的不同是?如果Java语言要透明的实现Coroutine,你觉得主要的难点是?
答:这题也很大,先简单回答,后面专题写。
Java的线程机制主要还是基于Native Thread,Go的Coroutine是进程里自己管理的一种"Thread",所以在高并发的场景下,Coroutine可以有效的降低比较重的native的线程上下文切换,从而来提高并发处理能力。
但目前很多的Java版本的Coroutine实现都不是很透明,非常多的限制,导致Java很难用上,比较难的是Java里有很多类似synchronized、各种锁、BIO等造成Native Thread直接block住的地方,怎么让这些地方在Coroutine环境里也透明的不block native thread,是关键问题,感兴趣的大家可以关注下Openjdk的Project Loom,以及阿里的AJDK Coroutine Wisp。
欢迎关注我的公众号hellojavacases,
聊聊编程能力的高级进阶,
聊聊系统设计,
聊聊技术方向,
聊聊职业生涯的发展。