面试经 | 资深 Java 程序员设计的 23 个问题(含答案)

640?wx_fmt=jpeg

Photo by Evan Dennis on Unsplash


自问自答,既是对知识体系的一种扫盲,也是和其他程序员交流的一种方式,通过这种方式来交换见解,也许还能成为彼此敬佩的人。例如,在面试这个场景中,当面试官提出一个开放性问题,你的答案和面试官心中的答案惊人的一致,相信两位是有着很相似的实践经历和理论体系。


Bug 易写,知己难觅。


本文转载至 「Hello Java」,作者根据 Java编程进阶的思路 设计了23个问题,并给出了自己的答案。问题从来不是千篇一律,答案也不是固定不变,但问题背后的知识点值得大家去学习和掌握。


1、基于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的根本不同。


2、一个典型的客户端集群->LB->服务端集群这样的结构中,如客户端采用连接池,长连接的方式,这种设计你觉得可能会出现什么问题?如果客户端采用的是单个长连接的方式呢?如果有问题,你觉得应该怎么解决?


答:这题比较开放,会有各种回答,这里讲下当年我自己在这里碰到的一个很大的坑,血泪教训,也是我认为这样的结构里最大的风险。


客户端采用连接池,长连接,通过LB去连接后端的服务端集群,在这样的结构下,由于客户端看到的其实只有LB提供出来的vip,会导致的一个严重问题是服务端集群出现不均衡的现象,尤其是在服务端集群发布重启等情况下,最恶劣的情况下搞不好会导致服务端集群压根就没法启动了。


客户端采用单个长连接,其实也会碰到同样的问题。


当年,最早的时候我们的系统就是采用这样的方式,导致出现过严重故障,服务端发布的时候启动不了,因为发布的分批导致了压力压在了少数的机器上,容量不够就崩了,那次处理的时候只好先把vip disable掉,服务端集群全部发布好了,再把vip enable,才勉强扛过去了。


像这种问题,解决起来很麻烦,例如让长连接到达一定条件下就断开下,但这样长连接的作用就降低了,比较根本的解决方法是在这样的场景里把中间的LB去掉,换成类似通过服务注册/发现的机制来解决。


有些同学回答风险是LB的连接会爆掉,这个大家可能小看了LB设备的能力,在一定规模的场景下是完全没问题的,毕竟到达阿里这样规模的企业也很少。


3、CGLIB和Java的动态代理相比,具体有什么不同?


答:我自己也不是很懂,简单点讲是CGLIB可以代理类,这非常有助于像Spring AOP增强这样的场景的实现。


4、在基于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线程的处理效率。


5、用Executors.newCachedThreadPool创建的线程池,在运行的过程中有可能产生的风险是?


答:这题比较简单,主要是在考察对自带的这些线程池API的掌握能力,有没有在用的时候仔细的去了解,newCachedThreadPool最大的风险就是可能会创建超多的线程,导致最后不能创建线程。


这道题稍微拓展开下可以顺带问问创建100个线程会耗费多少资源,一个Java进程能创建多少线程池是受什么限制。


6、new ThreadPoolExecutor(10,100,10,TimeUnit.MILLISECONDS,new LinkedBlockingQueue(10));

一个这样创建的线程池,当已经有10个任务在运行时,第11个任务提交到此线程池执行的时候会发生什么,为什么?


答:之所以问这个题,是以前刚学ThreadPoolExecutor的时候就进了这个坑,正常逻辑好像会觉得是当线程数还没到达max,就应该一直创建线程来处理并发的任务。但事实上ThreadPoolExecutor的实现却是当coreSize满了后,会先往Queue里面塞,只有Queue塞满了,max又还没到,才会去创建线程来处理。所以这道题当第11个任务提交时,会放到Queue里,所以对于用到的API,千万别自以为然,还是去翻翻它具体的实现比较好。


这道题拓展的更难一点可以是问问如果来设计一个类似ThreadPoolExecutor的类,大概怎么设计?


7、实现一个自定义的ThreadFactory的作用通常是?


答:通常的作用是给线程取名字,便于以后查问题,很多查过问题的同学应该都会发现,看到jstack出来后一堆看不出名字意义的线程是多么的崩溃。


8、除了用Object.wait和Object.notifyAll来实现线程间的交互外,你还会常用哪些来实现?


答:这题主要看对线程交互的掌握程度,方法非常的多,j.u.c里的不管是BlockingQueue的实现,还是各种类似CountDownLatch、CyclicBarrier,都可以用来实现线程的交互。


9、为什么ConcurrentHashMap可以在高并发的情况下比HashMap更为高效?


答:主要是ConcurrentHashMap在实现时采用的拆分锁,以及巧妙的使用final、volatile,网上有很多相关的解读的文章,这里就不展开了。


10、AtomicInteger、AtomicBoolean这些类之所以在高并发时高效,共同的原因是?


答:CAS,CAS是硬件级的原语,可以借助此实现Lock-free算法,网上解读的文章同样非常的多,这里也不展开了。


11、请合理的使用Queue来实现一个高并发的生产/消费的场景,给些核心的代码片段。


答:这道题主要是想看看对于各种Queue实现的掌握情况,例如通常可能会借助LinkedBlockingQueue来实现简单的生产/消费,那么像ArrayBlockingQueue、LinkedBlockingQueue的区别是什么,或者你自己实现一个Queue你会怎么做?


12、请实现让10个任务同时并发启动,给些代码片段。


答:借助CyclicBarrier实现,之所以让给代码片段,是看对代码的熟练程度,写代码写的多的话,是完全可以做到手写一段简单的编译不会出错,可运行的代码的。


同样,这种题目可以进一步的问,CyclicBarrier是怎么实现的。


13、在Java程序运行阶段,可以用什么命令行工具来查看当前Java程序的一些启动参数值,例如Heap Size等。


答:jinfo -flags,这个主要是看对Java一些查问题的工具的掌握情况,别的能做到类似效果的工具其实也都ok的。


14、用什么命令行工具可以查看运行的Java程序的GC状况,请具体写出命令行格式。


答:通常可以用jstat -gcutil [pid] [频率,例如多少毫秒一次] [多少次]来看目前的GC情况,如果已经打开了GC log,可以直接查看GC日志。


这种问题,稍微拓展下就可以看GC log通常怎么打开,具体的命令行参数,一段GC log的解读等。


15、用什么工具,可以在Java程序运行的情况下跟踪某个方法的执行时间,请求参数信息等,并请解释下工具实现的原理。


答:BTrace和Arthas,主要借助JVM attach agent,ASM以及Instrumentation来动态的替换字节码,从而实现动态的对程序运行情况的跟踪。


这题拓展开,可以问会有什么限制,这个可以进一步了解对原理的掌握程度,也可以请实际的讲一个借助这些工具排查的case,来看看实践情况。


16、当一个Java程序接收请求,很长时间都没响应的话,通常你会怎么去排查这种问题?


答:这题非常开放,原因会很多,通常来说,需要先确认下请求是不是已经过来了。如果确认请求过来了的话,需要梳理下Java程序接收请求的处理过程,然后jstack看看对应的线程池的情况,看看是不是哪个环节卡住了。


同样,这种题展开的问法就是问一个实际的case。


17、Java进程突然消失了,你会怎么去排查这种问题?


答:这题也非常开放,通常来说,先去看看Java进程的日志,例如有没有hs_err_[pid].log,如果有,看看日志里的内容,相应的来处理;另外可以看看有没有core dump,如果有,用GDB查查看;还可以用dmesg,看看是不是什么原因被OS kill了;还有看运维系统的一些操作日志。


同样,这种题展开的问法就是问一个实际的case。


18、以下这段代码思路,你觉得在运行时可能会产生的风险是,应该如何改进?

public List<User> getUsers(String[] userIds){   // 从数据库查找符合userIds的user记录  //  将返回的记录组装为User对象,放入List并返回}

答:这题最大的风险是没有限制userIds的个数,可能会导致从数据库里查找大量的数据,并拼装为User对象。一方面可能会使得数据库扛不住,另一方面也有可能导致Java这边OOM了,类似的这样的代码曾经导致过非常严重的故障。


之所以问这个问题,是想提示大家在写代码的过程中要比较好的进行防御性编程。


19、以下两种代码,在运行时有什么不同?为什么?


第一种

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运行时的编译过程中会直接优化掉,整段代码会彻底拿掉。


这题可以拓展开的问问会有哪些编译的优化技巧。


20、Java程序为什么通常在刚启动的时候会执行的比较慢,而处理了一些请求后会变快,AOT能带来什么帮助?


答:因为刚启动的时候Java还处于解释执行阶段,处理了一些请求后随着C1、C2编译的介入,会优化为机器码,并且借助各种运行时数据的高级优化,使得Java程序逐渐进入一个高速运行的状态,这也是Java这门语言很大的优势,使得程序员间的差距一定程度缩小了,以及不会出现太烂的Java程序。


AOT带来的帮助是在启动前就将一些代码直接编译为机器码,从而在启动瞬间就可以直接跳过解释执行,进入比较高效的执行。


这个话题确实有点大,后面有机会邀请专业的JVM同学来写一篇。顺带给大家讲讲阿里的场景里是怎么尽可能去解决启动瞬间慢的这个问题的,包括大家也可以去了解下Azul的ReadyNow!,阿里提的JEP JWarmup。


21、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的。


22、请写一段程序,让其运行时的表现为触发5次YGC,然后3次FGC,然后3次YGC,然后1次FGC,请给出代码以及启动参数。


答:这个我不写了,很早以前的文章里好像也有写过,需要基于对Java内存管理的分代、GC触发机制来设计相应的代码,这种题目变化就更多样了,一方面可以调整GC的触发形式,另一方面可以通过调整启动参数,GC的形式,来看是不是真的彻底掌握GC的知识点。


23、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。


说明:本文中的问题并非阿里巴巴中间件团队的官方面试题,仅作学习交流使用。


/ 除了面试经, 还有一份 画架构图 的经验 /


640?wx_fmt=jpeg

Photo by Markus Spiske on Unsplash


©每周一推

第一时间获得下期分享



640?wx_fmt=jpeg

Tips:

# 点下“好看”❤️

# 然后,公众号对话框内发送“帽衫”,试试手气??

# 本期奖品面向开发者的「云栖社区」赞助

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值