1. 为什么要使用并发编程
-
提升多核CPU的利用率:一台主机通常有多个CPU核心,创建多个线程,操作系统理论上可将线程分配给不同CPU执行,提高CPU使用效率,单线程则只能使用一个CPU核心。 -
方便业务拆分,提升应用性能:如网上购物时,拆分减库存、生成订单等操作,利用多线程技术可提升响应速度,并行程序更适应复杂业务模型。
2. 多线程应用场景
例如迅雷多线程下载、数据库连接池、分批发送短信等。
3. 并发编程有什么缺点
并发编程目的是提高程序执行效率,但并不总是能提升速度,还可能遇到内存泄漏、上下文切换、线程安全、死锁等问题。
4. 并发编程三个必要因素是什么?
-
原子性:一个或多个操作要么全部执行成功,要么全部执行失败。 -
可见性:一个线程对共享变量的修改,另一个线程能够立刻看到,可通过synchronized、volatile实现。 -
有序性:程序执行顺序按照代码先后顺序执行,但处理器可能对指令进行重排序。
5. Java程序中怎么保证多线程的运行安全?
出现线程安全问题的原因及解决办法:
-
线程切换带来的原子性问题,解决办法:使用多线程之间同步synchronized或使用锁(lock)。 -
缓存导致的可见性问题,解决办法:synchronized、volatile、LOCK可解决。 -
编译优化带来的有序性问题,解决办法:Happens-Before规则可解决。
6. 并行和并发有什么区别?
-
并发:多个任务在同一个CPU核上,按细分时间片轮流(交替)执行,逻辑上任务同时执行。 -
并行:单位时间内,多个处理器或多核处理器同时处理多个任务,是真正意义上的“同时进行”。 -
串行:有n个任务,由一个线程按顺序执行,不存在线程不安全情况和临界区问题。
7. 什么是多线程
多线程指程序中包含多个执行流,一个程序中可同时运行多个不同线程执行不同任务。
8. 多线程的好处
可以提高CPU的利用率。在多线程程序中,一个线程等待时,CPU可运行其他线程,提高程序效率,允许单个程序创建多个并行执行的线程完成各自任务。
9. 多线程的劣势:
-
线程需占用内存,线程越多占用内存越多。 -
多线程需要协调和管理,需要CPU时间跟踪线程。 -
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题。
10. 线程和进程区别
-
进程:一个在内存中运行的应用程序,每个正在系统上运行的程序都是一个进程。 -
线程:进程中的一个执行任务(控制单元),负责在程序里独立执行。一个进程至少有一个线程,可运行多个线程,多个线程可共享数据。 -
区别: -
根本区别:进程是操作系统资源分配的基本单位,线程是处理器任务调度和执行的基本单位。 -
资源开销:进程有独立代码和数据空间,程序切换开销大;线程是轻量级进程,同一类线程共享代码和数据空间,线程切换开销小。 -
包含关系:线程是进程的一部分,一个进程内多个线程的执行过程是多条线共同完成。 -
内存分配:同一进程的线程共享本进程地址空间和资源,进程间地址空间和资源相互独立。 -
影响关系:一个进程崩溃后,在保护模式下不影响其他进程,一个线程崩溃可能导致整个进程死掉,多进程比多线程健壮。 -
执行过程:进程有程序运行入口、顺序执行序列和程序出口,线程不能独立执行,依存在应用程序中,由应用程序提供执行控制,两者均可并发执行。
11. 什么是上下文切换?
多线程编程中线程个数大于CPU核心个数,一个CPU核心任意时刻只能被一个线程使用。CPU为每个线程分配时间片并轮转,当一个线程时间片用完,重新处于就绪状态让给其他线程,这个过程就是一次上下文切换。即当前任务在执行完CPU时间片切换到另一个任务前,会先保存自己的状态,下次切换回时再加载,从保存到再加载的过程就是一次上下文切换。上下文切换通常是计算密集型的,会消耗大量CPU时间。
12. 守护线程和用户线程有什么区别呢?
-
用户线程:运行在前台,执行具体任务,如程序的主线程、连接网络的子线程等。 -
守护线程:运行在后台,为其他前台线程服务,是JVM中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作。
13. 如何在Windows和Linux上查找哪个线程cpu利用率最高?
-
Windows:用任务管理器查看。 -
Linux: -
用top工具,找出cpu耗用厉害的进程pid,终端执行top命令,然后按下shift+p(shift+m是找出消耗内存最高)查找出cpu利用最厉害的pid号。 -
根据pid号,执行top -H -p pid,然后按下shift+p,查找出cpu利用率最厉害的线程号。 -
将获取到的线程号转换成16进制。 -
使用jstack工具将进程信息打印输出,jstack pid号 > /tmp/t.dat ,也可使用JDK自带的工具“jconsole”、“visualVm”查看。
14. 什么是线程死锁
死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或彼此通信而造成的一种阻塞现象,若无外力作用,它们都将无法推进下去。此时系统处于死锁状态,这些永远在互相等待的进程(线程)称为死锁进程(线程)。多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放,程序不可能正常终止。
15. 形成死锁的四个必要条件是什么
-
互斥条件:在一段时间内某资源只由一个进程占用,其他进程请求时只能等待,直至占有资源的进程用毕释放。 -
占有且等待条件:进程已保持至少一个资源,又提出新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但对自己已获得的其它资源保持不放。 -
不可抢占条件:别人已占有某项资源,不能因为自己需要就去抢夺。 -
循环等待条件:若干进程之间形成头尾相接的循环等待资源关系。
16. 如何避免线程死锁
-
避免一个线程同时获得多个锁。 -
避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。 -
尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
17. 创建线程的四种方式
-
继承Thread类。 -
实现Runnable接口。 -
实现Callable接口。 -
使用匿名内部类方式。
18. 说一下runnable和callable有什么区别
-
相同点:都是接口,都可编写多线程程序,都采用Thread.start()启动线程。 -
主要区别: -
Runnable接口run方法无返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可获取异步执行的结果。 -
Runnable接口run方法只能抛出运行时异常,且无法捕获处理;Callable接口call方法允许抛出异常,可获取异常信息。
19. 线程的run()和start()有什么区别?
-
run()方法是线程体,用于执行线程的运行时代码,可重复调用。 -
start()方法用于启动线程,真正实现多线程运行,调用start()方法无需等待run方法体代码执行完毕,线程处于就绪状态,然后通过调用run()方法完成运行状态,run()方法运行结束,线程终止。直接调用run()方法相当于调用普通函数,是在主线程里执行,并非多线程工作。
20. 为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
new一个Thread,线程进入新建状态。调用start()方法,会启动一个线程并使线程进入就绪状态,当分配到时间片后开始运行,start()会执行线程的相应准备工作,然后自动执行run()方法的内容,这是真正的多线程工作。而直接执行run()方法,会把run方法当成一个main线程下的普通方法去执行,并不会在某个线程中执行它,所以不是多线程工作。
21. 什么是Callable和Future?
-
Callable接口类似于Runnable,但功能更强大,被线程执行后可返回值。 -
Future接口表示异步任务,是一个可能还没有完成的异步任务的结果,可拿到Callable执行后的返回值。Callable用于产生结果,Future用于获取结果。
22. 什么是FutureTask
FutureTask表示一个异步运算的任务,里面可传入一个Callable的具体实现类,可对异步运算任务的结果进行等待获取、判断是否完成、取消任务等操作。只有运算完成时结果才能取回,若运算尚未完成,get方法将会阻塞。FutureTask也可对调用了Callable和Runnable的对象进行包装,且可放入线程池中。
23. 线程的状态
-
新建(new):新创建了一个线程对象。 -
就绪(可运行状态)(runnable):线程对象创建后,调用start()方法,该线程处于就绪状态,等待被线程调度选中,获取cpu的使用权。 -
运行(running):可运行状态的线程获得了cpu时间片,执行程序代码。 -
阻塞(block):处于运行状态中的线程因某种原因,暂时放弃对CPU的使用权,停止执行,进入阻塞状态,直到进入就绪状态,才有机会再次被CPU调用进入运行状态。阻塞分为等待阻塞、同步阻塞、其他阻塞。 -
死亡(dead)(结束):线程run()、main()方法执行结束,或因异常退出run()方法,则该线程结束生命周期,死亡的线程不可再次复生。
24. Java中用到的线程调度算法是什么?
Java虚拟机采用抢占式调度模型,优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
25. 线程的调度策略
线程调度器选择优先级最高的线程运行,但发生以下情况时,会终止线程的运行:
-
线程体中调用了yield方法让出了对cpu的占用权利。 -
线程体中调用了sleep方法使线程进入睡眠状态。 -
线程由于IO操作受到阻塞。 -
另外一个更高优先级线程出现。 -
在支持时间片的系统中,该线程的时间片用完。
26. 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?
-
线程调度器:是一个操作系统服务,负责为Runnable状态的线程分配CPU时间。 -
时间分片:指将可用的CPU时间分配给可用的Runnable线程的过程,分配可基于线程优先级或线程等待时间。线程调度不受Java虚拟机控制,应用程序控制它是更好的选择。
27. 请说出与线程同步以及线程调度相关的方法。
-
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁。 -
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用需处理InterruptedException异常。 -
notify():唤醒一个处于等待状态的线程,由JVM确定唤醒哪个线程,与优先级无关。 -
notifyAll():唤醒所有处于等待状态的线程,让它们竞争锁,只有获得锁的线程才能进入就绪状态。
28. sleep()和wait()有什么区别?
-
类的不同:sleep()是Thread线程类的静态方法,wait()是Object类的方法。 -
是否释放锁:sleep()不释放锁;wait()释放锁。 -
用途不同:Wait通常用于线程间交互/通信,sleep通常用于暂停执行。 -
用法不同:wait()方法被调用后,线程不会自动苏醒,需别的线程调用同一个对象上的notify()或者notifyAll()方法;sleep()方法执行完成后,线程会自动苏醒,也可使用wait(long timeout)超时后线程自动苏醒。
29. 你是如何调用wait()方法的?使用if块还是循环?为什么?
wait()方法应该在循环中调用。因为处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序可能在未满足结束条件的情况下退出。当线程获取到CPU开始执行时,其他条件可能还未满足,循环检测条件是否满足会更好。
30. 为什么线程通信的方法wait(), notify()和notifyAll()被定义在Object类里?
因为Java所有类都继承了Object,Java想让任何对象都可作为锁,wait()、notify()等方法用于等待对象的锁或唤醒线程,而Java线程中没有可供任何对象使用的锁,所以这些方法定义在Object类中。若定义在Thread类里,一个线程可能持有多个锁,放弃锁时难以确定放弃哪个锁,管理会更复杂。
31. 为什么wait(), notify()和notifyAll()必须在同步方法或者同步块中被调用?
当一个线程调用对象的wait()方法时,该线程必须拥有该对象的锁,接着释放锁并进入等待状态,直到其他线程调用该对象的notify()方法。同样,调用notify()方法时也需释放对象的锁,以便其他等待的线程获取锁。由于这些方法都需要线程持有对象的锁,所以只能通过同步实现,只能在同步方法或同步块中被调用。
32. Thread类中的yield方法有什么作用?
使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。当前线程到了就绪状态后,接下来哪个线程从就绪状态变成执行状态由系统分配。
33. 为什么Thread类的sleep()和yield ()方法是静态的?
Thread类的sleep()和yield()方法在当前正在执行的线程上运行,在其他处于等待状态的线程上调用这些方法没有意义,所以它们是静态的,可避免程序员错误地在非运行线程调用这些方法。
34. 线程的sleep()方法和yield()方法有什么区别?
-
sleep()方法给其他线程运行机会时不考虑线程的优先级,会给低优先级的线程运行机会;yield()方法只会给相同优先级或更高优先级的线程运行机会。 -
线程执行sleep()方法后转入阻塞状态,执行yield()方法后转入就绪状态。 -
sleep()方法声明抛出InterruptedException,yield()方法没有声明任何异常。 -
sleep()方法比yield()方法具有更好的可移植性,通常不建议使用yield()方法控制并发线程的执行。
35. 如何停止一个正在运行的线程?
在java中有以下3种方法可以终止正在运行的线程:
-
使用退出标志,使线程正常退出,即run方法完成后线程终止。 -
使用stop方法强行终止,但不推荐,因为stop和suspend及resume一样都是过期作废的方法。 -
使用interrupt方法中断线程。
36. Java中interrupted和isInterrupted方法的区别?
-
interrupt:用于中断线程,调用该方法的线程状态将被置为”中断”状态,但线程中断仅仅是置线程的中断状态位,不会停止线程,需要用户自己监视线程状态并处理。 -
interrupted:是静态方法,查看当前中断信号是true还是false并且清除中断信号。如果一个线程被中断了,第一次调用interrupted则返回true,第二次和后面的就返回false了。 -
isInterrupted:可返回当前中断信号是true还是false,与interrupt最大的差别在于它不会清除中断信号。
37. 什么是阻塞式方法?
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情,ServerSocket的accept()方法就是一直等待客户端连接。调用结果返回之前,当前线程会被挂起,直到得到结果之后才会返回。此外,还有异步和非阻塞式方法在任务完成前就返回。
38. Java中你怎样唤醒一个阻塞的线程?
wait()、notify()方法针对对象,调用任意对象的wait()方法将导致线程阻塞并释放该对象的锁,调用notify()方法则随机解除该对象阻塞的线程,但需重新获取锁,直到获取成功才能往下执行。wait、notify方法必须在synchronized块或方法中被调用,且同步块或方法的锁对象与调用wait、notify方法的对象需为同一个。
39. notify()和notifyAll()有什么区别?
如果线程调用了对象的wait()方法,线程便会处于该对象的等待池中,等待池中的线程不会竞争该对象的锁。notifyAll()会唤醒所有的线程,notify()只会唤醒一个线程。notifyAll()调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,不成功则留在锁池等待锁被释放后再次参与竞争;notify()只会唤醒一个线程,具体唤醒哪一个线程由虚拟机控制。
40. 如何在两个线程间共享数据?
在两个线程间共享变量即可实现共享。共享变量要求变量本身是线程安全的,在线程内使用时,若有对共享变量的复合操作,也需保证复合操作的线程安全性。
原文始发于微信公众号(柯基的安全笔记):40个Java并发编程高频面试题
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论