面试,难还是不难?取决于面试者的底蕴(气场+技能)、心态和认知及沟通技巧。面试其实可以理解为一场聊天和谈判,在这过程中有心理、思想上的碰撞和博弈。其实你只需要搞清楚一个逻辑:“面试官为什么会这样问?他希望听到什么答案?”然后针对性的准备和回答就行了,无他。
不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题。Java语言一个重要的特点就是内置了对并发的支持,让Java大受企业和程序员的欢迎。大多数待遇丰厚的Java开发职位都要求开发者精通多线程技术并且有丰富的Java程序开发、调试、优化经验,所以线程相关的问题在面试中经常会被提到。 在典型的Java面试中,面试官会从线程的基本概念问起
如:为什么你需要使用线程,如何创建线程,用什么方式创建线程比较好(比如:继承thread类还是调用Runnable接口),然后逐渐问到并发问题像在Java并发编程的过程中遇到了什么挑战,Java内存模型,JDK1.5引入了哪些更高阶的并发工具,并发编程常用的设计模式,经典多线程问题如生产者消费者,哲学家就餐,读写器或者简单的有界缓冲区问题。仅仅知道线程的基本概念是远远不够的,你必须知道如何处理死锁,竞态条件,内存冲突和线程安全等并发问题。掌握了这些技巧,你就可以轻松应对多线程和并发面试了。 许多Java程序员在面试前才会去看面试题,这很正常。
因为收集面试题和练习很花时间,所以我从许多面试者那里收集了Java多线程和并发相关的50个热门问题。
1、什么是线程?
2、什么是线程安全和线程不安全?
3、什么是自旋锁?
4、什么是Java内存模型?
5、什么是CAS?
6、什么是乐观锁和悲观锁?
7、什么是AQS?
8、什么是原子操作?在Java Concurrency API中有哪些原子类(atomic classes)?
9、什么是Executors框架?
10、什么是阻塞队列?如何使用阻塞队列来实现生产者-消费者模型?
11、什么是Callable和Future?
12、什么是FutureTask?
13、什么是同步容器和并发容器的实现?
14、什么是多线程?优缺点?
15、什么是多线程的上下文切换?
16、ThreadLocal的设计理念与作用?
17、ThreadPool(线程池)用法与优势?
18、Concurrent包里的其他东西:ArrayBlockingQueue、CountDownLatch等等。
19、synchronized和ReentrantLock的区别?
20、Semaphore有什么作用?
21、Java Concurrency API中的Lock接口(Lock interface)是什么?对比同步它有什么优势?
22、Hashtable的size()方法中明明只有一条语句”return count”,为什么还要做同步?
23、ConcurrentHashMap的并发度是什么?
24、ReentrantReadWriteLock读写锁的使用?
25、CyclicBarrier和CountDownLatch的用法及区别?
26、LockSupport工具?
27、Condition接口及其实现原理?
28、Fork/Join框架的理解?
29、wait()和sleep()的区别?
30、线程的五个状态(五种状态,创建、就绪、运行、阻塞和死亡)?
31、start()方法和run()方法的区别?
32、Runnable接口和Callable接口的区别?
33、volatile关键字的作用?
34、Java中如何获取到线程dump文件?
35、线程和进程有什么区别?
36、线程实现的方式有几种(四种)?
37、高并发、任务执行时间短的业务怎样使用线程池?并发不高、任务执行时间长的业务怎样使用线程池?并发高、业务执行时间长的业务怎样使用线程池?
38、如果你提交任务时,线程池队列已满,这时会发生什么?
39、锁的等级:方法锁、对象锁、类锁?
40、如果同步块内的线程抛出异常会发生什么?
41、并发编程(concurrency)并行编程(parallellism)有什么区别?
42、如何保证多线程下 i++ 结果正确?
43、一个线程如果出现了运行时异常会怎么样?
44、如何在两个线程之间共享数据?
45、生产者消费者模型的作用是什么?
46、怎么唤醒一个阻塞的线程?
47、Java中用到的线程调度算法是什么
48、单例模式的线程安全性?
49、线程类的构造方法、静态块是被哪个线程调用的?
50、同步方法和同步块,哪个是更好的选择?
51、如何检测死锁?怎么预防死锁?
以下是前五道题的答案,需要剩余面试题答案的,关注我简信回复Java线程面试获取。
什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位,可以使用多线程对进行运算提速。
比如,如果一个线程完成一个任务要100毫秒,那么用十个线程完成改任务只需10毫秒
什么是线程安全和线程不安全?
通俗的说:加锁的就是是线程安全的,不加锁的就是是线程不安全的
线程安全
线程安全: 就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问,直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
一个线程安全的计数器类的同一个实例对象在被多个线程使用的情况下也不会出现计算失误。很显然你可以将集合类分成两组,线程安全和非线程安全的。 Vector 是用同步方法来实现线程安全的, 而和它相似的ArrayList不是线程安全的。
线程不安全
线程不安全:就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。 若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
什么是自旋锁?
基本概念
自旋锁是SMP架构中的一种low-level的同步机制。
当线程A想要获取一把自选锁而该锁又被其它线程锁持有时,线程A会在一个循环中自选以检测锁是不是已经可用了。
自选锁需要注意:
[if !supportLists]§ [endif]由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
[if !supportLists]§ [endif]持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。
实现自旋锁
参考
https://segmentfault.com/q/1010000000530936
一个简单的while就可以满足你的要求。
目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大。
[if !supportLists]1. [endif]public class MyWaitNotify3{
[if !supportLists]2. [endif]
[if !supportLists]3. [endif] MonitorObjectmyMonitorObject = new MonitorObject();
[if !supportLists]4. [endif] booleanwasSignalled = false;
[if !supportLists]5. [endif]
[if !supportLists]6. [endif] public void doWait(){
[if !supportLists]7. [endif] synchronized(myMonitorObject){
[if !supportLists]8. [endif] while(!wasSignalled){
[if !supportLists]9. [endif] try{
[if !supportLists]10. [endif] myMonitorObject.wait();
[if !supportLists]11. [endif] }catch(InterruptedException e){...}
[if !supportLists]12. [endif] }
[if !supportLists]13. [endif] //clear signal and continue running.
[if !supportLists]14. [endif] wasSignalled =false;
[if !supportLists]15. [endif] }
[if !supportLists]16. [endif] }
[if !supportLists]17. [endif]
[if !supportLists]18. [endif] public voiddoNotify(){
[if !supportLists]19. [endif] synchronized(myMonitorObject){
[if !supportLists]20. [endif] wasSignalled =true;
[if !supportLists]21. [endif] myMonitorObject.notify();
[if !supportLists]22. [endif] }
[if !supportLists]23. [endif] }
[if !supportLists]24. [endif]}
什么是Java内存模型?
Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。它描述了“程序中的变量“ 和 ”从内存或者寄存器获取或存储它们的底层细节”之间的关系。Java内存模型通过使用各种各样的硬件和编译器的优化来正确实现以上事情。
Java包含了几个语言级别的关键字,包括:volatile, final以及synchronized,目的是为了帮助程序员向编译器描述一个程序的并发需求。Java内存模型定义了volatile和synchronized的行为,更重要的是保证了同步的java程序在所有的处理器架构下面都能正确的运行。
“一个线程的写操作对其他线程可见”这个问题是因为编译器对代码进行重排序导致的。例如,只要代码移动不会改变程序的语义,当编译器认为程序中移动一个写操作到后面会更有效的时候,编译器就会对代码进行移动。如果编译器推迟执行一个操作,其他线程可能在这个操作执行完之前都不会看到该操作的结果,这反映了缓存的影响。
此外,写入内存的操作能够被移动到程序里更前的时候。在这种情况下,其他的线程在程序中可能看到一个比它实际发生更早的写操作。所有的这些灵活性的设计是为了通过给编译器,运行时或硬件灵活性使其能在最佳顺序的情况下来执行操作。在内存模型的限定之内,我们能够获取到更高的性能。
看下面代码展示的一个简单例子:
[if !supportLists]1. [endif]ClassReordering {
[if !supportLists]2. [endif]
[if !supportLists]3. [endif] int x = 0, y = 0;
[if !supportLists]4. [endif]
[if !supportLists]5. [endif] public void writer()
{
[if !supportLists]6. [endif] x =1;
[if !supportLists]7. [endif] y =2;
[if !supportLists]8. [endif] }
[if !supportLists]9. [endif]
[if !supportLists]10. [endif] public void reader()
{
[if !supportLists]11. [endif] int r1 = y;
[if !supportLists]12. [endif] int r2 = x;
[if !supportLists]13. [endif] }
[if !supportLists]14. [endif]}
让我们看在两个并发线程中执行这段代码,读取Y变量将会得到2这个值。因为这个写入比写到X变量更晚一些,程序员可能认为读取X变量将肯定会得到1。但是,写入操作可能被重排序过。如果重排序发生了,那么,就能发生对Y变量的写入操作,读取两个变量的操作紧随其后,而且写入到X这个操作能发生。程序的结果可能是r1变量的值是2,但是r2变量的值为0。
但是面试官,有时候不这么认为,认为就是JVM内存结构
JVM内存结构主要有三大块:堆内存、方法区和栈。
堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。
JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)
java堆(Java Heap)
[if !supportLists]§ [endif]可通过参数 -Xms 和-Xmx设置
[if !supportLists]1. [endif]Java堆是被所有线程共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建。
[if !supportLists]2. [endif]Java堆唯一的目的是存放对象实例,几乎所有的对象实例和数组都在这里。
[if !supportLists]3. [endif]Java堆为了便于更好的回收和分配内存,可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor区。
[if !supportLists]§ [endif]新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1。
[if !supportLists]§ [endif]老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
[if !supportLists]1. [endif]Survivor空间等Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可(就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的)。
据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
java虚拟机栈(stack)
可通过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置
1.Java虚拟机栈是线程私有的,它的生命周期与线程相同。
[if !supportLists]1. [endif]每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
[if !supportLists]2. [endif]虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都会创建一个栈帧,用于存储 局部变量表、操作数栈、动态链接、方法出口等信息。
[if !supportLists]§ [endif]局部变量表:32位变量槽,存放了编译期可知的各种基本数据类型、对象引用、returnAddress类型。
[if !supportLists]§ [endif]操作数栈:基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。
[if !supportLists]§ [endif]动态连接:每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态连接
[if !supportLists]§ [endif]方法出口:返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。
[if !supportLists]1. [endif]局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的。
[if !supportLists]2. [endif]在方法运行期间不会改变局部变量表的大小。主要存放了编译期可知的各种基本数据类型、对象引用 (reference类型)、returnAddress类型)。
java虚拟机栈,规定了两种异常状况:
[if !supportLists]1. [endif]如果线程请求的深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
[if !supportLists]2. [endif]如果虚拟机栈动态扩展,而扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
本地方法栈
可通过参数 栈容量可由-Xss设置
[if !supportLists]1. [endif]虚拟机栈为虚拟机执行Java方法(也就是字节码)服务。
[if !supportLists]2. [endif]本地方法栈则是为虚拟机使用到的Native方法服务。有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一
方法区(Method Area)
可通过参数-XX:MaxPermSize设置
[if !supportLists]1. [endif]线程共享内存区域,用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码,方法区也称持久代(Permanent
Generation)。
[if !supportLists]2. [endif]虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
[if !supportLists]3. [endif]如何实现方法区,属于虚拟机的实现细节,不受虚拟机规范约束。
[if !supportLists]4. [endif]方法区主要存放java类定义信息,与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收。
[if !supportLists]5. [endif]方法区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
[if !supportLists]6. [endif]运行时常量池,也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池。
运行时常量池
JDK1.6之前字符串常量池位于方法区之中。 JDK1.7字符串常量池已经被挪到堆之中。
可通过参数-XX:PermSize和-XX:MaxPermSize设置
[if !supportLists]§ [endif]常量池(Constant Pool):常量池数据编译期被确定,是Class文件中的一部分。存储了类、方法、接口等中的常量,当然也包括字符串常量。
[if !supportLists]§ [endif]字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储编译期类中产生的字符串类型数据。
[if !supportLists]§ [endif]运行时常量池(Runtime Constant Pool):方法区的一部分,所有线程共享。虚拟机加载Class后把常量池中的数据放入到运行时常量池。常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目资源关联最多的数据类型。
[if !supportLists]1. [endif]常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic
Reference)。
[if !supportLists]2. [endif]字面量:文本字符串、声明为final的常量值等。
[if !supportLists]3. [endif]符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。
直接内存
可通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值(-Xmx指定)一样。
[if !supportLists]§ [endif]直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
总结的简单一点
java堆(Java Heap)
可通过参数 -Xms 和-Xmx设置
[if !supportLists]1. [endif]Java堆是被所有线程共享,是Java虚拟机所管理的内存中最大的一块 Java堆在虚拟机启动时创建
[if !supportLists]2. [endif]Java堆唯一的目的是存放对象实例,几乎所有的对象实例和数组都在这里
[if !supportLists]3. [endif]Java堆为了便于更好的回收和分配内存,可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor区
[if !supportLists]§ [endif]新生代:包括Eden区、From Survivor区、To Survivor区,系统默认大小Eden:Survivor=8:1。
[if !supportLists]§ [endif]老年代:在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
java虚拟机栈(stack)
可通过参数 栈帧是方法运行期的基础数据结构栈容量可由-Xss设置
[if !supportLists]1. [endif]Java虚拟机栈是线程私有的,它的生命周期与线程相同。
[if !supportLists]2. [endif]每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
[if !supportLists]3. [endif]虚拟机栈是执行Java方法的内存模型(也就是字节码)服务:每个方法在执行的同时都会创建一个栈帧,用于存储 局部变量表、操作数栈、动态链接、方法出口等信息
方法区(Method Area)
可通过参数-XX:MaxPermSize设置
[if !supportLists]1. [endif]线程共享内存区域),用于储存已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码,方法区也称持久代(Permanent
Generation)。
[if !supportLists]2. [endif]方法区主要存放java类定义信息,与垃圾回收关系不大,方法区可以选择不实现垃圾回收,但不是没有垃圾回收。
[if !supportLists]3. [endif]方法区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
[if !supportLists]4. [endif]运行时常量池,也是方法区的一部分,虚拟机加载Class后把常量池中的数据放入运行时常量池。
什么是CAS?
CAS(compare and swap)的缩写,中文翻译成比较并交换。
CAS 不通过JVM,直接利用java本地方 JNI(Java Native Interface为JAVA本地调用),直接调用CPU 的cmpxchg(是汇编指令)指令。
利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法,实现原子操作。其它原子操作都是利用类似的特性完成的。
整个java.util.concurrent都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
CAS应用
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS优点
确保对内存的读-改-写操作都是原子操作执行
CAS缺点
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作
总结
[if !supportLists]1. [endif]使用CAS在线程冲突严重时,会大幅降低程序性能;CAS只适合于线程冲突较少的情况使用。
[if !supportLists]2. [endif]synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
以上是前五道题的答案,需要剩余面试题答案的,关注我简信回复Java线程面试获取。
也可以加群:650385180,答案在群的共享区,供大家免费下载。
下面是我总结出来的关于多线程所要掌握的一些知识点,在群里面也有学习资料。