面试难度:★★★★
考察概率:★★★
本人从毕业开始一直在一线互联网大厂工作,现任技术TL,出版过《深入理解Java并发》一书,折腾过技术开源项目,并长期作为面试官参与面试,深谙双方的诉求与技术沟通。如今归零心态,再出发。#莫等闲,白了少年头#
技术交流+v:xxxyxsyy1234(和笔者一起努力,每日打卡) 2000+以面试官视角总结的考点,可与我共同打卡学习
面试官视角
利用java api能创建线程并应用到实际业务开发中,这是最基本的能力要求。包括线程的状态变化以及基础的线程操作,对基础知识的掌握在校招面试的时候会被考察到,会反映出学生自学的能力。同时在社招的时候,对低阶开发者,也会偶尔被问到,用来筛选出那些只背八股文的候选人,实际上根本没有怎么写过代码的人,基本上一问就心里有底了。
面试题
1、从性能优化领域,简述Amdahl和Gustafson定律的意义?
2、简述并发编程的优缺点以及如何在串行化和并行化间做出选择?
3、简述操作系统的线程模型以及它的实现机制?
4、简述线程的状态转移的过程?
5、利用JavaAPI创建线程的背后经历了哪些流程?
6、你对于线程组是如何理解的?
7、简述线程常见的实现方式?
回答要点
1、从性能优化领域,简述Amdahl和Gustafson定律的意义。
Amdahl定律是指,如果一台计算机系统中某个任务只能串行执行(无法并行化),那么无论系统中有多少个处理器或者计算机,这个任务的执行时间总是无法缩短到串行部分的执行时间,因为该任务的执行时间受限于串行部分的执行速度。因此,在优化性能时,我们需要重点优化串行部分,以最大化性能提升。
Gustafson定律则是指,在并行计算中,任务的规模是可变的,因此我们可以根据系统资源的增加而逐步增加任务规模,以实现更高的并行度和更快的速度。也就是说,增加处理器的数量可以扩展任务规模,从而加快整个系统的执行速度。因此,在优化性能时,我们可以通过增加处理器数量和优化可并行化部分,来不断扩展任务规模,以最大化性能提升。
可加分亮点
面试官心理: 一般而言候选人只会死记硬背相关的知识点,会缺少横向对比的分析,这种能力在实际工作中是很需要的一种能力素质,会经常涉及到方案选型以及和业界竞品的分析。对面试官而言,如果候选人能够展现出分析能力,是能够有一个好印象的。
Amdahl定律和Gustafson定律的意义非常重要,两者有什么联系,你怎么理解?
这两个定律看起来是矛盾的,实际上是针对系统加速优化两个不同的视角去看待问题的。从两个极端场景来说,系统的串行比例如果为0的话,也就是全部都可以并行化,那么系统加速比均为最大值等于当前处理器的个数,理想情况下,使用更多的处理器能够使得系统性能更快。另一方面,如果系统的串行比例为1的话,也就是系统完全不能并行化,这两个定律最终的加速比都是1,也就说明无论使用多少个处理器个数都无法使得当前系统能够加速,性能得到提升。关于这两个定律如何去理解呢?
实际上,Gustafson定律更多的是表明如果当前系统存在可并行化优化的空间的话,适当通过增加多核处理器并发处理的优势,能够使得系统加速,完成系统整体的性能优化,并且加速比与处理器的个数成正比例的关系。那么,这种优化是否存在极限呢?很显然,Amdahl定律表明这种多处理器的优化很显然存在一定的上限,这个上限取决于当前系统的串行化的比例。通俗的理解是,最大的优化程度自然而然取决于当前系统还存在多大的可优化空间。
1、指导我们进行性能优化:Amdahl定律和Gustafson定律提供了优化性能的基本思路和方法,帮助我们重点优化串行部分,同时增加处理器数量和优化可并行化部分,以最大化性能提升。在实际的开发工作中,我们可以根据这两个定律提供的指导,来选择合适的方法,进行性能优化。
2、提升系统性能的关键:在当今计算机系统中,为了提高系统的吞吐量和响应速度,我们通常会采用多处理器或者分布式系统来加速计算。了解Amdahl定律和Gustafson定律对于设计和实现高性能的分布式、高并发系统非常重要,可以提高系统的吞吐量和响应速度,满足不断增长的业务需求。
3、帮助人们更好地理解并行计算的本质:Amdahl定律和Gustafson定律的提出,让人们更好地认识到并行计算的本质,即计算任务的并行度受限于串行部分的执行时间。这能够帮助我们更加深入地了解并行计算的本质以及如何最大化利用系统的资源和并行计算的能力。
总之,了解Amdahl定律和Gustafson定律对于性能优化从业者和系统设计者都非常重要,它们能够指导我们如何提升系统性能,在实际应用中具有非常广泛的应用价值。
2、简述并发编程的优缺点以及如何在串行化和并行化间做出选择?
并发编程是一种利用计算机系统中多个同时运行的执行线程或进程来处理多个任务的编程方案。 其优点和缺点如下:
优点:
1、提高程序的执行效率:在多核CPU的计算机中,使用并发编程可以让我们同时使用多个CPU核心,提高程序的处理速度和响应能力;
2、增加程序的可靠性:并发编程允许程序通过将任务分解到不同的线程或进程中来减少单点故障的风险,从而提高程序的可靠性;
3、提高程序的可扩展性:并发编程可以让我们将任务分配到不同的线程或进程中,以支持更高的并发度和更广泛的用户群体,从而提高程序的可扩展性。
缺点:
1、复杂度增加:并发编程增加了程序的复杂性,需要我们更加仔细地设计和管理线程和进程,防止死锁、竞态条件等问题的发生,否则会影响程序的正确性和可维护性;
2、调试困难:并发编程会产生一些难以重现的问题,例如死锁、竞态条件等,这会导致调试变得更加困难;
3、可能存在性能问题:并发编程中的线程和进程切换、通信等操作都会造成一定的开销,这可能会导致性能下降,需要仔细评估和优化。
可加分亮点
面试官心理: 对技术选型的思考很重要,回答这部分如果能结合自己的业务经验就更好了。
在选择并发编程和串行化之间时,需要根据实际情况做出选择?
1、如果任务本身就是串行的,例如简单的数学计算等,那么使用并发编程反而会增加复杂性和降低性能,会让程序变得更慢;
2、如果任务可以被分割成多个独立的子任务,并且这些子任务可以并发运行,那么使用并发编程可以提高程序的执行效率和响应能力;
3、如果我们需要处理的数据量非常大,在单核CPU的情况下需要很长时间才能完成,而在多核CPU的情况下使用并发编程可以大幅缩短处理时间。
总之,在选择并发编程和串行化之间需要进行权衡,在实际应用中需要根据任务本身的特点和实际情况来决定,以达到最好的性能和可靠性。
3、简述操作系统的线程模型以及它的实现机制。
操作系统的线程模型是为支持多线程而设计的架构和机制,包括线程模型、线程调度、线程同步等方面。线程模型包括用户线程模型和内核线程模型。用户线程模型是在应用程序中实现的,线程的创建、销毁、同步等操作都由应用程序负责。内核线程模型是由操作系统内核实现的,线程的创建、销毁、同步等操作都由内核负责。
线程调度包括抢占式调度和协作式调度两种方式。抢占式调度会抢占正在执行的线程,将处理器分配给优先级更高的线程,协作式调度让线程自己控制自己的执行。操作系统会根据实际需求选择哪种调度方式。
线程同步主要是为了解决同一时刻多个线程访问同一资源所带来的问题。常用的同步机制有信号量、互斥量和条件变量等。
实现机制方面,操作系统会为每个线程维护一份线程控制块,包括线程的状态、优先级、栈指针等信息。操作系统还需要提供调度器、同步机制等支持,以便进行线程的调度和同步操作。操作系统在支持线程的同时还需要解决多进程、内存管理等其他问题。
4、简述线程的状态转移。
线程是一种轻量级的进程,它具有独立的执行序列和调用栈,线程和线程之间可以共享进程中的资源。在操作系统中,每一个线程都会具有一个状态,状态会随着线程的执行过程而发生改变。线程的状态一般包括以下几种:
-
新建状态(New):创建一个线程后,线程会被分配一个表示其控制信息的数据结构,此时线程还没有开始执行,处于新建状态。
-
就绪状态(Ready):当线程已经准备好执行,但还没有获得CPU资源时,它处于就绪状态。此时操作系统会将线程添加到可执行线程池中,等待被调度执行。
-
运行状态(Running):当操作系统为就绪状态的线程分配CPU资源,线程开始执行,这时它处于运行状态。
-
阻塞状态(Blocked):当线程需要等待某个事件发生(如等待I/O操作的完成),以致于它无法执行时,它就进入了阻塞状态。在阻塞状态中,线程不占用CPU,也不参与竞争CPU资源。
-
等待状态(Waiting):当线程需要等待其他线程执行完成后再执行,或等待某个对象的notify()或notifyAll()方法被调用时,线程进入等待状态。此时线程不会自动被唤醒,需要其他线程唤醒它。
-
超时等待状态(Timed Waiting):当线程需要等待一定时间后再执行或等待某个对象的notify()或notifyAll()方法被调用时,并且设置了等待时间,线程进入超时等待状态。在等待时间到达前,线程不会自动被唤醒,需要其他线程唤醒它。
-
终止状态(Terminated):当线程完成了它的工作或被强制终止时,线程进入终止状态。此时操作系统会回收该线程占用的资源,并从系统中移除对应的线程控制块。
线程的状态转移可以通过以下四种事件进行:
-
创建线程事件:当一个线程被创建时,它进入新建状态。
-
调度事件:当操作系统为就绪状态的线程分配CPU资源时,线程进入运行状态。当CPU资源被剥夺时,线程又重新回到就绪状态等待下一次被分配CPU资源运行。
-
阻塞事件:当线程需要等待某项事件(如I/O)时,它由运行状态转换为阻塞状态。当事件完成时,线程又重新进入就绪状态。
-
终止事件:当线程完成了它的工作或者被强制终止时,它进入终止状态。
需要特别注意的是,等待状态和超时等待状态可能会被打破,使线程重新进入就绪状态,这往往是由于与等待相关的事件发生了改变。例如,另一个线程调用了等待线程所等待的对象的notify()或notifyAll()方法,或者等待时间到期。
总之,当线程需要等待某个事件或条件满足后再执行时,就会进入等待状态或超时等待状态,这些状态在多线程编程中非常常见。需要特别注意的是,这些状态可能会出现死锁等问题,因此在编写多线程程序时,需要特别小心。
5、利用JavaAPI创建线程的背后经历了哪些流程?
在Java中,创建线程主要有两种方式:一种是继承Thread类,另一种是实现Runnable接口。下面以继承Thread类的方式为例,说明创建线程的流程:
-
创建一个Thread子类,并在其中重写run()方法,该方法中包含线程运行时的代码。
-
创建Thread子类的对象,该对象即为线程对象。
-
调用线程对象的start()方法,该方法会将线程设置为就绪状态,等待系统调度执行。
-
系统调度执行线程时,会执行线程对象的run()方法,实现线程的具体逻辑。
具体来说,当线程对象调用start()方法时,Java虚拟机会为该线程分配一个唯一的ID,并将线程状态设置为可运行。此时,线程并不一定会立即执行,需要等待Java虚拟机为其分配CPU资源,并将其加入可执行线程池中。一旦系统为其分配CPU资源,线程就会开始执行,直到run()方法执行完毕或被中断、异常终止等。
需要特别注意的是,多线程编程中容易出现的问题有很多,如死锁、竞争条件等。因此,在编写多线程程序时,需要特别小心,要确保所有的线程都能正确地运行,并避免出现死锁等问题。同时,Java提供了很多线程安全的工具类和方法,如synchronized、ReentrantLock等,可以帮助开发者更好地编写多线程程序。
6、你对于线程组是如何理解的?
在Java中,线程组(Thread Group)是一组线程的集合,可以让我们对一组线程进行统一的管理。线程组可以包含其他线程组或线程,它们之间构成了一个树形结构。
线程组的主要作用是对线程进行管理和控制,可以对一个线程组中的所有线程进行操作,如中断、挂起、恢复等。此外,线程组也能够方便地统计和监控线程的运行状况,能够更好地管理应用程序的线程。
线程组的创建可以通过如下方式进行:
ThreadGroup group = new ThreadGroup("my-thread-group");
在创建线程时,可以将线程添加到线程组中:
Thread thread1 = new Thread(group, "my-thread-1");
Thread thread2 = new Thread(group, "my-thread-2");
线程组中的所有线程可以通过group.list()方法来列出
Thread[] threads = new Thread[group.activeCount()];
group.enumerate(threads);
for (Thread thread : threads) {
System.out.println(thread.getName());
}
除了上述的基本操作外,线程组还可以设置优先级、设置守护线程等。线程组的主要作用是对一组线程进行分类和管理,便于开发者进行线程的管理和监控。
可加分亮点
面试官心理:Daemon守护线程是个比较冷门的知识点,对业务开发来说基本上不会使用到,如果候选人能够知道这点的话,其实是比较可喜的。
你了解Daemon守护线程吗?
守护线程是一种十分特殊的线程,就和它的名称一样,它是整个Java程序运行时的守护者,在后台默默的守护者一些系统服务,比如垃圾回收线程,JIT线程就可以理解成守护线程。与之对应的就是用户线程,一般用来执行自定义的用户操作,当系统所有的用户线程都执行结束后,也就是说当前系统没有任何对象是需要“守护”的了,那么对守护线程而言就会自动结束执行,虚拟机结束执行退出。一个普通的用户线程可以通过setDaemon方法将其转换成守护线程,可以看如下的代码示例:
public static void main(String[] args) throws InterruptedException {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("I'm daemon thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("Removal of resources in finally block");
}
}
});
daemonThread.setDaemon(true);
daemonThread.start();
TimeUnit.SECONDS.sleep(3);
}
代码输出结果如下:
I'm daemon thread
Removal of resources in finally block
I'm daemon thread
Removal of resources in finally block
I'm daemon thread
7、简述线程常见的实现方式。
Java中线程的实现方式有三种:实现Runnable接口、继承Thread类以及实现Callable接口。
1、实现Runnable接口
实现Runnable接口是最常见的创建线程的方式,需要实现run()方法,并将其作为参数传递给Thread的构造函数。
public class MyRunnable implements Runnable {
public void run() {
// 线程执行逻辑
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
2、继承Thread类
继承Thread类是另一种创建线程的方式,需要扩展Thread类,并重写其中的run()方法。
public class MyThread extends Thread {
public void run() {
// 线程执行逻辑
}
}
MyThread thread = new MyThread();
thread.start();
3、实现Callable接口
实现Callable接口是Java 5中新引入的方式,类似于Runnable接口,但是它允许线程执行并返回一个结果。
public class MyCallable implements Callable<String> {
public String call() {
// 线程执行逻辑
return "result";
}
}
ExecutorService executor = Executors.newFixedThreadPool(1);
Future<String> future = executor.submit(new MyCallable());
String result = future.get();
以上是Java中常见的线程实现方式。在实际情况下,选择哪种方式取决于具体的业务需求。通常来说,实现Runnable接口更为推荐,因为它可以避免单继承的限制,而且更容易实现线程的资源共享和代码的复用。
代码考核
在校招时会手撕代码考察下,使用api创建线程的基本写法,对候选人的一个基础考察,但整体不会很难。可以参考问题5和7,进行掌握。