下面的所有面试题均参考自CodeSheep的微信公众号
-
多线程有什么用?
- 发挥多核CPU的优势
现在的电脑有双核、4核、8核之类的,如果是单线程的程序,在 双核系统上就浪费了50%的CPU,在4核上就浪费了75%,所以说多线程的设计是为了充分利用多核CPU。 - 防止数据的阻塞
单线程程序执行不一定比多线程程序慢,相反,多线程程序在单核CPU上由于频繁的执行上下文的切换,反而导致程序执行效率低,那为什么还要设计成多线程呢?为了防止数据的阻塞,一个下载的操作,如果其中一条线程阻塞了,还有别的线程,照样可以完成任务。
- 发挥多核CPU的优势
-
start()方法和run()方法的区别?
- 只有调用了start方法,才有表现出多线程的特性。只是调用了run(),就只是个类中的普通方法,程序还是至少而下串行的。
-
Runnable接口和Callable接口的区别?
- Runnable接口没有返回值,Callable接口有返回值,而且后者可以配合FutureTask<>来获取线程运行时的状态信息,决定是否取消线程,线程是否运行结束。
-
volatile关键字的作用?
- 它是一个轻量级的同步机制,可以禁止编译器对于指令的重排序的优化,从而保证共享变量的可见性,即被volatile修饰的变量每次被线程读取数据的时候,会保证一定是内存中最新的数据,当然由于禁止了指令的重排序,在一定程度上也降低了程序的执行效率。
-
什么是线程安全?
- 如果是多线程的程序的运行结果能够和在单线程程序上运行的结果一致,那么这就是线程安全的。
-
Java中如何获取到线程dump文件?
- 死循环、死锁、阻塞、页面打开慢等问题,打线程dump是最好的解决问题的途径。所谓线程dump也就是线程堆栈,获取到线程堆栈有两步:
- 获取到线程的pid,可以通过使用jps命令,在Linux环境下还可以使用ps -ef | grep java;
- 打印线程堆栈,可以通过使用jstack pid命令,在Linux环境下还可以使用kill -3 pid;
- 另外提一点,Thread类提供了一个getStackTrace()方法也可以用于获取线程堆栈;
-
一个线程如果出现了运行时异常会怎么样?
- 如果没有对异常进行捕获的话,就会抛出异常,中断程序 ,另外如果某个线程持有锁对象,也是释放锁。
-
如何在两个线程之间共享数据?
- 线程的等待唤醒机制就可以共享数据,两个线程之间使用同一个锁对象,然后使用wait / notify、notifyAll等方法。
-
sleep方法和wait方法有什么区别?
- sleep方法和wait方法都是放弃CPU的执行权,只不过sleep方法不会释放当前的锁,而wait方法会释放当前的锁。
-
ThreadLocal有什么用?
- 被ThreadLocal维护的数据对于每个线程而言都是单独的一份副本,不会进行共享,也就不会有线程安全问题。
-
为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用?
- 这是JDK的强制规定。这些方法在调用前都必须获得锁对象。
-
为什么要使用线程池?
- 避免频繁地创建线程和销毁线程,而且我补充一下,对于并发高,任务持续时间短的业务,应该使用线程池,这样就避免了系统大量的时间消耗在了创建和销毁这个工作上。
-
synchronized和ReentrantLock的区别?
- synchronized是Java中的关键字,而ReentrantLock是一个类,所以后者具备更多的灵活性,可以被继承、可以有方法、可以有成员属性,主要区别有以下三点:
- ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁;
- ReentrantLock可以获取各种锁的信息;
- ReentrantLock可以灵活地实现多路通知;
-
ReadWriteLock是什么?
- 读写锁,只有在发生“写”的情况下才独占锁,而“读”的这个情况下共享锁,因为“读”的情况不会产生线程安全问题,没必要加锁,提高了读写性能。
-
Linux环境下如何查找哪个线程使用CPU最长?
- ps -ef | grep java
- top -H -p pid,顺序不能改变
-
Java编程写一个会导致死锁的程序?
- 死锁:两个线程双方互相等待对方释放锁而陷入无限等待状态。
步骤:
-
(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的对象锁,便被阻塞,此时一个死锁就形成了。
-
怎么唤醒一个阻塞的线程?
- 中断线程,并抛出 InterruptedException来唤醒它。
-
不可变对象对多线程有什么帮助?
- 因为本身这些类被final修饰,即使不设置额外的同步措施,他们的变量也不会被修改,所以提高了代码执行效率。
-
什么是多线程的上下文切换?
- 从当前这个正在运行的线程切换到某一个就绪并等待获取CPU执行权的线程。 我觉的一个就绪并等待获取CPU执行权的线程就是一个锁阻塞线程(Blocked)
-
如果你提交任务时,线程池队列已满,这时会发生什么?
- 这里我就说下有界队列,反正无界队列容量很大,可以接近认为能够存放无限任务。当有界队列满后,会根据MaximunPoolSize最大线程池容量再增加几条线程来参与任务的处理,如果还是满了,则默认采取abortPolicy拒绝策略并抛出拒绝执行的异常。
-
Java中用到的线程调度算法是什么?
- 抢占式。一个线程使用完时间片后,CPU会根据线程的优先级、饥饿程度等参数来计算下一个要被执行的线程。
-
Thread.sleep(0)的作用是什么?
- 让出当前CPU的使用权,让CPU去执行其他的线程,有个注意事项是sleep方法确实是持有着锁,但不意味着CPU干不了其他事,因为其他线程又依赖你这把锁。
-
单例模式的线程安全性?
- 单例模式意味着一个系统内存中只存在一个对象的实例化,也就是在多线程的情况下只允许创建一个对象在内存中,如何保证只创建一个这样的实例化在内存中呢,你别说是说单例模式,到时候没控制好多线程导致new了多个实例。 下面说几个方法:
- 饿汉式:线程安全
- 懒汉式:线程不安全(反例,只是创建单例模式的一个手段)
- double check双重检查:线程安全
-
同步方法和同步块,哪个是更好的选择?
- 一般来说,同步的范围越小越好,如果一大段代码都被同步方法包括了,那么执行效率肯定低,而且没必要这样, 真正需要原子性操作的就是某一段代码,所以同步块更好。
- 还有个就是锁粗化的特例,这种方法就是把同步范围变大。这是有用的,比方说StringBuffer,它是一个线程安全的类,自然最常用的append()方法是一个同步方法,我们写代码的时候会反复append字符串,这意味着要进行反复的加锁->解锁,这对性能不利,因为这意味着Java虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此Java虚拟机会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,这样就减少了加锁–>解锁的次数,有效地提升了代码执行的效率。
-
高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
-
个人看法是:
-
(1)高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
-
(2)并发不高、任务执行时间长的业务要区分开看:
-
a)假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
-
b)假如是业务时间长集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)一样吧,线程池中的线程数设置得少一些,减少线程上下文的切换
-
-
(3)并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
-
好的,到这里基本上我参考的面试题就总结完了,我觉得对于初学者而言,掌握多线程这块知识点还是有一段路要走的,不过很多为了面试没办法,如果以后有时间可以买本书从最基本的知识点来学这块,那时候就是真正的理解而不用担心面试官会在每个问题后面多加个“为什么”,现在别急,不用看了这么多知识点害怕,要知道这也是别人很长时间积累下来的。