写点什么

阿里巴巴中高级 java 面试题详解,吃透这 20 道面试题,offer 拿到你手软

用户头像
愚者
关注
发布于: 7 小时前



最近,有很多童鞋咨询我面试的问题,今天专门为大家整理了一些程序员面试中常见的问题,希望对童鞋们有帮助哦!

问题一多线程有什么用?

一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓”知其然知其所以然”,”会用”只是”知其然”,”为什么用”才是”知其所以然”,只有达到”知其然知其所以然”的程度才可以说是把一个知识点运用自如。OK,下面说说我对这个问题的看法:

(1)发挥多核 CPU 的优势

随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4 核、8 核甚至 16 核的也都不少见,如果是单线程的程序,那么在双核 CPU 上就浪费了 50%,在 4 核 CPU 上就浪费了 75%。单核 CPU 上所谓的”多线程”那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程”同时”运行罢了。多核 CPU 上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的。

(2)防止阻塞

从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核 CPU 上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。

(3)便于建模

这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

问题二Java 中如何获取到线程 dump 文件

死循环、死锁、阻塞、页面打开慢等问题,打线程 dump 是最好的解决问题的途径。所谓线程 dump 也就是线程堆栈,获取到线程堆栈有两步:

(1)获取到线程的 pid,可以通过使用 jps 命令,在 Linux 环境下还可以使用 ps -ef | grep java

(2)打印线程堆栈,可以通过使用 jstack pid 命令,在 Linux 环境下还可以使用 kill -3 pid

另外提一点,Thread 类提供了一个 getStackTrace()方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈,

问题三生产者消费者模型的作用是什么

这个问题很理论,但是很重要:

(1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用

(2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约

问题四:short s1=1;s1=s1+1;有什么错?short s1=1;s1+=1;有什么错?

解析:

面试题都是很变态的,要做好受虐的准备。

s1=s1+1 会出错,s1+1 是 int 型,不能将 int 赋值给 s1。需要显示转换,s1=(int)(s1+1),而 s1+=1 不会出错,至于原因,有人说和编译器的机制有关,需要看编译原理,话说编译原理什么的最讨厌了,就这样吧。


问题五怎么检测一个线程是否持有对象监视器

我也是在网上看到一道多线程面试题才知道有方法可以判断某个线程是否持有对象监视器:Thread 类提供了一个 holdsLock(Object obj)方法,当且仅当对象 obj 的监视器被某条线程持有的时候才会返回 true,注意这是一个 static 方法,这意味着“某条线程”指的是当前线程。

问题六:给我一个你最常见到的 runtimeexception。

解析

这个题也很常见,如果你答不出来,面试官会觉得你没有编程经验。

NullPointerException,空引用异常。说实话,中软的笔试题就有这个,很多人连题目意思都理解错了,压根没认出来 runtime exception 是指运行时异常。

问题七synchronized 和 ReentrantLock 的区别

synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:

(1)ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁

(2)ReentrantLock 可以获取各种锁的信息

(3)ReentrantLock 可以灵活地实现多路通知

问题八volatile 关键字的作用

一个非常重要的问题,是每个学习、应用多线程的 Java 程序员都必须掌握的。理解 volatile 关键字的作用的前提是要理解 Java 内存模型,这里就不讲 Java 内存模型了,可以参见第 31 点,volatile 关键字的作用主要有两个:

(1)多线程主要围绕可见性和原子性两个特性而展开,使用 volatile 关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到 volatile 变量,一定是最新的数据

(2)代码底层执行不像我们看到的高级语言—-Java 程序这么简单,它的执行是 Java 代码–>字节码–>根据字节码执行对应的 C/C++代码–>C/C++代码被编译成汇编语言–>和硬件电路交互,现实中,为了获取更好的性能 JVM 可能会对指令进行重排序,多线程下可能会出现一些意想不到的问题。使用 volatile 则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率

从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

问题九:什么是乐观锁和悲观锁

(1)乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

(2)悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,不管三七二十一,直接上了锁就操作资源了。

问题十Java 编程写一个会导致死锁的程序

第一次看到这个题目,觉得这是一个非常好的问题。很多人都知道死锁是怎么一回事儿:线程 A 和线程 B 相互等待对方持有的锁导致程序无限死循环下去。当然也仅限于此了,问一下怎么写一个死锁的程序就不知道了,这种情况说白了就是不懂什么是死锁,懂一个理论就完事儿了,实践中碰到死锁的问题基本上是看不出来的。

真正理解什么是死锁,这个问题其实不难,几个步骤:

(1)两个线程里面分别持有两个 Object 对象:lock1 和 lock2。这两个 lock 作为同步代码块的锁;

(2)线程 1 的 run()方法中同步代码块先获取 lock1 的对象锁,Thread.sleep(xxx),时间不需要太多,50 毫秒差不多了,然后接着获取 lock2 的对象锁。这么做主要是为了防止线程 1 启动一下子就连续获得了 lock1 和 lock2 两个对象的对象锁

(3)线程 2 的 run)(方法中同步代码块先获取 lock2 的对象锁,接着获取 lock1 的对象锁,当然这时 lock1 的对象锁已经被线程 1 锁持有,线程 2 肯定是要等待线程 1 释放 lock1 的对象锁的

这样,线程 1″睡觉”睡完,线程 2 已经获取了 lock2 的对象锁了,线程 1 此时尝试获取 lock2 的对象锁,便被阻塞,此时一个死锁就形成了。代码就不写了,占的篇幅有点多,Java 多线程 7:死锁这篇文章里面有,就是上面步骤的代码实现。


问题十一如果你提交任务时,线程池队列已满,这时会发生什么

如果你使用的 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说 ArrayBlockingQueue 的话,任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue 满了,则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy。

问题十二:hashmap 和 hashtable 的区别。

解析:

这个碰到的比较多。


问题十三什么是自旋

很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

问题十四:什么是 Java 内存模型

Java 内存模型定义了一种多线程访问 Java 内存的规范。Java 内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下 Java 内存模型的几部分内容:

(1)Java 内存模型将内存分为了主内存和工作内存。类的状态,也就是类之间共享的变量,是存储在主内存中的,每次 Java 线程用到这些主内存中的变量的时候,会读一次主内存中的变量,并让这些内存在自己的工作内存中有一份拷贝,运行自己线程代码的时候,用到这些变量,操作的都是自己工作内存中的那一份。在线程代码执行完毕之后,会将最新的值更新到主内存中去

(2)定义了几个原子操作,用于操作主内存和工作内存中的变量

(3)定义了 volatile 变量的使用规则

(4)happens-before,即先行发生原则,定义了操作 A 必然先行发生于操作 B 的一些规则,比如在同一个线程内控制流前面的代码一定先行发生于控制流后面的代码、一个释放锁 unlock 的动作一定先行发生于后面对于同一个锁进行锁定 lock 的动作等等,只要符合这些规则,则不需要额外做同步措施,如果某段代码不符合所有的 happens-before 规则,则这段代码一定是线程非安全的

问题十五:当一个线程进入一个对象的一个 synchronized 方法后,其它线程是否可进入此对象的其它方法?

解析:



问题十六:try{}里有一个 return 语句,那么紧跟在这个 try 后的 finally{}里的 code 会不会被执行,什么时候被执行,在 return 前还是后?

解析

return 前被执行,有程序为证:


结果为:

retrun

finally

return 1

问题十七单例模式的线程安全性

老生常谈的问题了,首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:

(1)饿汉式单例模式的写法:线程安全

(2)懒汉式单例模式的写法:非线程安全

(3)双检锁单例模式的写法:线程安全

问题十八Hashtable 的 size()方法中明明只有一条语句”return count”,为什么还要做同步?

这是我之前的一个困惑,不知道大家有没有想过这个问题。某个方法中如果有多条语句,并且都在操作同一个类变量,那么在多线程环境下不加锁,势必会引发线程安全问题,这很好理解,但是 size()方法明明只有一条语句,为什么还要加锁?

关于这个问题,在慢慢地工作、学习中,有了理解,主要原因有两点:

(1)同一时间只能有一条线程执行固定类的同步方法,但是对于类的非同步方法,可以多条线程同时访问。所以,这样就有问题了,可能线程 A 在执行 Hashtable 的 put 方法添加数据,线程 B 则可以正常调用 size()方法读取 Hashtable 中当前元素的个数,那读取到的值可能不是最新的,可能线程 A 添加了完了数据,但是没有对 size++,线程 B 就已经读取 size 了,那么对于线程 B 来说读取到的 size 一定是不准确的。而给 size()方法加了同步之后,意味着线程 B 调用 size()方法只有在线程 A 调用 put 方法完毕之后才可以调用,这样就保证了线程安全性

(2)CPU 执行代码,执行的不是 Java 代码,这点很关键,一定得记住。Java 代码最终是被翻译成汇编代码执行的,汇编代码才是真正可以和硬件电路交互的代码。即使你看到 Java 代码只有一行,甚至你看到 Java 代码编译之后生成的字节码也只有一行,也不意味着对于底层来说这句语句的操作只有一个。一句”return count”假设被翻译成了三句汇编语句执行,完全可能执行完第一句,线程就切换了。

问题十九:swtich 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 string 上?

解析:

switch 语句中的表达式只能是整数类型,即必须是 int、char 或者枚举类型数据。不能是 boolean 或浮点型,甚至其他类型的整数数据(byte,short 及 long)。

问题二十高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?

这是我在并发编程网上看到的一个问题,把这个问题放在最后一个,希望每个人都能看到并且思考一下,因为这个问题非常好、非常实际、非常专业。关于这个问题,个人看法是:

(1)高并发、任务执行时间短的业务,线程池线程数可以设置为 CPU 核数+1,减少线程上下文的切换

(2)并发不高、任务执行时间长的业务要区分开看:

  • 假如是业务时间长集中在 IO 操作上,也就是 IO 密集型的任务,因为 IO 操作并不占用 CPU,所以不要让所有的 CPU 闲下来,可以加大线程池中的线程数目,让 CPU 处理更多的业务

  • 假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换

(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。

读者福利:

由于篇幅原因,不能把所有的问题全部罗列出来,小编已经把这些问题整理成文档传入网盘,需要的朋友只需转发+关注点击此处私信【资料】即可免费领取哦!!!





用户头像

愚者

关注

还未添加个人签名 2021.07.22 加入

还未添加个人简介

评论

发布
暂无评论
阿里巴巴中高级java面试题详解,吃透这20道面试题,offer拿到你手软