多线程和并发面试问题与答案 – 终极列表(PDF下载)
编者按: 并发一直是开发者的挑战,编写并发程序可以是非常困难的。当涉及并发,有一系列事情可能会出现和系统复杂性大幅提高。但是,编写健壮的并发程序的能力是开发者的一个伟大的工具,可以帮助建立复杂的企业级的应用。在这篇文章中,我们将讨论可用于编程面试的不同类型问题,以评估面试者对并发和多线程的的认识问题。这些问题不仅是Java的特异性,同时也关于常规的编程原则。享受
目录
- 1. 什么是我们理解的并发?
- 2. 进程和线程之间的区别是什么?
- 3. 在Java中,什么是进程和线程?
- 4. 什么是调度器?
- 5. 一个Java程序至少有多少个线程?
- 6. 如何在Java应用程序中访问当前线程?
- 7. 每一个Java线程有哪些特性?
- 8. 线程组的目的是什么?
- 9. 线程有什么状态,每一个状态的意思是什么?
- 10. 我们如何设置线程的优先级?
- 11. 如何在Java创建线程?
- 12. 我们如何停止在Java的线程?
- 13. 为什么一个线程不会被调用的方法stop()停止?
- 14. 是否有可能启动一个线程两次?
- 15. 下列代码的输出结果是什么?
- 16. 什么是守护线程?
- 17. 有可能当一个普通用户线程已经开始了,将它转为守护线程吗?
- 18. 我们怎么理解忙等待?
- 19. 怎么避免忙等待?
- 20. 我们能否把Thread.sleep()用于实时处理?
- 21. 一个线程在被放在使用线程休眠Thread.sleep()前怎么被唤醒?
- 22. 怎么查询一个线程是否已经中断?
- 23. 如何处理一个InterruptedException异常?
- 24. 在启动子线程后,我们如何在父线程等待子线程终止?
- 25. 下列程序的输出是什么?
- 26. 当未捕获的异常逃逸 run()方法?
- 27. 什么是关闭钩子?
- 28. 关键字synchronized什么目的下使用?
- 29. 同步方法获取有什么内在锁?
- 30. 构造方法可以同步?
- 31. 原始值可以用于内部锁?
- 32. 内部锁重入么?
- 33. 我们怎么理解原子操作?
- 34. 语句C + +是否原子性?
- 35. 什么操作是原子操作?
- 36. 下面代码是否为线程安全?
- 37. 我们所理解的死锁?
- 38. 一个死锁的情况有什么要求?
- 39. 是否可能防止死锁的发生呢?
- 40. 有可能实现一个死锁检测?
- 41. 活锁是什么?
- 42. 我们所理解的线程饥饿?
- 43. 同步块会引起线程饥饿?
- 44. 我们所理解的并发冲突?
- 45. 我们所理解的公平锁?
- 46. 这两种方法可以让每个继承java.lang.Object的对象用来实现一个简单的生产者/消费者的情况吗?
- 47. notify() 和 notifyAll()之间的区别是什么?
- 48. 如何确定哪个线程通过调用 notify()?
- 49. 以下代码,从队列检索一个整型值的实现是否正确?
- 50. 是否可能检查一个线程在一些给定的对象持有监控锁?
- 51. 什么是线程的方法 Thread.yield() do?
- 52. 当传递对象实例从一个线程到另一个你要考虑什么?
- 53. 为了实现一个不可变类你必须遵守的规则?
- 54. 类 java.lang.ThreadLocal的目的是什么?
- 55. 什么情况可能使用 java.lang.ThreadLocal?
- 56. 可以通过使用多线程来提高应用程序的性能?举例.
- 57. 我们所理解的术语的可扩展性?
- 58. 是否可能为应用计算通过使用多个处理器的理论上最大速度?
- 59. 我们所理解的锁争用?
- 60. 哪种技术有助于减少锁争用?
- 61. 为了减少下面的代码的锁争用可应用于哪种技术?
- 62. 通过例子说明分拆锁.
- 63. SDK 类 ReadWriteLock使用什么样的技术减少锁争用的?
- 64. 我们所理解的锁分离?
- 65. 我们所理解的CAS操作?
- 66. 哪个Java类使用CAS操作?
- 67. 提供一个例子解释为什么单线程应用程序的性能改进可以使多线程应用程序的性能退化.
- 68. Is object po对象池是否总是多线程应用程序的性能改进?
- 69. 两个接口Executor和ExecutorService之间的关系是什么?
- 70. 当队列已满,你submit()一个新任务给ExecutorService实例会发生什么?
- 71. ScheduledExecutorService是什么?
- 72. 你知道一个简单的方法来构建一个有5个线程执行一些任务返回一个值的线程池?
- 73. 两个接口Runnable 和Callable的区别是什么?
- 74. 这是类java.util.concurrent.Future用例?
- 75. HashMap 和Hashtable 特别在是关于线程安全之间的区别是什么?
- 76. 有没有简单的方法来创建任意的Collection, List 或 Map同步实例吗?
- 77. semaphore是什么?
- 78. CountDownLatch是什么?
- 79. CountDownLatch 和 CyclicBarrier之间的区别是什么?
- 80. 什么样的任务可以通过Fork/Join框架解决?
- 81. 是否可能利用Fork/Join框架找到最小的号码?
- 82. RecursiveTask 和 RecursiveAction之间的区别是什么?
- 83. 是否可能在Java 8线程池执行流操作?
- 84. 我们怎样才能访问由平行流操作使用的线程池?
1. 什么是我们所理解的并发?
并发是一个程序同时执行多个计算的能力。这可以通过把计算分布在一个机器可用的CPU内核上或者甚至在同一网络上的不同的机器来实现。
2. 进程和线程的区别是什么?
一个进程是由操作系统提供的执行环境,有自己的一套资源(如内存,打开的文件等)。相反,线程存在进程里和进程里的其他线程一起使用进程的资源(内存,打开的文件等)。不同线程之间的资源共享能力使得进程更适合性能要求更高的任务。
3. 在Java中,什么是进程和线程?
在Java中,进程对应于虚拟机(JVM),线程存活在进程里,运行时线程可以动态地被虚拟机创建和停止.
4. 什么是调度器?
调度器是在有限资源中,例如处理器或者I/O通道,管理进程和线程调度算法的一种实现。绝大多数调度算法的目标是为可使用的进程和线程提供一些负载平衡,以保证每个进程或线程可以在适当的时间内接触到需要的资源.
5. 一个Java程序至少有多少个线程?
每个Java项目都是在主线程里运行,因此,一个Java至少有一个线程.
6. 如何在Java应用程序中访问当前线程?
当前线程可以通过调用JDK类java.lang.Thread
静态方法 currentThread()
来访问当前线程:
public class MainThread { public static void main(String[] args) { long id = Thread.currentThread().getId(); String name = Thread.currentThread().getName(); ... } }
7. 每一个Java线程有哪些特性?
一个Java线程有如下属性:
- 在JVM里类型为长整数的唯一标识符
- 类型为字符型的名字
- 类型为整数的优先级
java.lang.Thread.State的状态
- 线程所属的线程组
8. 线程组的目的是什么?
每个线程都属于一个线程组. The JDK class java.lang.ThreadGroup
提供了一些方法去处理一整个线程组。我们可以用这些方法,例如去中断全部线程或者调整它们的优先级.
9. 线程有什么状态,每一个状态的意思是什么?
NEW:
线程还没有开始的状态.RUNNABLE:
线程正运行在虚拟机的状态.BLOCKED:
线程被堵塞正等待监控锁的状态.WAITING:
线程正在等待另外一个线程执行某个特定动作.TIMED_WAITING:
一个线程在指定的等待时间里等待另一个线程执行某个动作.TERMINATED:
线程已经退出了的状态.
10. 我们如何设置线程的优先级?
一个线程的优先级使用方法setPriority(int)设置。设置优先级为最大值,我们使用常数Thread.MAX_PRIORITY和设置它的最小值我们使用常数Thread.MIN_PRIORITY因为这些值可以在不同的JVM的实现之间有差异。
11. 如何在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(); } }
12. 我们如何停止在Java的线程?
一个线程可以被其他线程设置指向当前线程的volatile引用为空,表示当前线程停止执行:
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) { } } } }
13. 为什么一个线程不会被调用的方法stop()停止?
一个线程不应该用java.lang.thread过时的方法stop()来停止,当调用此方法使该线程解锁它所有的锁了。如果被释放的锁所保护的某对象处于不一致的状态,这种状态会对其他线程可见。其他线程的工作在这不一致状态的对象上可能发生任意不可预测的行为
14. 是否有可能启动一个线程两次?
不,通过调用其方法start()启动一个线程后,第二调用start()将抛出一个illegalthreadstateexception
.
15. 下列代码的输出结果是什么?
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().
16. 什么是守护线程?
守护线程是当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(); } }
即使守护线程仍然在循环里运行,上面的示例应用程序会终止.
17. 有可能当一个普通用户线程已经开始了,将它转为守护线程吗?
当一个用户线程一旦开始了,它不能转变为守护线程. 在一个运行线程上调用方法 thread.setDaemon(true)会导致
IllegalThreadStateException
.
18. 我们怎么理解忙等待
忙等待意味着等待一个事件进行一些积极的计算,虽然让线程/进程占用处理器也可以通过调度器从它删除。忙等待的一个例子是在循环里消耗等待时间,不断确定当前时间直到达到一个特定的时间点:
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(); } } });
19. 怎么避免忙等待?
避免忙等待的一种方法是使当前线程休眠一定时间。这可以通过传入参数毫秒数到方法java.lang.Thread.sleep(long)
来实现.
20. 我们能否把Thread.sleep()
用于实时处理?
传递给线程 Thread.sleep(long)
调用的毫秒数只是一个提示调度器当前线程有多长时间不需要被执行。实际运行中,调度器有可能几毫秒之前或之后让线程再执行。因此,Thread.sleep()
不能用于实时处理.
21. 一个线程在被放在使用线程休眠Thread.sleep()前怎么被唤醒
?
java.lang.Thread 的方法 interrupt()
可以中断睡眠线程. 中断线程通过调用 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(); } }
22. 怎么查询一个线程是否已经中断?
如果线程不是在一个方法里像 Thread.sleep()
抛出一个 InterruptedException异常.线程可以通过调用静态方法
Thread.interrupted()
或者从java.lang.Thread 继承的isInterrupted()查询是否已中断
.
23. 应如何处理一个InterruptedException
异常?
方法例如 sleep()
and join()会抛出
InterruptedException
异常告诉调用者另一个线程已经中断该线程. 在大多数情况下,这样做是为了让当前线程停止当前计算和意外地结束。因此忽略这种异常,仅仅捕获例外和记录在控制台或日志文件通常不是处理这种异常的适当方法. 不过这异常的问题是Runnable接口的方法 run()
不允许 run()抛出任何异常
. 所以重新抛出并没有帮助. 所以这意味着 run()
的实现本身必须处理受检异常,这往往导致其被捕获和被忽视.
24. 在启动子线程后,我们如何在父线程等待子线程终止?
等待一个线程的终止是通过对线程的实例变量调用方法 join()
:
Thread thread = new Thread(new Runnable() { @Override public void run() { } }); thread.start(); thread.join();
25. 下列程序的输出是什么?
public class MyThreads { private static class MyDaemonThread extends Thread { public MyDaemonThread() { setDaemon(true); } @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { } } } public static void main(String[] args) throws InterruptedException { Thread thread = new MyDaemonThread(); thread.start(); thread.join(); System.out.println(thread.isAlive()); } }
上述代码的输出是 “false”. 虽然 MyDaemonThread 实例是守护线程, 调用的 join()
导致主线程等待守护线程执行完毕. 因此,在线程实例调用 isAlive()
会导致守护线程不再运行.
26.当未捕获的异常逃逸 run()
方法?
一个不受检查的异常有可能从run()
方法逃逸. 在这种情况下, 该线程被Java虚拟机停止. UncaughtExceptionHandler
作为例外处理程序的接口,通过注册其实例, 来捕获此异常. 这要么是在线程本身没有特定处理,通过调用静态方法Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler)告诉JVM使用所提供的处理程序
, 或者通过调用线程实例本身setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler)
.
27. 什么是关闭钩子?
关闭钩子是JVM关闭线程时执行的线程. 通过调用Runtime 实例上面 addShutdownHook(Runnable)
方法可以注册它:
Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { } });
28. 关键字synchronized什么目的下使用?
当你必须实现资源的专用访问,像一些静态值或某些文件的引用,工作于专用资源的代码就可以使用synchronized块:
synchronized (SynchronizedCounter.class) { counter++; }
29. 同步方法获取有什么内在锁?
一个synchronized方法获得该方法对象的内在锁,并在返回的时候释放它的方法。即使方法抛出一个异常,内在锁被释放。因此,一个同步方法是等于以下代码:
public void method() { synchronized(this) { ... } }
30. 构造方法可以同步?
不,构造方法不能同步。这导致了语法错误,事实是只有正在构造的线程应该能够访问所构成的对象.
31. 原始值可以用于内部锁?
不,原始值不能用于内部锁.
32. 内部锁重入么?
是的,内在锁可以被同一个线程中一次又一次访问,需要获得锁的代码必须注意它可以不小心试图获取一个它已经获得了的锁.
33. 我们怎么理解原子操作?
一个原子操作就是要么完全执行或根本不执行的操作.
34. 语句C + +是否原子性?
不,一个整数型变量的增加包含一个以上的操作。首先我们得到C的现值,然后增加,最后存储新的价值。当前线程执行这个策略,可能会在任何这些三步骤之间中断,因此这不是原子操作.
35.什么操作是原子操作?
Java语言提供一些原子性的基本操作,因此可以被用来确保并发线程总是看到相同的值的:
- 参考变量和原始变量(除了long和double)的读写操作
- 所有声明为volatile变量的读写操作
36. 下面代码是否为线程安全?
public class DoubleCheckedSingleton { private DoubleCheckedSingleton instance = null; public DoubleCheckedSingleton getInstance() { if(instance == null) { synchronized (DoubleCheckedSingleton.class) { if(instance == null) { instance = new DoubleCheckedSingleton(); } } } return instance; } }
上面的代码是线程不安全的。虽然它在同步代码块(出于性能原因)再次检查实例的值,JIT编译器可以重新排列字节码,在构造函数完成执行之前改变实例的引用的这种方式。这意味着,该方法 getInstance()
返回一个对象,可能没有被完全初始化。要使代码是线程安全的,Java 5提供了实例变量的关键字volatile。一旦对象的构造函数完全完成执行,被标记为volatile变量才对其他线程可见的.
37. 我们所理解的死锁?
死锁的情况是在两个(或更多)的线程都在等待另一个线程释放资源,而线程本身已锁定其他线程等待的资源:线程1:锁资源A,等待资源B,线程2:锁资源B,等待资源A.
38. 一个死锁的情况有什么要求?
一般按下面条件,可以识别死锁:
- 互斥: 一个资源在任何时间点上只能用一个线程访问.
- 资源保持: 线程锁定一个资源,同时试图获取在另一个锁的一些独家资源.
- 非剥夺条件: 如果一个线程持有一段时间锁,没有机制释放资源.
- 环路等待: 运行时的一个环形链,两个(或更多)的线程都在等待另一个线程释放它已锁定资源.
39. 是否可能防止死锁的发生呢?
为了防止死锁的一个(或更多)死锁,死锁的一些条件要避免:
- 互斥: 在某些情况下可以用乐观锁防止相互排斥.
- 资源保持: 当一个线程没有获得所有的排它锁,它可能会释放它所有的排它锁.
- 非剥夺条件: 使用一个超时,排它锁在一个给定的时间后释放锁.
- 循环等待: 当所有线程在同一序列访问对象,就不会发生循环等待.
40. 有可能实现一个死锁检测?
当所有的排他锁被监测和建模为一个有向图,死锁检测系统可以搜索两个线程是否互相等待对方释放它们已锁定的资源。等待线程可以通过某种异常被迫释放其他线程正在等待的锁.
41. 活锁是什么?
一个活锁的情况是在两个或多个线程块被另一个线程所引起的作用互相阻塞。相比死锁的情况,在两个或多个线程在等待一个特定状态,参与活锁状态的线程改变一种方式,阻碍了他们的日常工作进度。一个例子是,两个线程尝试获得第一个锁,当他们无法获得第二个锁就释放第一个锁。它现在可能发生在两个线程同时试图获得第一个线程。由于只有一个线程能够成功,第二个线程可能成功获取第二个锁。现在两个线程持有两种不同的锁,但需要另外的这个锁,于是他们释放自己的锁从头开始。这种情况现在可能再次发生.
42. 我们所理解的线程饥饿?
具有较低优先级的线程得到比高优先级线程更少的执行时间。当优先级低的线程执行一个长期持久的计算,它可能发生这些线程没有得到足够的时间准时完成他们的计算。他们似乎“饿死”了因为高优先级线程窃取他们的计算时间.
43. 同步块会引起线程饥饿?
线程可以进入一个同步块的顺序未定义。因此在理论上,可能发生多线程正在等待进入一个同步块,一些线程必须比其他线程等待更长的时间。因此,他们不能有足够的计算时间按时完成工作.
44. 我们所理解的并发冲突?
并发冲突介绍了一些取决于参与线程的确切行为时间的多线程实现的循环等待结果。在大多数情况下,这是不可取的,并发冲突也意味着线程由于缺少线程同步会导致不同结果的bug。一个简单的并发冲突的例子是由两个并发线程对整数变量的增量。作为包含一个以上的单原子操作,它可能发生两个线程读取和增量相同的值。这种并行策略下,整数变量并不是增加两个,而是只有一个.
45. 我们所理解的公平锁?
一个公平锁在选择下一个线程通过障碍去访问一些独家资源时候会考虑到线程等待时间. 一个公平锁实现的例子由 Java SDK: java.util.concurrent.locks.ReentrantLock提供
. 如果构造函数布尔标志设置为true, ReentrantLock 授予最长等待时间的线程访问.
46. 这两种方法可以让每个继承java.lang.Object的对象用来实现一个简单的生产者/消费者的情况吗?
当一个线程完成当前的任务和新的任务队列是空的,它可以通过获取队列对象的内在锁和调用该方法 wait()来释放处理器
. 该线程被提出了新的任务到队列中生产者线程唤醒,再次获得同样的内在锁在队列对象并调用它的 notify()
.
47. notify()
和 notifyAll()之间的区别是什么
?
两种方法都是用来唤醒一个或多个调用 wait()睡眠自己的线程
. 而 notify()
只唤醒一个等待的线程, notifyAll()
唤醒所有等待的线程.
48. 它是如何确定哪个线程通过调用notify()
?
如果多个线程等待 notify()
它没有被指定哪个线程将被唤醒过来。因此代码不应该依靠任何具体的JVM实现.
49. 以下代码,从队列检索一个整型值的实现是否正确?
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()唤醒了
, 两个线程相继进入第二个同步块后.
50. 是否可能检查一个线程在一些给定的对象持有监控锁?
类 java.lang.Thread
提供静态方法 Thread.holdsLock(Object)
如果当前线程持有作为参数的方法调用该对象的锁返回true.
51. 什么是线程的方法 Thread.yield()
?
对静态方法 Thread.yield()
的调用暗示调度器当前线程愿意释放处理器。调度程序是可以忽略这个暗示,因为它是没有定义哪个线程将得到处理器在调用 Thread.yield()之后
, 它甚至可能发生当前线程成为“下一步”执行线程.
52. 当传递对象实例从一个线程到另一个你要考虑什么?
线程间传递对象时,你将注意到这些物体在同一时间不受两个线程操纵。一个例子是一个实现的键/值对的Map被两个并发线程修改。为了避免并发修改,你可以设计一个对象是不可变的.
53. 为了实现一个不可变类你必须遵守的?
- 所有属性应是final和private.
- 应该有没有setter方法.
- 类本身应声明final为了防止子类违反不变原理.
- 如果属性是不是一个原始类型,但对另一个对象的引用:
- 不应该有一个公开getter方法可以直接调用.
- 不要改变引用的对象(或者至少改变这些引用不会暴露给对象的客户).
54. 类 java.lang.ThreadLocal的目的是什么
?
因为不同线程之间共享内存, ThreadLocal
提供了一种方式分别为每个线程来存储和检索值.ThreadLocal
的实现为每个线程独立存储和检索,当在相同的 ThreadLocal
, 实例线程A存储值A1和线程B存储值B1的值的值,线程A1在从ThreadLocal实例检索值以后,线程B检索值B1.
55. 什么情况可能使用 java.lang.ThreadLocal
?
实例 ThreadLocal
可以用来在应用中传递信息而不需要通过从方法到方法. 例子是在安全/登录信息放在 ThreadLocal
实例中这样可以每个方法中访问. 另一个使用案例将信息放到一般对象,所有的方法都可以直接访问而不用排队访问.
56. 可以通过使用多线程来提高应用程序的性能?举例.
如果我们有一个以上的CPU核心,而且可以并行计算可用的CPU内核,一个应用程序的性能可以通过多线程提高。一个例子是,一个应用程序应该衡量存储在一个本地目录结构的所有图像。而不是一前一后遍历所有图片,一个生产者/消费者实现可以使用单线程扫描目录结构和大量的工作线程执行实际的缩放操作。另一个例子是一个应用程序镜像了一些网页。相比依次加载HTML页面,一个生产者线程可以解析第一个HTML页面和把第一次发现的链接发到一个队列中。工作线程监视队列和加载由解析器解析的网页。当线程等待页面被加载完全,其他线程可以使用CPU来解析已经加载页面和提出了新的要求.
57. 我们所理解的术语的可扩展性?
可伸缩性意味着一个程序可以通过增加资源来提高绩效的能力.
58. 是否可能为应用计算通过使用多个处理器的理论上最大速度?
Amdahl定律提供了计算公式来计算为应用程序提供多个处理器的理论最大速度。理论计算的公式为 S(n) = 1 / (B + (1-B)/n)
其中 n
为处理器的数量和 B
为程序不能被并行执行的部分. 当 n 收敛对无穷大, (1-B)/n
收敛对零. 因此,这种特殊情况下该公式可趋向为 1/B
. 正如我们看到的,理论上的最大加速比反比例于串行执行的部分。这意味着这部分越低,理论加速比可以越来越大.
59. 我们所理解的锁争用?
锁争用发生在两个或多个线程在竞争要求一个锁。调度器必须决定是否让线程它等待睡眠和执行上下文切换让另一个线程的CPU占用,或者让等待线程忙等待更有效。两种方式给了次级线程空闲时间.
60. 哪种技术有助于减少锁争用?
在某些情况下锁争用可以用以下方法之一的减少:
- 锁的范围缩小.
- 减少锁获得的次数(分拆锁).
- 使用硬件支持乐观锁操作代替同步.
- 尽可能避免同步.
- 避免对象池.
61. 为了减少下面的代码的锁争用可应用于哪种技术?
synchronized (map) { UUID randomUUID = UUID.randomUUID(); Integer value = Integer.valueOf(42); String key = randomUUID.toString(); map.put(key, value); }
上面的代码在同步块执行随机UUID计算和转换字符串42成整数型对象,虽然这两行代码是当前本地线程但不影响其他线程。因此,他们可以从同步块移出:
UUID randomUUID = UUID.randomUUID(); Integer value = Integer.valueOf(42); String key = randomUUID.toString(); synchronized (map) { map.put(key, value); }
62. 通过例子说明分拆锁.
分拆锁是一种可能减少锁争用的方式,当一个锁用于同步访问同一应用程序的不同方面。假设我们有一个类实现我们应用程序一些统计数据的计算。第一个版本这类使用关键字同步为每个方法签名来保护内部状态之前,防止因为由多个并发线程崩溃。这也意味着每个方法调用可能导致锁争用因为同样其他线程可能会尝试获得锁同步。但它可能会分拆对象实例的锁变成一些较小的为每种类型的统计数据中每一个方法的锁。因此,线程T1试图增加统计数据D1不用同时等待T2更新数据D2.
63. SDK 类ReadWriteLock使用什么样的技术减少锁争用的?
SDK 类ReadWriteLock
利用当没有其他的线程试图更新数据,并发线程读取数据的时候不用获得一个锁。这是由一对锁实现,一个是只读操作和另一个是写操作。而只读锁可以被多个线程得到,保证一旦写锁定释放,所有的读操作看到更新的值.
64. 我们所理解的锁分离?
相反锁分拆在应用程序的不同方面引入不同的锁,锁分离使用多个锁保护相同数据结构的不同部分。这种方法的一个例子是 包JDK’s java.util.concurrent
的类ConcurrentHashMap
. Map
的实现在内部使用不同的bucket来存储它的值。bucket是由值的键选择。ConcurrentHashMap
现在使用不同的锁守卫确保哈希桶。因此,当一个线程试图访问第一个哈希桶可以为这个桶获取锁,而另一个线程可以同时访问第二个哈希桶。相反,一个同步版本的HashMap
这种技术在不同的线程访问不同的桶时候可以提高性能.
65. 我们所理解的CAS操作?
CAS是compare-and-swap的缩写,意味着处理器提供了一个单独的指令,只有提供的值等于当前值,才更新寄存器的值。CAS操作可以避免同步时线程可以通过提供其当前值和CAS操作新值更新价值。如果另一个线程同时更新值,该线程的值不等于当前值,更新操作失败。然后这个线程读取新的价值并再次尝试.
66. 哪个Java类使用CAS操作?
在包 java.util.concurrent.atomic
的SDK类像AtomicInteger
或 AtomicBoolean
内部使用CAS操作来实现并行策略.
public class CounterAtomic { private AtomicLong counter = new AtomicLong(); public void increment() { counter.incrementAndGet(); } public long get() { return counter.get(); } }
67. 提供一个例子解释为什么单线程应用程序的性能改进可以使多线程应用程序的性能退化.
这种优化的一个突出例子是一个List
实现把元素的数目作为一个独立的变量来存储。这提高了的单线程应用程序的性能因为size()
操作不需要遍历所有元素,就可以直接返回当前的元素数量。在一个多线程应用程序,额外的计数器已被锁定因为多个并发线程可能把元素插入到列表。这种额外的锁在有更多的更新列表的size()
操作调用的时候有较大成大性能.
68. 对象池是否总是多线程应用程序的性能改进?
对象池试图通过汇集他们,避免新对象的构造,可以提高性能的单线程应用程序因为创建对象的成本是互换的从池中请求一个新的对象。在多线程应用程序,对象池必须同步对池的访问,锁争用的额外费用可能会超过它对新对象的构造和垃圾收集的节约成本。因此,对象池可能并不总是提高多线程应用程序的整体性能.
69. 两个接口Executor和ExecutorService之间的关系是什么?
接口Executor 只定义了一个方法:execute(Runnable)。该接口的实现必须在未来一段时间执行给定的Runnable实例。ExecutorService接口是Executor 接口的扩展,提供额外的方法来关闭底层实现,等待所有提交的任务终止,它允许提交Callable实例的调用
.
70. 当队列已满,你submit()一个新任务给ExecutorService实例会发生什么?
由submit()方法签名提示,ExecutorService 实现应该抛出一个RejectedExecutionException
.
71. ScheduledExecutorService是什么?
接口ScheduledExecutorService 扩展了接口ExecutorService 和增加了方法允许在一个给定的时间点提交新任务给底层的实现要执行。有两种方法来安排一次任务和两种方法来创建和执行周期性任务.
72. 你知道一个简单的方法来构建一个有5个线程执行一些任务返回一个值的线程池?
SDK提供了一个工厂类和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(); }
73. 两个接口Runnable和Callable的区别是什么?
接口Runnable 里定义方法run()
没有任何返回值而接口Callable
允许方法run()
返回一个值和抛出一个异常.
74. 这是类java.util.concurrent.Future用例
?
类java.util.concurrent.Future的实例被用来代表异步计算的结果当这个结果立即可用。因此,这类提供了方法检查异步计算是否已完成,取消任务和检索实际效果。后者可以通过提供的两种方法get()。第一个get()方法不需要参数和一种阻塞知道结果是可用的,而第二个get()方法需要一个超时参数,让方法调用返回是否实际结果不能在给定的时间内可以得到.
75. HashMap 和Hashtable 特别在是关于线程安全之间的区别是什么?
Hashtable
的方法都是同步的。这不是HashMap
实现的例子。因此,Hashtable
是线程安全而HashMap
不是线程安全的。因此对于单线程应用程序,使用HashMap
实现更有效.
76. 有没有简单的方法来创建任意Collection
, List
或 Map的同步实例吗
?
共用类的Collections 提供了方法synchronizedCollection(Collection)
, synchronizedList(List)
和synchronizedMap(Map)
,给支持的实例返回一个线程安全的collection/list/map.
77. semaphore是什么?
semaphore是一种数据结构,维护一组许可来允许竞争的线程。因此semaphore可用于控制多个线程同时访问临界区或资源。因此,java.util.concurrent.Semaphore 构造函数把允许线程竞争的数量作为第一个参数。每次调用它的acquire()方法试图获得一个可用的许可证。该方法acquire()
不需要任何参数会阻塞,直到下一个许可证变为可用。然后,当线程已完成对临界资源的工作,它可以通过调用方法release()
对semaphore实例释放许可.
78. CountDownLatch是什么
?
SDK类CountDownLatch
提供同步的援助,可以用来实现在线程必须等待其他线程直到已经达到相同的状态,这样所有线程可以启动的方案。这是通过提供一个同步计数器递减直至达到0值。在达到零的CountDownLatch
实例才让所有线程进行。这可以用来让所有线程用计数器值1在给定的时间点开始或等待一定数量的线程数完成了才开始。在后一种情况下,计数器用给定的线程数初始化,每个线程已完成工作的时候计数器会倒数1次。
79. CountDownLatch
和CyclicBarrier之间的区别是什么
?
这两个SDK类都是维护内部计数器,由不同的线程递减。线程等待内部计数器达到零,然后再继续。但相比CountDownLatch 类,CyclicBarrier
重置内部值恢复到初始值当值达到零。正如其名称所表明,CyclicBarrier
可以用来实现线程必须一次次等待对方的情况.
80. 什么样的任务可以通过Fork/Join framework框架解决?
Fork/Join框架的基本类java.util.concurrent.ForkJoinPool基本上是一个线程池,处理java.util.concurrent.ForkJoinTask实例。类ForkJoinTask
提供了两种方法fork()和join()。而fork()用于启动任务的异步执行方法,join()用于等待计算的结果。因此Fork/Join框架可以用来实现分而治之的算法,在一个更复杂的问题划分为若干较小的更容易解决的问题.
81. 是否可能利用Fork/Join框架找到最小的号码?
在数组里发现最小数字的问题可以通过使用一个分而治之算法解决。最小的非常容易可以解决的问题是在只有两个数的数组,我们可以直接确定两个数字中较小的数字。使用分而治之的方法将最初的阵列分成长度相等的两部分,两部分都是提供给RecursiveTask 扩展类的两个实例ForkJoinTask。通过分叉的两个任务他们被执行,要么直接解决问题,如果他们的阵列长度为两,他们再次递归地将数组分成两部分新的RecursiveTasks。最后,每一个任务实例返回结果(无论是通过直接计算或等待两个任务. 最终任务返回数组中的最小的数.
82. RecursiveTask
和RecursiveAction之间的区别是什么
?
对比RecursiveTask
, RecursiveAction
的方法 compute()没有返回一个值。因此当操作直接在数据结构无需返回计算值RecursiveAction
时可以使用.
83. 是否可能在Java 8线程池执行流操作?
集合提供的方法parallelStream()创建一个由线程池处理的流。或者你可以在一个给定的数据流调用中间体的方法parallel()转换顺序流到一个平行对应的.
84. 我们怎样才能访问由平行流操作使用的线程池?
用于平行流操作的线程池可以通过ForkJoinPool.commonPool()访问。这样我们就可以查询其并行commonPool.getParallelism()水平。水平不能在运行时改变,但它可以通过提供以下的JVM参数配置:-Djava.util.concurrent.ForkJoinPool.common.parallelism=5.
好了,现在你准备好你的面试!不要忘记查阅我们的免费学院课程的 Java Concurrency Essentials! 如果你喜欢这个,可以订阅 订阅我们的通讯 享受每周更新和免费的白皮书! 同时, 查阅 JCG Academy 关于更先进的培训! 欢迎提供您的意见,我们会把它包括在这篇文章!
Translated by: Klaus Cai |
This post is a translation of Multithreading and Concurrency Interview Questions and Answers – The ULTIMATE List from Martin Mois |