阿里毕玄:Java编程能力测试-我的回答

简介: 这些题目我是完全根据Java编程进阶的思路来设计的,很多同学可能觉得背背答案就能去应对一些面试,先不说为什么这个是很难靠背的,事实上只要面试官稍有点水平,通常在题目的问法上就会有很多的变化,所以我觉得发这些题目,最希望的是大家可以认真的去学习和掌握背后的知识点,这样才能以不变应万变。

作者:毕玄   
文章来源:微信公众号HelloJava

作为一个好几年没打开IDE的人来填下自己挖的坑,回答下上篇文章的题目,这应该也算能充分说明一旦真正的掌握了,其实是很难忘的,当然,另一方面貌似也说明这些年了Java语言层面进步是有限的。

这些题目我是完全根据Java编程进阶的思路来设计的,很多同学可能觉得背背答案就能去应对一些面试,先不说为什么这个是很难靠背的,事实上只要面试官稍有点水平,通常在题目的问法上就会有很多的变化,很容易判断出面试者的情况,而且面试官应该是根据面试者本身的背景来问问题,而不是千篇一律,所以我觉得发这些题目,最希望的是大家可以认真的去学习和掌握背后的知识点,这样才能以不变应万变。

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.通常来说基于NIO实现的Server端,会用多少个线程去处理IO事件,为什么?

答:见1里面的回答。

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

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

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

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

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

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

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

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

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

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

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

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

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

7.new ThreadPoolExecutor(10,100,10,TimeUnit.MILLISECONDS,new LinkedBlockingQueue(10));一个这样创建的线程池,当已经有10个任务在运行时,第11个任务提交到此线程池执行的时候会发生什么,为什么?

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

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

8.实现一个自定义的ThreadFactory的作用通常是?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

public List getUsers(String[] userIds){

   // 从数据库查找符合userIds的user记录

  //  将返回的记录组装为User对象,放入List并返回

}

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

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

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

第一种

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

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

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

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

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

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

22.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的。

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

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

24.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。

相关实践学习
部署高可用架构
本场景主要介绍如何使用云服务器ECS、负载均衡SLB、云数据库RDS和数据传输服务产品来部署多可用区高可用架构。
负载均衡入门与产品使用指南
负载均衡(Server Load Balancer)是对多台云服务器进行流量分发的负载均衡服务,可以通过流量分发扩展应用系统对外的服务能力,通过消除单点故障提升应用系统的可用性。 本课程主要介绍负载均衡的相关技术以及阿里云负载均衡产品的使用方法。
相关文章
|
4天前
|
IDE Java 物联网
《Java 简易速速上手小册》第1章:Java 编程基础(2024 最新版)
《Java 简易速速上手小册》第1章:Java 编程基础(2024 最新版)
11 0
|
Java 测试技术
Java 中的单元测试和集成测试策略
【4月更文挑战第19天】本文探讨了Java开发中的单元测试和集成测试。单元测试专注于单一类或方法的功能验证,使用测试框架如JUnit,强调独立性、高覆盖率和及时更新测试用例。集成测试则验证模块间交互,通过逐步集成或模拟对象来检测系统整体功能。两者相辅相成,确保软件质量和降低修复成本。
|
4天前
|
前端开发 JavaScript Java
《手把手教你》系列技巧篇(二十五)-java+ selenium自动化测试-FluentWait(详细教程)
【4月更文挑战第17天】其实今天介绍也讲解的也是一种等待的方法,有些童鞋或者小伙伴们会问宏哥,这也是一种等待方法,为什么不在上一篇文章中竹筒倒豆子一股脑的全部说完,反而又在这里单独写了一篇。那是因为这个比较重要,所以宏哥专门为她量身定制了一篇。FluentWait是Selenium中功能强大的一种等待方式,翻译成中文是流畅等待的意思。在介绍FluentWait之前,我们来讨论下为什么需要设置等待,我们前面介绍了隐式等待和显式等待。
27 3
|
4天前
|
安全 Java 开发者
Java并发编程:深入理解Synchronized关键字
【4月更文挑战第19天】 在Java多线程编程中,为了确保数据的一致性和线程安全,我们经常需要使用到同步机制。其中,`synchronized`关键字是最为常见的一种方式,它能够保证在同一时刻只有一个线程可以访问某个对象的特定代码段。本文将深入探讨`synchronized`关键字的原理、用法以及性能影响,并通过具体示例来展示如何在Java程序中有效地应用这一技术。
|
5天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
6天前
|
缓存 分布式计算 监控
Java并发编程:深入理解线程池
【4月更文挑战第17天】在Java并发编程中,线程池是一种非常重要的技术,它可以有效地管理和控制线程的执行,提高系统的性能和稳定性。本文将深入探讨Java线程池的工作原理,使用方法以及在实际开发中的应用场景,帮助读者更好地理解和使用Java线程池。
|
6天前
|
Java API 数据库
深研Java异步编程:CompletableFuture与反应式编程范式的融合实践
【4月更文挑战第17天】本文探讨了Java中的CompletableFuture和反应式编程在提升异步编程体验上的作用。CompletableFuture作为Java 8引入的Future扩展,提供了一套流畅的链式API,简化异步操作,如示例所示的非阻塞数据库查询。反应式编程则关注数据流和变化传播,通过Reactor等框架实现高度响应的异步处理。两者结合,如将CompletableFuture转换为Mono或Flux,可以兼顾灵活性和资源管理,适应现代高并发环境的需求。开发者可按需选择和整合这两种技术,优化系统性能和响应能力。
|
6天前
|
Java 测试技术 定位技术
《手把手教你》系列技巧篇(二十三)-java+ selenium自动化测试-webdriver处理浏览器多窗口切换下卷(详细教程)
【4月更文挑战第15天】本文介绍了如何使用Selenium进行浏览器窗口切换以操作不同页面元素。首先,获取浏览器窗口句柄有两种方法:获取所有窗口句柄的集合和获取当前窗口句柄。然后,通过`switchTo().window()`方法切换到目标窗口句柄。在项目实战部分,给出了一个示例,展示了在百度首页、新闻页面和地图页面之间切换并输入文字的操作。最后,文章还探讨了在某些情况下可能出现的问题,并提供了一个简单的本地HTML页面示例来演示窗口切换的正确操作。
27 0
|
7天前
|
缓存 监控 Java
Java并发编程:线程池与任务调度
【4月更文挑战第16天】Java并发编程中,线程池和任务调度是核心概念,能提升系统性能和响应速度。线程池通过重用线程减少创建销毁开销,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。任务调度允许立即或延迟执行任务,具有灵活性。最佳实践包括合理配置线程池大小、避免过度使用线程、及时关闭线程池和处理异常。掌握这些能有效管理并发任务,避免性能瓶颈。
|
8天前
|
设计模式 运维 安全
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第15天】在Java开发中,多线程编程是提升应用程序性能和响应能力的关键手段。然而,它伴随着诸多挑战,尤其是在保证线程安全的同时如何避免性能瓶颈。本文将探讨Java并发编程的核心概念,包括同步机制、锁优化、线程池使用以及并发集合等,旨在为开发者提供实用的线程安全策略和性能优化技巧。通过实例分析和最佳实践的分享,我们的目标是帮助读者构建既高效又可靠的多线程应用。

热门文章

最新文章