JavaRush /Java 博客 /Random-ZH /翻译:按主题排列的前 50 个面试问题。第1部分。
KapChook
第 19 级
Volga

翻译:按主题排列的前 50 个面试问题。第1部分。

已在 Random-ZH 群组中发布
原文第一部分翻译为新手、经验丰富的程序员的 Top 50 Java 线程面试问题答案。 第二部分。 注意:这篇文章很大,这就是为什么它不适合一个主题。另外,这很复杂,我尽力去google了,但还是不行。因此,我们要求英语流利的参与者看一下原文并与翻译进行比较,以防他们误解或翻译错误。先感谢您。 在任何面试中,无论是高级还是初级、经验丰富还是初学者,您都会面临一些有关线程、并行性和多线程的问题。事实上,这种内置的并发支持是 Java 的最大优势之一,并帮助它在企业家和程序员中广受欢迎。大多数利润丰厚的 Java 开发人员职位都需要出色的多线程技能以及开发、调试和调整高性能、低延迟应用程序的经验。因此,它是面试中最受欢迎的技能之一。在典型的Java面试中,面试官慢慢地从线程的基本概念开始,问一些问题,比如为什么需要线程、如何创建线程、哪种创建方式更好、继承Thread还是实现Runnable等,然后慢慢移动讨论并发的难点、开发并行应用程序遇到的难点、JDK 1.5中引入的高级并发实用程序、并行应用程序的原理和设计模式以及经典的多线程问题。仅仅了解多线程的基础知识是不够的,还必须知道如何处理并发问题,例如死锁、竞争条件、内存不一致以及各种线程安全问题。这些技能经过了彻底的测试,提出了各种多线程和并发挑战。许多Java开发人员通常只是在面试之前阅读问题,这并不是一件坏事,但你应该理解这一点。另外,积累问题并做同样的练习会浪费很多时间,所以我创建了这个列表。
  1. Java中什么是线程?

  2. 线程是一条独立的执行路径。其目标是利用机器中可用的多个处理器。通过使用多个线程,您可以加快 CPU 密集型任务的速度。例如,如果一个线程需要 100 毫秒来完成一项作业,则可以使用 10 个线程将该作业缩短至 10 毫秒。Java 在语言级别上提供了对多线程的出色支持,而且这是它最强大的优势之一。
  3. Java中线程和进程的区别?

  4. 线程是进程的子集;换句话说,一个进程可以包含多个线程。两个进程运行在不同的内存空间,但所有线程共享相同的内存空间。不要将其与堆栈内存混淆,堆栈内存对于每个线程来说都是不同的,用于存储该线程的本地数据。
  5. 如何创建线程?

  6. 在语言层面,有两种创建线程的方法。java.lang.Thread类的对象代表一个线程,但它需要一个任务来运行,该任务是一个实现了java.lang.Runnable接口的对象。由于 Thread 类实现了 Runnable 接口,因此您可以通过从 Thread 派生类或在其中实现 Runnable 接口来重写 run() 方法。
  7. 什么时候使用Runnable,什么时候使用Thread?

  8. 这是对上一个问题的补充。我们知道,线程可以通过继承Thread类或者实现Runnable接口来创建。那么问题来了,哪种方法更好,什么时候使用哪种方法?如果您知道 Java 不支持多类继承但允许实现多个接口,那么这个问题很容易回答。这意味着如果您想从另一个类继承,最好实现 Runnable。
  9. start() 和 run() 方法之间的区别?

  10. 过去的棘手问题之一,但它仍然足以区分对 Java 中多线程的肤浅理解。start()方法用于启动一个新线程。尽管start()在内部调用了run()方法,但它与简单地调用run()并不相同。如果你像普通方法一样调用 run() ,它会在同一个线程上调用,并且不会启动新线程,这就是调用 start() 方法时发生的情况。
  11. 可运行和可调用之间的区别?

  12. 两个接口都表示要在单独的线程中执行的任务。Runnable 从 JDK 1.0 就已经存在,Callable 是在 JDK 1.5 中添加的。主要区别在于Callable的call()方法可以返回值并抛出异常,而Runnable的run()方法则无法做到这一点。Callable 返回一个可以包含计算结果的 Future 对象。
  13. CyclicBarrier 和 CountDownLatch 之间的区别?

  14. 虽然这两个同步器都允许线程相互等待,但它们之间的主要区别在于,在计数器达到零后,您无法重用 CountDownLatch,但即使在屏障破坏后,您也可以再次使用 CyclicBarrier。
  15. Java内存模型是什么?

  16. 内存模型是一组规则和指南,允许 Java 程序在多个内存、处理器和操作系统体系结构上确定性地运行。这对于多丝的情况尤其重要。内存模型保证了一个线程所做的更改对其他线程可见,其中之一就是发生之前关系。这种关系定义了一些规则,允许程序员预测和确定并行程序的行为。例如,发生在保证之前:
    • 线程中的每个操作都发生在该线程中按程序顺序执行的每个操作之前,也称为程序顺序规则。
    • 解锁监视器发生在同一监视器的每个后续锁定之前,也称为监视器锁定规则。
    • 对易失性字段的写入发生在该字段的每次后续读取之前,这是易失性变量规则。
    • 线程上对 Thread.start() 的调用发生在任何其他线程注意到该线程已停止之前,或者在 Thread.join() 成功之后,或者如果 Thread.isAlive() 返回 false,则 Thread.start() 规则。
    • 线程被另一个线程中断发生在被中断线程注意到中断(通过抛出 InterruptedException 或通过检查 isInterrupted())(线程的中断规则)之前。
    • 对象构造函数的结束发生在该对象的终结器开始之前,即终结器规则。
    • 如果A发生在B之前,B发生在C之前,那么A发生在C之前,这意味着happens-before保证了传递性。
  17. 什么是易失性变量?

  18. 易失性是一种特殊的修饰符,只能应用于属性。在并行 Java 程序中,如果没有同步器,不同线程对属性所做的更改对其他人来说是不可见的。Volatile 变量确保写入发生在后续读取之前,如上一个问题中的 Volatile 变量规则所述。
  19. 什么是线程安全?Vector 类安全吗?

  20. 线程安全是对象或代码的一个属性,可确保当由多个线程执行或使用时,代码将按预期运行。例如,如果在多个线程中使用相同的计数器实例,线程安全计数器将不会跳过任何计数。显然,集合类可以分为两类,线程安全的和非线程安全的。Vector 是线程安全的,并通过同步更改 Vector 状态的方法来实现这一点,另一方面,它的对应项 ArrayList 不是线程安全的。
  21. 什么是竞争条件?

  22. 竞争条件是造成细微错误的原因。顾名思义,竞争条件是由于多个线程之间的竞争而发生的;如果应该第一个执行的线程输掉了竞争,而第二个线程被执行,则代码的行为会发生变化,从而导致非确定性错误。由于线程之间竞争的混乱性质,这些是一些最难捕获和重现的错误。竞争条件的一个例子是不稳定的执行。
  23. 如何停止线程?

  24. 我一直说 Java 为一切提供了丰富的 API,但具有讽刺意味的是,它并没有提供停止线程的便捷方法。JDK 1.0 有几种控制方法,例如 stop()、suspend() 和resume(),由于潜在的死锁威胁,这些方法在未来版本中被标记为已弃用;从那时起,Java API 开发人员就不再尝试提供线程-抵抗 - 一种安全而优雅的方式来停止线程。程序员主要依赖这样一个事实:线程一旦执行完 run() 或 call() 方法就会自行停止。要手动停止,程序员可以利用 易失性布尔变量,并在每次迭代中检查其值(如果 run() 方法中存在循环),或者使用 Interrupt() 方法中断线程以突然取消作业。
  25. 当线程抛出异常时会发生什么?

  26. 这是很好的技巧问题之一。简单来说,如果未捕获异常,则线程已死亡;如果安装了未捕获异常的处理程序,它将收到回调。Thread.UncaughtExceptionHandler 是一个接口,定义为嵌套接口,用于在线程由于未捕获的异常而突然停止时调用的处理程序。当线程由于未捕获的异常而即将终止时,JVM 将使用 Thread.getUncaughtExceptionHandler() 检查 UncaughtExceptionHandler 是否存在,并调用处理程序的 uncaughtException() 方法,将线程和异常作为参数传递。
  27. 如何在两个线程之间共享数据?

  28. 您可以使用共享对象或并行数据结构(例如 BlockingQueue)在线程之间共享数据。
  29. 通知和notifyAll之间的区别?

  30. 这是另一个棘手的问题,由于一个监视器可以由多个线程监视,因此 Java API 开发人员提供了一种方法来仅通知一个或所有线程其状态发生变化,但他们只提供了一半的实现。notify() 方法无法选择特定线程,因此仅当您确定只有一个线程正在等待时它才有用。另一方面,notifyAll()通知所有线程并允许它们竞争监视器,这确保至少有一个线程向前移动。
  31. 为什么wait、notify和notifyAll不在Thread类中?

  32. 这是一个设计问题,测试候选人对现有系统的看法,或者他们是否曾经想到过类似的、乍一看不合时宜的东西。要回答这个问题,您需要提供几个原因,说明为什么这些方法在 Object 类中可以更好地实现,而为什么不在 Thread 类中实现。第一个明显的原因是 Java 支持对象级别的锁,而不是线程级别的锁。任何对象都有一个锁,它由线程获取。如果一个线程需要等待某个锁,那么在对象上调用 wait() 比在该线程上调用 wait() 更有意义。如果 wait() 是在 Thread 类中声明的,那么就不清楚线程正在等待哪个锁。简而言之,由于wait、notify和notifyAll是在锁级别操作的,因此在Object类中声明它们会更方便,因为lock指的是一个对象。
  33. 什么是 ThreadLocal 变量?

  34. ThreadLocal 变量是 Java 程序员可以使用的一种特殊类型的变量。正如状态有状态变量一样,线程也有 ThreadLocal 变量。对于创建成本高昂的对象来说,这是实现线程安全的好方法;例如,您可以使用 ThreadLocal 使 SimpleDateFormat 成为线程安全的。由于这是一个昂贵的类,因此不建议在每次调用都需要单独实例的本地范围内使用它。通过为每个线程提供自己的副本,您可以一石二鸟。首先,通过使用新的固定数量的实例来减少昂贵对象的实例数量,其次,您可以在不丢失同步和不变性的情况下实现线程安全。线程局部变量的另一个很好的例子是 ThreadLocalRandom 类,它减少了多线程环境中创建成本高昂的 Random 对象的实例数量。
  35. 什么是未来任务?

  36. FutureTask 是并行 Java 应用程序中可取消的异步计算。此类提供了基本的 Future 实现,包括启动和停止计算的方法、查询计算状态的方法以及检索结果的方法。只有计算完成后才能得到结果;如果计算尚未完成,getter 方法将会阻塞。FutureTask 对象可用于包装 Callable 和 Runnable 对象。由于FutureTask实现了Runnable,因此可以传递给Executor执行。
  37. 中断和 isInterrupted 之间的区别?

  38. Interrupted() 和 isInterrupted() 之间的主要区别是前者重置中断状态,而后者则不重置。Java 中的中断机制是使用称为中断状态的内部标志来实现的。通过调用 Thread.interrupt() 中断线程会设置此标志。当被中断的线程通过调用静态Thread.interrupted()方法检查中断状态时,中断状态将被重置。非静态 isInterrupted() 方法由一个线程用来检查另一个线程的中断状态,不会更改中断标志。按照惯例,任何通过抛出 InterruptedException 终止的方法都会重置中断标志。然而,如果另一个线程调用interrupt(),该标志总是有可能立即被再次设置。
  39. 为什么在同步块中调用wait和notify方法?

  40. 从静态块或方法调用 wait 和 notification 的主要原因是 Java API 需要它。如果您从同步块外部调用它们,您的代码将抛出 IllegalMonitorStateException。一个更聪明的原因是避免等待和通知调用之间的竞争条件。
  41. 为什么要循环检查等待状态?

  42. 如果等待线程不检查循环中的等待状态,则有可能会收到错误警告和错误唤醒调用,即使未达到该状态,它也会直接退出。当等待线程醒来时,它不会考虑它正在等待的状态可能仍然有效的事实。它实际上可能是过去的,但在调用notify()方法之后和线程唤醒之前发生了变化。因此,最好在循环内调用 wait()。
  43. 同步集合和并发集合之间的区别?

  44. 虽然同步和并发集合都提供线程安全集合,但后者更具可扩展性。在 Java 1.5 之前,程序员只能访问同步集合,当多个线程同时访问它们时,这会成为争用的根源,从而难以扩展系统。Java 5 引入了并发集合(例如 ConcurrentHashMap),它不仅提供线程安全性,而且还使用锁剥离和内部表分区等现代技术提高可伸缩性。
  45. 栈和堆的区别?

  46. 为什么在有关多线程的问题中会出现这个问题?因为栈是一块与线程密切相关的内存。每个线程都有自己的堆栈,其中存储局部变量、方法参数和调用堆栈。存储在一个线程堆栈上的变量对另一个线程不可见。另一方面,堆是所有线程共享的公共内存区域。对象,无论是本地还是任何其他级别,都是在堆上创建的。为了提高性能,线程通常将堆中的值缓存到其堆栈上,这就是易失性变量发挥作用的地方。易失性告诉线程从主内存读取变量。
  47. 什么是线程池?

  48. 创建线程在时间和资源方面都是昂贵的。如果在处理请求时创建线程,则会减慢响应时间,并且进程只能创建有限数量的线程。为了避免这些问题,在应用程序启动时创建一个线程池,并重用这些线程来处理请求。这个线程池称为“线程池”,其中的线程称为工作线程。从Java 1.5开始,Java API提供了Executor框架,它允许你创建各种线程池,比如单个线程池,单位时间只处理一个作业,固定线程池,固定数量的线程池线程数、缓存线程池、可扩展池。适合具有许多短期任务的应用程序。
  49. 如何解决生产者消费者问题?

  50. 您在现实中解决的大多数线程问题都来自生产者消费者模式,其中一个线程创建问题,第二个线程消耗它。您需要知道如何构建内部线程交互来解决这个问题。在低级别,您可以利用等待和通知方法,在高级别,您可以利用 Semaphore 或 BlockingQueue
  51. 如何避免死锁?

  52. 翻译:按主题排列的前 50 个面试问题。 第 1 部分 - 1 死锁是一种状态,其中一个线程正在等待第二个线程执行某些操作,而第二个线程同时也在等待第一个线程执行相同的操作。这是一个非常严重的问题,会导致您的程序冻结并且无法执行其设计目的。当达到这 4 种状态时就会发生死锁:
    • 互斥:不可分割模式下,至少必须占用一种资源。在任何给定时间只有一个进程可以使用资源。
    • 持有并等待:进程至少持有一种资源,并请求其他进程持有的其他资源。
    • 无预清理:如果资源已被占用,操作系统不会重新分配资源,必须自愿将资源分配给持有进程。
    • 循环等待:一个进程等待另一个进程释放资源,而另一个进程又等待第一个进程释放资源。
    避免死锁的最简单方法是避免循环等待;这可以通过按一定顺序获取锁并按相反顺序释放锁来实现。
  53. 活锁和死锁的区别?

  54. 活锁与死锁类似,只是在活锁中,所涉及的线程或进程的状态不断变化,相互依赖。活锁是资源短缺的一种特例。活锁的一个真实例子是,当两个人在狭窄的走廊里相遇时,每个人都试图保持礼貌,退到一边,因此他们不断地从一边走到另一边。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION