Java面试题3 线程
自己尝试通过打字来回答一些网上常见的面试题,答案仅代表我自己的观点
35. 并行和并发有什么区别?#
并发是指两个任务,一个在另一个的开始后结束前之间开始,所以,先执行一个任务,执行一会儿换到另一个任务,这样也算并发,它并不要求两个任务必须同时执行。
而并行是并发的一种特殊形式,它是指两个任务真的在同时执行。
36. 线程和进程的区别?#
进程是运行中的程序,对你编写的程序来说,进程是计算机资源的抽象。在一个进程中,程序会有它在独占计算机资源的假象,比如它使用的内存是独占的,不会被其他进程访问,它具有独立的打开文件列表等等。而对操作系统来说,进程是运行中的程序的一个抽象,操作系统可以以某种方式调度它们来让它们并发(或在多核操作系统上并行)的执行。
和进程的概念差不多,线程可以看作是进程内部的“进程”。进程让程序间并发执行成为了可能,而线程将并发执行的粒度细化到程序内部,让程序中的多个模块可以并发的执行。同时,线程之间除了栈区之外不具有独立的私有空间,一个进程中的所有线程共享堆内存空间。
37. 守护线程是什么?#
守护线程是从后台运行的线程,JVM关闭后它就关闭。
38. 创建线程有哪几种方式?#
就一种,通过Thread
。因为在Java层面,与JVM线程绑定的对象只有Thread
一个。任何一种创建线程的方式归根结底都是new Thread
。
该面试题的答案有:
- extends Thread,重写run方法
- new Thread(new Runnable)
- new Thread(new Callable)
- Future
- 线程池
下面我来攻击这些答案,首先是第二个和第三个,线程用于执行一个任务,线程是一个工作者(Worker),而想要让线程工作,你需要分配给它一个任务(Task),Runnable
和Callable
显然都是作为任务的角色存在的,这怎么能算做创建线程的方式呢?第一种第二种第三种都可以被总结为通过创建Thread
对象来创建线程吧。
对于第四个第五个,这不过是Java官方提供的一个线程框架Executor
中的几个组件,它们是可以创建线程,归根结底还是创建Thread
嘛。那并发框架多了,你要不要都写上?写的完吗?写的全吗?
39. 说一下 runnable 和 callable 有什么区别?#
Runnable从名字来看是可运行的
,没错仅此而已,它就是可运行而已,它没有返回值,不抛出任何编译时异常。
Callable从名字来看是可调用的
,它具有返回值,也就是调用结果,抛出受检测异常。
40. 线程有哪些状态?#
下面是Java中定义的线程状态,除此之外,线程还应该具有一个正在运行的状态。
public enum State {
// 线程刚被创建,还没有start
NEW,
// 线程已经可以被执行了,但它还缺少执行所必须的资源,等待操作系统分配后就可以执行
RUNNABLE,
// 阻塞状态,一个线程在等待监视器锁时处于这种状态
BLOCKED,
/* 等待状态的线程处于这种状态,下列三个方法将线程置于等待状态
* Object.wait
* Thread.join
* LockSupport.park
* 等待状态的线程是在等待其它线程满足某种条件,比如其他线程调用Object.notify。或者其他线程终结。
* 和BLOCKED状态不同的是,BLOCKED状态的线程正在等待锁,而WAITING状态的线程连等待锁的机会都没有
*/
WAITING,
// 计时的等待,调用wait(long)、sleep等方法进入该状态
TIMED_WAITING,
// 线程完成执行时的状态
TERMINATED;
}
41. sleep() 和 wait() 有什么区别?#
没有任何关系,谈何区别。
sleep
是用于让当前线程暂时进入TIMED_WAITING
状态,等待一段时间。
wait
是多线程之间的协作工具,使用wait
的线程进入WATING/TIMED_WAITING
状态,不再执行,直到其它的线程通知它或超时时间到达。
42. notify()和 notifyAll()有什么区别?#
notify
从等待队列里随机拿出一个线程,将它转换成RUNNABLE
状态,也就是就绪状态,notifyAll
则是将等待队列中的所有线程转换成RUNNABLE
状态。
进入到就绪状态后的线程不一定能立即执行,还要竞争对象的锁。
43. 线程的 run()和 start()有什么区别?#
run
方法来自Runnable
接口,Thread
本身实现了Runnable
导致它可以代表一个任务,run
方法就是任务中的代码。
如果你直接执行run
方法,那么你并没有启动一个新的线程,你就在当前的线程中运行了任务而已(我倒觉得Thread
类作为一个任务的工作者不应该实现Runnable
,这样的话就不会有今天这个让人产生疑问的问题了)。
而start
方法是Thread
类的方法,它通知JVM虚拟机启动了一个新的线程,并从新线程来调用run
方法来执行任务代码。
44. 创建线程池有哪几种方式?#
- Executors中的静态方法
new ThreadPoolExecutor
- 自己继承ThreadPoolExecutor
45. 线程池都有哪些状态?#
下面是ThreadPoolExecutor
中定义的线程池状态
// 线程池处于工作状态,接收新任务,处理排队任务
private static final int RUNNING = -1 << COUNT_BITS;
// 线程池被关闭,不接收新任务,但处理排队任务
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 线程池停止,不接受新任务,不处理排队任务,中断所有正在执行的任务
private static final int STOP = 1 << COUNT_BITS;
// 所有任务已经终结,workerCount为0。进入该状态的线程池将调用`terminated`钩子方法
private static final int TIDYING = 2 << COUNT_BITS;
// terminated钩子方法执行完毕
private static final int TERMINATED = 3 << COUNT_BITS;
46. 线程池中 submit()和 execute()方法有什么区别?#
说它们的不同就牵扯到两个接口,Executor
和ExecutorService
。
execute
是Executor
中的方法,它用于在未来的某个时间内执行一个任务,它没有返回值。Executor
接口只有这一个方法,它只做这一件事。
submit
是ExecutorService
中的方法,这个方法返回一个Future
,可以跟踪该任务的状态和返回值,同时接收一个Callable
。ExecutorService
是更加强大易用的Executor
,它扩展了一些功能,比如刚刚说的任务返回、状态检测、中断执行等。
47. 在 java 程序中怎么保证多线程的运行安全?#
在不同的场景下,线程安全的定义是不同的,这主要看你的类需要维护什么状态在什么条件下的一致性。举两个例子吧,Hashtable
类可以保证所有线程对它的访问都是顺序的,所以它其中的数据是非常精准的,不管有多少个线程共同访问它,都不会出现一点儿错误,而ConcurrenctHashMap
类为了获得更高的并发性放弃了一些统计数据的一致性,它让多个线程可以共同操作容器,但使用它的时候一些统计数据可能不精准。
所以我们没法讨论在Java程序中怎么保证多线程的运行安全,不过以下是我们在保证线程安全时要考虑的一些东西:
- 互斥访问:代码中有一些地方,我们称为临界区,它必须被多个线程互斥的访问,任何时候不可以有多个线程在临界区内。这可以使用锁来实现。
- 可见性:可见性是比互斥更弱的一种机制。由于现代操作系统具有缓存,所以很可能两个被分配到不同CPU上执行的线程中,一个改了内存数据但未刷到主存中,另一个访问它改的内存数据得到了旧数据。可见性用于保证对于一个变量,这种情况不会发生,可以使用volatile和final来确保可见性,同时高级的并发程序设计者也可以遵循
happens-before
原则来确保并发变量的可见性。
48. 多线程锁的升级原理是什么?#
多线程锁升级不是JVM标准的一部分,JVM可以不实现这一部分。不过我们平常使用的HotSpot虚拟机使用这种方法对synchronizd
的性能进行优化。加解锁操作涉及到用户态到内核态再到用户态的转换,而且也可能涉及到线程挂起态到就绪态的转化,这些都是非常耗时的,所以要进行优化。
HotSpot把锁分成几个不同的级别,这些级别通过被加锁对象——也就是synchroinzed
修饰的对象在虚拟机中的对象头中的一些字段来描述。所以,锁升级实际上都是在操作这个被加锁对象。
在没有对象加锁时,处于无锁状态,等待第一个线程来进行加锁,加锁操作使用CAS保证不会出现问题。
然后是偏向锁,偏向锁基于一个假设,就是锁倾向于被同一个线程多次连续获得。偏向锁会判断锁对象的头里是否存储了当前线程的ID,如果是就代表锁偏向于被当前线程获取,那么当前线程直接获取到锁,不需要任何其它操作。那么当对象头中的线程ID并不是当前线程咋办?就通过CAS操作来尝试将当前线程ID写入对象头,成功就是加锁成功,该锁偏向于当前线程了,失败就需要暂停原来的持有偏向锁的线程,撤销原来的偏向锁。这时如果原来的线程还没退出同步代码块,开始锁升级。
再升级就是轻量级锁。直到轻量级锁,实际的加锁操作还没有出现,轻量级锁也是通过CAS来获取,如果获取失败就自旋,进行下一次CAS,所以轻量级锁也被称作自旋锁。
再升级,重量级锁,真正的加锁操作出现。因为自旋操作CPU在空转,在线程长时间自旋也得不到锁时,JVM会考虑将它升级为重量级锁,干脆让该线程挂起,系统调用就系统调用吧、内核态转换就转换吧,挂起线程就挂起吧,慢点慢点吧,您别在这耗费CPU了。
总结一下:
- 初始是无锁状态
- 然后是偏向锁,如果偏向锁不偏向当前线程并且通过CAS改变偏向线程也没成功,就需要暂停当前偏向线程的执行,尝试撤销偏向锁。
- 如果被暂停的偏向线程没有执行完,升级轻量级锁,想获得锁的线程进行竞争
- 当轻量级锁长时间自旋,升级成重量级锁吧,别搁着浪费CPU资源了。
49. 什么是死锁?#
两个线程正在持有着对方正在请求获得的资源。两个线程可以推广为多个线程。
50. 怎么防止死锁?#
- 按顺序加锁
- 使用具有超时时间的锁
51. ThreadLocal 是什么?有哪些使用场景?#
ThreadLocal是绑定在当前线程对象中的一个线程私有Map,每一个线程具有一个。当你想要在每一个线程中存储一些信息时就可以用它。而且当你使用它时,该线程必须是由你来管理的,一个反例就是线程池,在线程池中,你的任务设置的ThreadLocal中的值可能会被下一个分配到该线程上的任务读取。
52.说一下 synchronized 底层实现原理?#
这好像是JVM来实现的吧,我也不知道。
只能说,通过moniterenter
和moniterexit
字节码实现的,而且不同的虚拟机对这个关键字的优化不同,可以把锁升级给答上。
53. synchronized 和 volatile 的区别是什么?#
它们没有共同点。
synchronized
是保护一段代码在所有访问的线程上互斥,任意时间段只有一个线程能进入到这段代码。
volatile
是保证一个线程对该变量的更新立刻反映到其他线程中。
54. synchronized 和 Lock 有什么区别?#
synchronized
是Java中内置的关键字,这个锁由JVM实现。
Lock
是通过Java实现的,表示锁的类,它比synchronized
提供更多的功能,比如可中断、有超时时间、读写分离、具有公平和不公平两种模式。
通常认为Lock
比synchronized
更轻量,但随着Java的升级,二者的差距已经慢慢减小。
55. synchronized 和 ReentrantLock 区别是什么?#
同上
56. 说一下 atomic 的原理?#
通过CAS操作来实现对值的原子更新。
就往CAS上答就完了。
作者:Yudoge
出处:https://www.cnblogs.com/lilpig/p/16400327.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
欢迎按协议规定转载,方便的话,发个站内信给我嗷~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix