收下这一波2021年,最新的,Java并发面试题
1.进程和线程
1.我们对“并发”一词有什么了解?
并发是程序同时执行多个计算的能力。这可以通过将计算分布在计算机的可用CPU内核上,甚至在同一网络内的不同计算机上来实现。
2.进程和线程之间有什么区别?
进程是操作系统提供的执行环境,它具有自己的一组私有资源(例如,内存,打开的文件等)。与流程相反,线程位于流程内,并与流程的其他线程共享资源(内存,打开的文件等)。在不同线程之间共享资源的能力使线程更适合于对性能有重要要求的任务。
3.在Java中,什么是进程和线程?
在Java中,进程对应于正在运行的Java虚拟机(JVM),而线程位于JVM中,并且可以由Java应用程序在运行时动态创建和停止。
4.什么是调度程序?
调度程序是一种调度算法的实现,该算法管理进程和线程对某些有限资源(如处理器或某些I / O通道)的访问。大多数调度算法的目标是为可用的进程/线程提供某种负载平衡,以确保每个进程/线程都有适当的时间范围以排他地访问所请求的资源。
5. Java程序至少具有多少个线程?
每个Java程序都在主线程中执行;因此,每个Java应用程序都有至少一个线程。
6. Java应用程序如何访问当前线程?
可以通过调用currentThread()
JDK类的静态方法来访问当前线程java.lang.Thread
:
12345678 | public class MainThread {`` ``public static void main(String[] args) {`` ``long id = Thread.currentThread().getId();`` ``String name = Thread.currentThread().getName();`` ``...`` ``}``} |
---|---|
7.每个Java线程都有哪些属性?
每个Java线程都具有以下属性:
- 在JVM中唯一的long类型的标识符
- 类型为String的名称
- int类型的优先级
- 类型状态
java.lang.Thread.State
- 线程所属的线程组
8.线程组的目的是什么?
每个线程都属于一组线程。JDK类java.lang.ThreadGroup
提供了一些方法来处理整个线程组。使用这些方法,例如,我们可以中断组中的所有线程或设置其最大优先级。
9.线程可以具有哪些状态,每个状态的含义是什么?
NEW:
尚未启动的线程处于此状态。RUNNABLE:
在Java虚拟机中执行的线程处于这种状态。BLOCKED:
等待监视器锁定而被阻塞的线程处于此状态。WAITING:
无限期地等待另一个线程执行特定操作的线程处于此状态。TIMED_WAITING:
正在等待另一个线程执行操作的线程最多达到指定的等待时间,该线程处于此状态。TERMINATED:
退出的线程处于此状态。
10.多线程编程的好处是什么?
多线程代码可以在以下方面提供帮助:
- 改善应用程序响应能力
- 有效地使用多处理器
- 改善计划结构
- 使用更少的系统资源
11.在多线程环境中遇到哪些常见问题?
- 僵局–两个线程A和B分别持有lock_A和lock_B。他们两个都希望访问资源R。为了安全地访问R,需要lock_A和lock_B。但是线程A需要lock_B,线程B需要lock_A。但是他们两个都不准备放弃他们持有的锁。因此,没有任何进展。陷入僵局!
- 种族条件–考虑生产者-消费者的经典例子。如果您在添加或从队列中删除项目之前忘记锁定该怎么办?想象一下,两个线程A和B试图在不锁定的情况下添加项目。线程A访问队列的后面。然后,调度程序就有机会运行线程B,该线程B成功添加了项并更新了尾指针。现在,线程A读取的尾指针已过时,但它认为它是尾并添加了该项。因此,B添加的项目是LOST!数据结构已损坏!更糟糕的是,清理时还可能导致内存泄漏。
- 数据竞争–想象应该设置的标志变量。假设您已锁定以避免比赛条件。现在,不同的线程想要设置不同的值。由于调度程序可以以任何方式调度线程执行,因此您最终不知道标志的值。
- 饥饿–这是线程调度程序引起的问题。一些线程没有机会运行和完成,或者无法获得所需的锁,因为其他线程被赋予了更高的优先级。他们“饿死”了CPU周期或其他资源。
- 优先级反转–想象两个线程A和B。A具有比B高的优先级,因此比B具有更多的CPU周期。但是在访问共享资源时,B持有A所需的锁并屈服。现在,如果没有锁,A无法做任何事情,并且浪费了大量CPU周期,因为B没有获得足够的周期,但是拥有了锁。
12.我们如何设置线程的优先级?
使用方法可以设置线程的优先级setPriority(int)
。要将优先级设置为最大值,我们使用常量Thread.MAX_PRIORITY
,将其设置为最小值,我们使用常量,Thread.MIN_PRIORITY
因为这些值在不同的JVM实现之间可能会有所不同。
13.什么是多线程中的上下文切换?
纯粹的多任务处理不存在。同时执行两项具有挑战性的任务是不可能的。因此,当我们执行多任务时,我们真正要做的就是不断地从一项任务切换到另一项任务。这就是上下文切换。
该术语起源于计算机科学。但是,它同样适用于人类执行的心理任务。毕竟,人类的思想在许多方面都类似于CPU。
就像运行多线程进程的CPU在运行另一个线程时暂时搁置一个给定线程的执行一样,人类的大脑也搁置一个任务以将其重点转移到另一个任务上。
14. Java中的绿色线程和本地线程之间的区别?
- 绿色线程是指Java虚拟机本身在一个操作系统进程中创建,管理和上下文切换所有Java线程的模型。没有使用操作系统线程库。
- 本机线程是指Java虚拟机使用操作系统线程库(在UnixWare上名为libthread)创建和管理Java线程,并且每个Java线程都映射到一个线程库线程。
15.我们对种族条件一词有什么了解?
竞争条件描述的星座图,其中某些多线程实现的结果取决于参与线程的确切计时行为。在大多数情况下,具有这种行为是不希望的,因此,竞赛条件一词还意味着由于缺少线程同步而导致的错误会导致不同的结果。竞争条件的一个简单示例是两个并行线程对整数变量的递增。由于该操作由一个以上的单个原子操作组成,因此可能发生两个线程读取并增加相同值的情况。在此并发增量之后,整数变量的数量不会增加2,而只会增加1
16.将对象实例从一个线程传递到另一个线程时,您需要考虑什么?
在线程之间传递对象时,必须注意这些对象不能同时由两个线程操纵。一个示例是一个Map
实现,其键/值对由两个并发线程修改。为了避免并发修改出现问题,可以将对象设计为不可变的。
17.是否可以通过使用多线程来提高应用程序的性能?列举一些例子。
如果我们有多个CPU内核可用,并且可以在可用的CPU内核上并行化计算,则可以通过多线程来提高应用程序的性能。一个示例是应该缩放存储在本地目录结构中的所有图像的应用程序。生产者/消费者实现可以使用一个线程扫描目录结构和执行实际缩放操作的一堆工作线程,而不是一个接一个地遍历所有图像。另一个示例是镜像某些网页的应用程序。生产者线程可以解析第一个HTML页面并将发现的链接发布到队列中,而不是先加载一个HTML页面。工作线程监视队列并加载解析器找到的网页。
18.我们对可扩展性一词有什么了解?
可伸缩性是指程序通过向其添加更多资源来提高性能的能力。
19.是否可以通过使用多个处理器来计算应用程序的理论最大速度?
通过为应用程序提供多个处理器,阿姆达尔定律提供了一个计算理论上最大速度的公式。理论上的加速由S(n) = 1 / (B + (1-B)/n)
下式计算:其中n
表示处理器数量和B
无法并行执行的程序部分。当n收敛于无穷大时,项(1-B)/n
收敛于零。因此,在这种特殊情况下,公式可以简化为1/B
。正如我们所看到的,理论上最大加速比是必须依次执行的分数的倒数。这意味着该分数越低,可以实现越多的理论加速。
20.提供一个示例,说明为什么单线程应用程序的性能改进会导致多线程应用程序的性能下降。
此类优化的一个突出示例是将List
元素数量保持为单独变量的实现。由于该size()
操作不必遍历所有元素,而是可以直接返回当前数量的元素,因此可以提高单线程应用程序的性能。在多线程应用程序中,由于多个并发线程可能会将元素插入列表,因此必须通过锁来保护其他计数器。当列表的更新次数超过size()
操作的调用次数时,此附加锁定可能会降低性能。
2.线程对象
2.1定义和启动线程
21.如何用Java创建线程?
基本上,有两种方法可以用Java创建线程。
第一个是编写一个扩展JDK类java.lang.Thread
并调用其方法的类start()
:
public` `class` `MyThread ``extends` `Thread {`` ``public` `MyThread(String name) {`` ``super``(name);`` ``}`` ``@Override`` ``public` `void` `run() {`` ``System.out.println(``"Executing thread "``+Thread.currentThread().getName());`` ``}`` ``public` `static` `void` `main(String[] args) ``throws` `InterruptedException {`` ``MyThread myThread = ``new` `MyThread(``"myThread"``);`` ``myThread.start();`` ``}``}
第二种方法是实现接口java.lang.Runnable
,并将此实现作为参数传递给的构造函数java.lang.Thread
:
public` `class` `MyRunnable ``implements` `Runnable {`` ``public` `void` `run() {`` ``System.out.println(``"Executing thread "``+Thread.currentThread().getName());`` ``}`` ``public` `static` `void` `main(String[] args) ``throws` `InterruptedException {`` ``Thread myThread = ``new` `Thread(``new` `MyRunnable(), ``"myRunnable"``);`` ``myThread.start();`` ``}``}
22.为什么不应该通过调用其方法来停止线程stop()
?
线程不应该被使用过时的方法来停止stop()
的java.lang.Thread
,因为该方法的调用导致线程解锁已获取所有的显示器。如果受释放锁之一保护的任何对象处于不一致状态,则此状态对所有其他线程可见。当其他线程对此不一致的对象进行处理时,这可能导致任意行为。
23.是否可以启动一个线程两次?
否,在通过调用线程的start()
方法启动线程之后,第二次调用start()
将抛出IllegalThreadStateException
。
24.以下代码的输出是什么?
public class MultiThreading {`` ``private static class MyThread ``extends Thread {`` ``public MyThread(String name) {`` ``super``(name);`` ``}`` ``@Override`` ``public void run() {`` ``System.out.println(Thread.currentThread().getName());`` ``}`` ``}`` ``public static void main(String[] args) {`` ``MyThread myThread = ``new MyThread(``"myThread"``);`` ``myThread.run();`` ``}``} | |
---|---|
上面的代码产生输出“ main”而不是“ myThread”。从该main()
方法的第二行可以看出,我们错误地调用run()
而不是方法start()
。因此,没有启动新线程,但是该方法run()
在主线程中执行。
25.什么是守护线程?
守护程序线程是当JVM决定是否应该停止时,不评估其执行状态的线程。当所有用户线程(与守护程序线程相反)终止时,JVM停止。因此,一旦所有用户线程停止,守护程序线程就可以用于实现监视功能,例如,JVM停止了该线程:
public class Example {`` ``private static class MyDaemonThread ``extends Thread {`` ``public MyDaemonThread() {`` ``setDaemon(``true``);`` ``}`` ``@Override`` ``public void run() {`` ``while (``true``) {`` ``try {`` ``Thread.sleep(``1``);`` ``} ``catch (InterruptedException e) {`` ``e.printStackTrace();`` ``}`` ``}`` ``}`` ``}`` ``public static void main(String[] args) ``throws InterruptedException {`` ``Thread thread = ``new MyDaemonThread();`` ``thread.start();`` ``}``} | |
---|---|
即使守护线程仍在其无尽的while循环中运行,以上示例应用程序也会终止。
26.什么是Java线程转储?
Java线程转储是一种找出JVM中每个线程在特定时间点正在做什么的方法。
如果您的Java应用程序有时在负载下运行时挂起,这将特别有用,因为对转储的分析将显示线程卡在哪里。
您可以在Unix / Linux下通过运行kill -QUIT 来生成线程转储,而在Windows下则可以通过按Ctl + Break来生成线程转储。
27.是否可以在启动普通用户线程后将其转换为守护线程?
用户线程一旦启动就无法转换为守护线程。thread.setDaemon(true)
在已经运行的线程实例上调用该方法会导致IllegalThreadStateException
。
28.忙碌等待使我们了解什么?
繁忙等待是指通过执行一些活动计算来等待事件的实现,这些计算使线程/进程占用了处理器,尽管调度程序可以将其从处理器中删除。繁忙等待的一个示例是将等待时间花费在一个循环中,该循环一次又一次地确定当前时间,直到到达某个时间点为止:
Thread thread = ``new` `Thread(``new` `Runnable() {`` ``@Override`` ``public` `void` `run() {`` ``long` `millisToStop = System.currentTimeMillis() + ``5000``;`` ``long` `currentTimeMillis = System.currentTimeMillis();`` ``while` `(millisToStop > currentTimeMillis) {`` ``currentTimeMillis = System.currentTimeMillis();`` ``}`` ``}``});
29. Java中的wait()和sleep()方法之间有什么区别?
等待():
- wait()方法释放锁定。
- wait()是Object类的方法。
- wait()是非静态方法–公共最终void wait()引发InterruptedException {//…}
- 应该通过notify()或notifyAll()方法通知wait()。
- 需要从循环中调用wait()方法以处理错误警报。
- 必须从同步上下文(即同步方法或块)中调用wait()方法,否则它将引发IllegalMonitorStateException
sleep():
- sleep()方法不会释放锁。
- sleep()是java.lang.Thread类的方法。
- sleep()是静态方法–公共静态无效睡眠(长毫秒,int nanos)抛出InterruptedException {//…}
- 在指定的时间后,sleep()完成。
- sleep()最好不要从循环中调用(即参见下面的代码)。
- sleep()可以从任何地方调用。没有具体要求。
30.当未捕获的异常离开run()
方法时会发生什么?
我可能碰巧一个未经检查的异常从run()
方法中逃逸了。在这种情况下,线程由Java虚拟机停止。通过将实现接口的实例注册UncaughtExceptionHandler
为异常处理程序,可以捕获此异常。
这可以通过调用static方法来完成,该方法Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler)
告诉JVM在线程自身上没有注册任何特定的处理程序的情况下使用提供的处理程序,或者通过setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler)
在线程实例本身上进行调用。
31.两个接口Runnable
和
**Callable?**
接口Runnable
定义了run()
没有任何返回值Callable
的方法call()
,而接口则允许方法返回值并抛出异常。
32.什么是关机钩?
关闭钩子是在JVM关闭时执行的线程。可以通过addShutdownHook(Runnable)
在Runtime实例上调用来注册它:
Runtime.getRuntime().addShutdownHook(``new` `Thread() {`` ``@Override`` ``public` `void` `run() {`` ``}``});
2.2通过sleep暂停执行
33.如何防止繁忙的等待?
防止繁忙等待的一种方法是使当前线程在给定的时间内休眠。这可以java.lang.Thread.sleep(long)
通过传递方法将进入睡眠的毫秒数作为参数来调用该方法来完成。
34.我们可以Thread.sleep()
用于实时处理吗?
传递给的毫秒数Thread.sleep(long)
仅表示调度程序不需要当前线程执行多长时间。根据实际的实现,调度程序可能会让线程提前或延迟几毫秒再执行一次。因此,Thread.sleep()
不应将的调用用于实时处理。
35.该方法Thread.yield()
做什么?
静态方法的调用为Thread.yield()
调度程序提供了一个提示,即当前线程愿意释放处理器。调度程序可以随意忽略此提示。由于未定义调用哪个线程将获得处理器Thread.yield()
,因此甚至有可能当前线程成为要执行的“下一个”线程。
36.该类有哪些用例java.util.concurrent.Future
?
该类的实例java.util.concurrent.Future
用于表示异步计算的结果,这些结果无法立即获得。因此,该类提供了一些方法来检查异步计算是否已完成,取消任务并检索实际结果。后者可以通过提供的两种get()
方法来完成。第一个get()
方法不带参数,并阻塞直到结果可用为止;第二个get()
方法带一个超时参数,如果结果在给定的时间范围内不可用,则该超时参数使方法调用返回。
37.我们如何在Java中停止线程?
要停止线程,可以使用指向当前线程的易失性引用,该引用可以被其他线程设置为null,以指示当前线程应停止其执行:
private static class MyStopThread ``extends Thread {`` ``private volatile Thread stopIndicator;`` ``public void start() {`` ``stopIndicator = ``new Thread(``this``);`` ``stopIndicator.start();`` ``}`` ``public void stopThread() {`` ``stopIndicator = ``null``;`` ``}`` ``@Override`` ``public void run() {`` ``Thread thisThread = Thread.currentThread();`` ``while``(thisThread == stopIndicator) {`` ``try {`` ``Thread.sleep(``1000``);`` ``} ``catch (InterruptedException e) {`` ``}`` ``}`` ``}``} | |
---|---|
2.3中断
38.如何唤醒使用前已进入睡眠状态的线程Thread.sleep()
?
该方法interrupt()
的java.lang.Thread
中断睡眠线程。已通过调用进入睡眠状态的中断线程Thread.sleep()
被唤醒InterruptedException
:
public` `class` `InterruptExample ``implements` `Runnable {`` ``public` `void` `run() {`` ``try` `{`` ``Thread.sleep(Long.MAX_VALUE);`` ``} ``catch` `(InterruptedException e) {`` ``System.out.println(``"["``+Thread.currentThread().getName()+``"] Interrupted by exception!"``);`` ``}`` ``}`` ``public` `static` `void` `main(String[] args) ``throws` `InterruptedException {`` ``Thread myThread = ``new` `Thread(``new` `InterruptExample(), ``"myThread"``);`` ``myThread.start();`` ``System.out.println(``"["``+Thread.currentThread().getName()+``"] Sleeping in main thread for 5s..."``);`` ``Thread.sleep(``5000``);`` ``System.out.println(``"["``+Thread.currentThread().getName()+``"] Interrupting myThread"``);`` ``myThread.interrupt();`` ``}``}
39.线程如何查询是否已被中断?
如果线程不在Thread.sleep()
抛出的方法之内InterruptedException
,则该线程可以通过调用静态方法Thread.interrupted()
或isInterrupted()
从其继承的方法来查询是否已被中断。java.lang.Thread.
40.应该如何InterruptedException
处理?
像sleep()
和join()
抛出这样的方法InterruptedException
告诉调用者另一个线程已中断该线程。在大多数情况下,这样做是为了告诉当前线程停止其当前计算并意外完成它们。因此,通过捕获异常并仅将其记录到控制台或某些日志文件中来忽略该异常通常不是处理此类异常的适当方法。此异常的问题在于,run()
Runnable接口的方法不允许run()
抛出任何异常。因此,仅将其重新添加无济于事。这意味着的实现run()
必须自己处理此检查的异常,这通常会导致其被捕获和忽略的事实。
2.4加入
41.在启动子线程之后,我们如何在父线程中等待子线程的终止?
通过调用join()
线程的实例变量上的方法来等待线程终止:
Thread thread = ``new` `Thread(``new` `Runnable() {`` ``@Override`` ``public` `void` `run() {`` ``}``});``thread.start();``thread.join();
42.以下程序的输出是什么?
上面的代码的输出为“ false”。尽管MyDaemonThread的实例是守护程序线程,但是的调用join()
会使主线程等待,直到守护程序线程的执行完成。因此,调用isAlive()
线程实例显示守护程序线程不再运行。
3.同步
43.同步方法和同步块之间的区别?
- 同步块减小了锁定范围,但是同步方法的锁定范围是整个方法。
- 同步块的性能更好,因为只有关键部分被锁定,但是同步方法的性能比块差。
- 同步块提供了对锁的精细控制,但对此对象或类级别锁表示的当前对象提供了同步方法锁。
- 同步块可以抛出NullPointerException,但同步方法不会抛出。
- 同步块:同步(此){}
- 同步方法:公共同步void fun(){}
44.什么是Java中的volatile关键字,它与Java中的同步方法有何不同?
使用volatile强制线程直接从RAM存储器读取和写入变量。因此,当许多线程都使用相同的volatile变量时,它们都将看到RAM内存中存在的最新版本,而不是高速缓存中可能的旧副本。当线程进入同步块时,它需要控制监视变量。所有其他线程等待,直到第一个线程从同步块退出。为了确保所有线程都能看到相同的修改,同步块中使用的所有变量都直接从RAM内存而不是从缓存副本读取和写入。
45.同步关键字用于什么目的?
当您必须实现对资源的互斥访问(例如某些静态值或某些文件引用)时,可以使用同步块包含与互斥资源一起使用的代码:
synchronized` `(SynchronizedCounter.``class``) {`` ``counter++;``}
46.什么是信号量?
信号量是一种数据结构,它维护一组必须由竞争线程获取的许可。因此,信号量可用于控制有多少线程同时访问关键部分或资源。因此,的构造函数java.util.concurrent.Semaphore
将线程竞争的许可数量作为第一个参数。每次调用其acquire()
方法都会尝试获取可用许可之一。acquire()
在下一个允许获得可用之前,没有任何参数块的方法。稍后,当线程完成对关键资源的工作时,它可以通过release()
在Semaphore实例上调用该方法来释放许可。
47.什么是CountDownLatch
?
SDK类CountDownLatch
提供了一个同步辅助工具,可用于实现以下场景:线程必须等待,直到其他一些线程达到相同状态才能启动所有线程。通过提供一个递减的同步计数器,直到其达到零值,即可完成此操作。CountDownLatch
实例达到零后,将允许所有线程继续进行。可以使用计数器的值1来让所有线程在给定的时间点启动,也可以等待直到多个线程完成。在后一种情况下,计数器将使用线程数进行初始化,并且每个完成其工作的线程都会将锁存器递减一个。
48. aCountDownLatch
和a有CyclicBarrier
什么区别?
这两个SDK类都在内部维护一个计数器,该计数器由不同的线程递减。线程等待直到内部计数器达到零值,然后从那里继续。但是与CountDownLatch
类相反,该类CyclicBarrier
将值重置为零后将内部值重置为初始值。顾名思义,CyclicBarrier
因此的实例可用于实现用例,其中线程必须一次又一次地等待彼此。
3.1内部锁和同步
49.同步方法获得什么内在锁?
同步方法获取该方法对象的固有锁定,并在方法返回时释放该锁定。即使该方法引发异常,也将释放固有锁定。因此,同步方法等于以下代码:
public` `void` `method() {`` ``synchronized``(``this``) {`` ``...`` ``}``}
50.如果两个线程同时在不同的对象实例上调用同步方法,这些线程之一会阻塞吗?
两种方法都锁定同一监视器。因此,您不能同时在不同线程上的同一对象上执行它们(两种方法之一将阻塞,直到另一种方法完成)。
51.构造函数可以同步吗?
否,构造函数无法同步。之所以导致语法错误,是因为只有构造线程才有权访问正在构造的对象。
52. Lock与同步相比有什么好处?
锁的优点是:
- 有可能使它们公平
- 在等待Lock对象时,可以使线程响应中断。
- 可以尝试获取锁,但是如果无法获取锁,则立即返回或在超时后返回
- 可以在不同的范围内以不同的顺序获取和释放锁
53.原始值可以用于内部锁吗?
不可以,原始值不能用于内部锁。
54.内在锁是否可重入?
是的,同一线程可以一次又一次地访问固有锁。否则,获取锁的代码将必须注意,它不会偶然尝试获取已获取的锁。
55.我们对公平锁有什么了解?
选择是通过障碍的一些独家资源的下一个线程时,一个公平的锁占用线程的等待时间考虑在内。公平锁的示例实现由Java SDK提供:java.util.concurrent.locks.ReentrantLock
。如果使用布尔标志设置为true的构造函数,则ReentrantLock授予对等待时间最长的线程的访问权限。
56. SDK类ReadWriteLock使用了哪种减少锁争用的技术?
SDK类ReadWriteLock
使用以下事实:并发线程在没有其他线程尝试更新值的情况下想要读取值时不必获取锁。这由一对锁实现,一个锁用于只读操作,一个锁用于写操作。尽管可以通过多个线程获得只读锁,但是该实现保证释放写锁后,所有读取操作都将看到更新的值。
3.2原子访问
57.我们通过原子操作了解什么?
原子操作是完全执行或根本不执行的操作。
58.语句c ++是原子的吗?
不,一个整数变量的增量包括一个以上的运算。首先,我们必须加载c的当前值,将其递增,然后最后将新值存储回去。执行此增量的当前线程可能会在这三个步骤中的任何一个之间中断,因此此操作不是原子操作。
59. Java中哪些操作是原子操作?
Java语言提供了一些原子性的基本操作,因此可用于确保并发线程始终看到相同的值:
- 对参考变量和原始变量(长整型和双精度型除外)的读写操作
- 对声明为易失性的所有变量的读写操作
4.活泼
4.1死锁
60.我们对僵局有什么了解?
死锁是一种情况,其中两个(或更多)线程各自在另一个线程上等待以释放其已锁定的资源,而线程本身已锁定另一个线程在等待的资源:
- 线程1:锁定资源A,等待资源B
- 线程2:锁定资源B,等待资源A
61.僵局情况有哪些要求?
通常,可以确定以下死锁要求:
- 互斥:有一种资源在任何时间点只能由一个线程访问。
- 资源持有:锁定一个资源后,线程尝试获取对某个其他排他资源的另一个锁定。
- 无抢占:没有机制,如果一个线程在特定时间段内持有锁,则该机制可以释放资源。
- 循环等待:在运行时发生一个星座,其中两个(或更多)线程分别在另一个线程上等待以释放已锁定的资源。
62.完全可以防止死锁吗?
为了防止死锁,必须消除一个或多个死锁的要求:
- 互斥:在某些情况下,可以通过使用乐观锁定来防止互斥。
- 资源持有:线程无法成功获取所有排他锁时,可能会释放其所有排他锁。
- 无抢占:对独占锁使用超时将在给定时间后释放锁。
- 循环等待:当所有线程以相同顺序获得所有排他锁时,不会发生循环等待。
63.是否可以实现死锁检测?
当监视所有排他锁并将其建模为有向图时,死锁检测系统可以搜索两个线程,每个线程都在等待另一个线程以释放已锁定的资源。然后可以通过某种异常强制等待线程释放另一个线程正在等待的锁。
4.2饥饿和活锁
64.什么是活锁?
活锁是这样的情况,其中两个或更多线程通过响应另一个线程引起的动作而相互阻塞。与死锁情况相反,死锁情况是两个或多个线程在一个特定状态下等待,而参与活动锁的线程则以防止其正常工作进展的方式更改其状态。一个示例是这样一种情况,其中两个线程尝试获取两个锁,但是在无法获取第二个锁时释放它们获取的锁。现在可能会发生,两个线程同时尝试获取第一个线程。由于只有一个线程成功,因此第二线程可以成功获取第二锁。现在,两个线程都持有两个不同的锁,但是由于两个线程都想要拥有两个锁,因此它们释放其锁并从头开始尝试。现在可能一次又一次地发生这种情况。
65.通过线程饥饿我们了解什么?
具有较低优先级的线程比具有较高优先级的线程获得的执行时间更少。当具有较低优先级的线程执行长时间的持久计算时,可能会发生这些线程没有足够的时间及时完成其计算的情况。它们似乎“饿死了”,因为具有更高优先级的线程会占用它们的计算时间。
66.同步块会导致线程饥饿吗?
没有定义线程可以进入同步块的顺序。因此,从理论上讲,如果许多线程正在等待同步块的入口,则某些线程必须比其他线程等待更长的时间。因此,他们没有足够的计算时间来及时完成工作。
5.守卫块
67.每个对象继承的哪两种方法java.lang.Object
可用于实现简单的生产者/消费者方案?
当工作线程完成其当前任务并且新任务的队列为空时,它可以通过获取队列对象的内在锁并调用方法来释放处理器wait()
。该生产者线程将唤醒该线程,该生产者线程将新任务放入队列中,并再次获取队列对象的相同内在锁并对其进行调用notify()
。
68.notify()
和之间有什么区别notifyAll()
?
两种方法都用于唤醒一个或多个通过调用进入睡眠状态的线程wait()
。虽然notify()
仅唤醒其中一个等待线程,但notifyAll()
唤醒所有等待线程。
69.如何确定通过调用唤醒哪个线程notify()
?
notify()
如果有多个线程正在等待,则不指定将通过调用唤醒哪些线程。因此,代码不应依赖任何具体的JVM实现。
70
下面的从某些队列实现中检索整数值的代码是否正确?
public Integer getNextInt() {`` ``Integer retVal = ``null``;`` ``synchronized (queue) {`` ``try {`` ``while (queue.isEmpty()) {`` ``queue.wait();`` ``}`` ``} ``catch (InterruptedException e) {`` ``e.printStackTrace();`` ``}`` ``}`` ``synchronized (queue) {`` ``retVal = queue.poll();`` ``if (retVal == ``null``) {`` ``System.err.println(``"retVal is null"``);`` ``throw new IllegalStateException();`` ``}`` ``}`` ``return retVal;``} | |
---|---|
尽管上面的代码将队列用作对象监视器,但是在多线程环境中它无法正确运行。原因是它有两个单独的同步块。当第6行中的另一个线程唤醒了两个线程时,两个线程在另一个notifyAll()
同步块中一个接一个地输入。如果队列的第二个块现在只有一个新值,那么第二个线程将轮询一个空队列并获得null作为返回值。
6.不可变的对象
71.什么是不可变对象
- 不变对象可以通过任何机制发布
- 不可变对象可以被任何线程安全地使用,而无需额外的同步,即使不使用同步来发布它们。
72.如何创建一个不可变的对象
要创建一个不变的对象,您需要:
- 不要添加任何设置方法
- 声明所有字段的最终和私有
- 如果字段是可变对象,则为获取方法创建其防御性副本
- 如果必须将传递给构造函数的可变对象分配给字段,请为其创建防御性副本
- 不允许子类覆盖方法。
73.为了实现不可变的类,您必须遵循哪些规则?
-
所有字段均应为最终字段和私有字段。
-
不应使用setter方法。
-
应该将类本身声明为final,以防止子类违反不变性原则。
-
如果字段不是原始类型,而是对另一个对象的引用:
-
- 不应有将引用直接暴露给调用者的getter方法。
- 不要更改引用的对象(或者至少更改这些引用对对象的客户端不可见)。
7,锁定对象
74.是否可以检查线程是否在某个给定对象上持有监视器锁定?
该类java.lang.Thread
提供静态方法Thread.holdsLock(Object)
,当且仅当当前线程持有作为方法调用的参数给定的对象上的锁时,该方法才返回true。
75.通过锁争用我们了解什么?
当两个或多个线程在获取锁中竞争时,发生锁争用。调度程序必须决定是否让必须等待睡眠的线程并执行上下文切换以让另一个线程占用CPU,或者让等待线程忙于等待是否更有效率。两种方式都会将空闲时间引入劣质线程。
76.哪些技术有助于减少锁争用?
在某些情况下,可以通过应用以下技术之一来减少锁争用:
- 锁的范围减小了。
- 减少获取某个锁的次数(锁拆分)。
- 使用硬件支持的乐观锁定操作而不是同步。
- 尽可能避免同步。
- 避免对象池。
77.可以将哪种减少锁争用的技术应用于以下代码?
synchronized` `(map) {`` ``UUID randomUUID = UUID.randomUUID();`` ``Integer value = Integer.valueOf(``42``);`` ``String key = randomUUID.toString();`` ``map.put(key, value);``}
上面的代码执行随机UUID的计算,并将文字42转换为同步块内的Integer对象,尽管这两行代码对于当前线程而言是本地的,并不影响其他线程。因此,可以将它们移出同步块:
UUID randomUUID = UUID.randomUUID();``Integer value = Integer.valueOf(``42``);``String key = randomUUID.toString();``synchronized` `(map) {`` ``map.put(key, value);``}
78.举例说明技术锁拆分。
当一个锁用于同步对同一应用程序不同方面的访问时,锁拆分可能是减少锁争用的一种方法。假设我们有一个实现对应用程序的某些统计数据进行计算的类。此类的第一个版本在每个方法签名中使用关键字sync,以便在多个并发线程损坏之前保护内部状态。这也意味着每个方法调用都可能导致锁争用,因为其他线程可能会尝试同时获取同一锁。但是对于每种方法中每种统计数据类型,可以将对象实例上的锁拆分为几个较小的锁。因此,试图递增统计数据D1的线程T1不必等待锁定,而线程T2同时更新数据D2。
79.通过锁条我们了解什么?
与锁拆分相反,在锁拆分中,我们针对应用程序的不同方面引入了不同的锁,而锁条使用了多个锁来保护同一数据结构的不同部分。这种技术的一个示例是ConcurrentHashMap
JDKjava.util.concurrent
软件包中的类。该Map
实现使用内部不同的存储桶来存储其值。通过值的键选择存储桶。ConcurrentHashMap
现在使用不同的锁来保护不同的哈希桶。因此,一个尝试访问第一个哈希存储桶的线程可以获取该存储桶的锁,而另一个线程可以同时访问第二个存储桶。与同步版本HashMap
相比,当不同线程在不同存储桶上工作时,此技术可以提高性能。
80. Java Concurrency API中的Lock接口是什么?
java.util.concurrent.locks.Lock接口用作类似于同步块的线程同步机制。新的锁定机制比同步块更灵活,并提供更多选项。
锁和同步块之间的主要区别如下:
- 保证顺序?同步块不提供对等待线程进行访问的顺序的任何保证。锁接口处理它。
- 没有超时?如果未授予锁定,则同步块没有超时选项。锁定界面提供了这种选择。
- 单一方法?同步块必须完全包含在一个方法中,而锁接口的方法lock()和unlock()可以在不同的方法中调用。
8,执行人
8.1执行器接口
81. ExecutorService在计时器上的优点
- 与ExecutorService不同,Timer无法利用可用的CPU内核,特别是在使用ExecutorService之类的多个任务(例如ForkJoinPool)时
- 如果您需要在多个任务之间进行协调,则ExecutorService提供了协作式API。假设您必须提交N个工作者任务,然后等待所有任务完成。您可以使用invokeAll API轻松实现它。如果要通过多个Timer任务实现相同的目标,这将不那么简单。
- ThreadPoolExecutor提供了更好的API,用于管理线程生命周期。
82. Executor和ExecutorService这两个接口之间是什么关系?
该接口Executor
仅定义一种方法:execute(Runnable)
。该接口的实现将必须在将来的某个时间执行给定的Runnable实例。该ExecutorService
接口是该接口的扩展,Executor
并提供了其他方法来关闭基础实现,等待所有提交的任务终止,并且允许提交的实例Callable
。
83.当您submit()
对队列已满的ExecutorService实例执行新任务时会发生什么?
如的方法签名submit()
所示,该ExecutorService
实现应该抛出RejectedExecutionException
。
84.什么是ScheduledExecutorService?
接口ScheduledExecutorService
扩展了接口ExecutorService
并添加了方法,该方法允许将新任务提交给应该在给定时间点执行的基础实现。有两种方法可以安排一次性任务,而有两种方法可以创建和执行定期任务。
8.2线程池
85.您知道一种简单的方法来构造具有5个线程的线程池,该线程池执行一些返回值的任务吗?
SDK提供了一个factory和utility类,Executors
其静态方法newFixedThreadPool(int nThreads)
允许创建具有固定线程数的线程池(MyCallable
省略了的实现):
public` `static` `void` `main(String[] args) ``throws` `InterruptedException, ExecutionException {`` ``ExecutorService executorService = Executors.newFixedThreadPool(``5``);`` ``Future<Integer>[] futures = ``new` `Future[``5``];`` ``for` `(``int` `i = ``0``; i < futures.length; i++) {`` ``futures[i] = executorService.submit(``new` `MyCallable());`` ``}`` ``for` `(``int` `i = ``0``; i < futures.length; i++) {`` ``Integer retVal = futures[i].get();`` ``System.out.println(retVal);`` ``}`` ``executorService.shutdown();``}
86.是否可以在Java 8中使用线程池执行流操作?
集合提供了一种parallelStream()
创建由线程池处理的流的方法。或者,您可以parallel()
在给定流上调用中间方法,以将顺序流转换为并行对应流。
87.对象池是否始终可以提高多线程应用程序的性能?
试图通过池化来避免构建新对象的对象池可以提高单线程应用程序的性能,因为通过从池中请求新对象来交换对象创建成本。在多线程应用程序中,此类对象池必须具有对该池的同步访问权限,并且锁争用的额外成本可能会超过新对象的额外构造和垃圾回收所节省的成本。因此,对象池可能无法始终提高多线程应用程序的整体性能。
8.3前叉/连接
88.我们如何访问并行流操作所使用的线程池?
并行流操作所使用的线程池可以通过访问ForkJoinPool.commonPool()
。这样,我们可以使用来查询其并行度commonPool.getParallelism()
。水平不能在运行时改变,但它可以通过提供以下JVM参数来配置:-Djava.util.concurrent.ForkJoinPool.common.parallelism=5
。
89.使用Fork / Join框架可以解决哪些任务?
Fork / Join Framework的基类java.util.concurrent.ForkJoinPool
基本上是一个线程池,它执行的实例java.util.concurrent.ForkJoinTask
。该类ForkJoinTask
提供了fork()
和的两种方法join()
。当fork()
用于启动任务的异步执行时,该方法join()
用于等待计算结果。因此,Fork / Join框架可用于实现分而治之的算法,其中将更复杂的问题分为多个更小且更容易解决的问题。
90.是否可以使用Fork / Join-Framework在数字数组中找到最小的数字?
通过使用分而治之算法可以解决在数字数组中找到最小数字的问题。可以轻松解决的最小问题是两个数字组成的数组,因为我们可以通过一个比较直接确定两个数字中较小的一个。使用分治法将初始数组分为相等长度的两个部分,并将这两个部分提供给RecursiveTask
扩展该类的两个实例ForkJoinTask
。通过分叉这两个任务,它们将被执行,或者直接解决问题(如果数组的切片长度为2),或者再次递归将数组分为两个部分,并派生两个新的RecursiveTasks。最后,每个任务实例返回其结果(通过直接计算结果或通过等待两个子任务)。然后,根任务返回数组中的最小数字。
91.这两个类RecursiveTask
和和有RecursiveAction
什么区别?
与RecursiveTask
方法相反compute()
,RecursiveAction
不必返回值。因此,RecursiveAction
当操作直接在某些数据结构上执行而不必返回计算值时,可以使用该操作。
9,并发集合
92.什么是并发收集类?
Java Collection类是快速失败的,这意味着如果在使用迭代器遍历某个线程的同时更改了Collection,则iterator.next()将抛出ConcurrentModificationException。
93.什么是Java内存模型?
Java内存模型描述了Java编程语言中的线程如何通过内存进行交互。连同代码的单线程执行描述一起,内存模型提供了Java编程语言的语义。
94.线程安全之间有什么区别HashMap
,Hashtable
特别是在线程安全方面有什么区别?
的方法Hashtable
都是同步的。HashMap
实施情况并非如此。因此Hashtable
是线程安全的,而HashMap
不是线程安全的。因此,对于单线程应用程序,使用“较新的”HashMap
实现更为有效。
95.有一个简单的方法来创建一个任意执行的同步实例Collection
,List
还是Map
?
实用程序类Collections提供了方法synchronizedCollection(Collection)
,synchronizedList(List)
并且synchronizedMap(Map)
返回了由给定实例支持的线程安全的collection / list / map。
10,阻塞队列
96.什么是BlockingQueue?
BlockingQueue是一个Java队列,它支持以下操作:在检索和删除元素时等待队列变为非空,并在添加元素时等待队列中的空间变为可用。接口TransferQueue已添加。这是对BlockingQueue接口的改进,生产者可以在其中等待消费者接收元素。
11,原子变量
97.通过CAS操作我们了解什么?
CAS代表比较交换,并且意味着处理器提供单独的指令,仅在提供的值等于当前值时才更新寄存器的值。CAS操作可用于避免同步,因为线程可以通过向CAS操作提供其当前值和新值来尝试更新值。如果另一个线程同时更新了该值,则该线程的值不等于当前值,并且更新操作失败。然后,线程读取新值,然后重试。这样,必要的同步就可以通过乐观的旋转等待来交换。
98.哪些Java类使用CAS操作?
程序包中的SDK类java.util.concurrent.atomic
类似于AtomicInteger
或在AtomicBoolean
内部使用CAS操作来实现并发增量。
public` `class` `CounterAtomic {`` ``private` `AtomicLong counter = ``new` `AtomicLong();`` ``public` `void` `increment() {`` ``counter.incrementAndGet();`` ``}`` ``public` `long` `get() {`` ``return` `counter.get();`` ``}``}
12.并发随机数
99.上课的目的是java.lang.ThreadLocal
什么?
由于内存在不同线程之间共享,因此ThreadLocal
提供了一种分别存储和检索每个线程的值的方法。ThreadLocal
独立存储和检索每个线程的值的实现,这样,当线程A存储值A1且线程B将值B1存储在的同一实例中时ThreadLocal
,线程A稍后从该ThreadLocal
实例中检索值A1 ,线程B检索值B1。
100.有哪些可能的用例java.lang.ThreadLocal
?
的实例ThreadLocal
可用于在整个应用程序中传输信息,而无需在方法之间传递此信息。示例是在实例中传输安全性/登录信息ThreadLocal
,以便每种方法均可访问它。另一个用例是传输事务信息或通常在所有方法中都应可访问的对象,而无需在方法之间传递它们。