0.前言
在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分,本文汇总了常见的一些多线程面试题。
一些问题,比如volatile关键词的作用,synchronized和ReentrantLock的区别,wait()和sleep()的区别等等问题,已经在之前写过的文章中提到过了,这里就不赘述了,有兴趣可以查看以下几篇文章:Java并发——线程同步volatile与synchronized详解、Java技术——Java多线程学习、Java并发——synchronized和ReentrantLock的联系与区别。
下面是总结的之前没有提到过的面试重点题。转载请注明出处:Java面试——多线程面试题_SEU_Calvin的博客-CSDN博客_java多线程面试题
1.多线程有什么用
(1)发挥多核CPU的优势
如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。多线程可以充分利用CPU的。
(2)防止阻塞
多条线程同时运行,一条线程的代码执行阻塞,也不会影响其它任务的执行。
2. Runnable接口和Callable接口的区别
Runnable接口中的run()方法的返回值是void,它只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
3. CyclicBarrier和CountDownLatch的区别
两个类都在java.util.concurrent下,都可以用来表示代码运行到某个点上,二者的区别在于:
(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,该线程会继续运行。
(2)CyclicBarrier只能唤起一个任务,CountDownLatch可以唤起多个任务。
(3)CyclicBarrier可重用,CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了
4. 线程安全的级别
代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么代码就是线程安全的。线程安全也是有级别之分的:
(1)不可变
像String、Integer、Long这些,都是final类型的类,要改变除非新创建一个。
(2)绝对线程安全
不管运行时环境如何都不需要额外的同步措施。Java中有绝对线程安全的类,比如CopyOnWriteArrayList、CopyOnWriteArraySet。
(3)相对线程安全
像Vector这种,add、remove方法都是原子操作,不会被打断。
如果有个线程在遍历某个Vector,同时另一个线程对其结构进行修改,会出现ConcurrentModificationException(fail-fast机制)。
(4)线程非安全
这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类。
5. 如何在两个线程之间共享数据
通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。
6.为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
这是JDK强制的,wait()方法和notify()/notifyAll()方法(都是Object的方法)在调用前都必须先获得对象的锁。
7. wait()方法和notify()/notifyAll()方法在放弃对象锁时有什么区别
wait()方法立即释放锁,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃锁。
8. 怎么检测一个线程是否持有对象监视器
Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象obj的锁被当前线程持有的时候才会返回true。
9.ConcurrentHashMap的并发度是什么
ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多可以同时有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势。
10. ReadWriteLock是什么
ReentrantLock的局限:
如果使用ReentrantLock是为了防止线程A在写数据、线程B在读数据造成的数据不一致。那么如果线程C在读数据,线程D也在读数据,读数据是不会改变数据的,那就没有必要加锁,但是ReentrantLock还是加锁了,这很显然降低了程序的性能。因此读写锁ReadWriteLock应运而生。
它是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离:
读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
11. 如果你提交任务时,线程池队列已满,这时会发生什么
如果使用的无界队列(如LinkedBlockingQueue)的话,继续添加任务到阻塞队列中等待执行。
如果你使用的是有界队列(如ArrayBlockingQueue)的话,则会使用拒绝策略。
12. Java中用到的线程调度算法是什么
抢占式:一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
13. 多线程中的忙循环是什么
忙循环就是程序员用空循环让一个线程等待,不像传统方法wait(),sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU。
这么做的目的是为了保留并避免重建CPU缓存(在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存)。
14. 什么是自旋
很多时候synchronized代码块中逻辑简单执行速度快,此时等待的线程都加锁可能是一种不太值得的操作。
因此不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞。
15. 什么是乐观锁和悲观锁
(1)乐观锁:认为竞争不总是会发生,因此它拿数据时不上锁,但是在更新的时候会去判断在此期间有没有人去更新这个数据,适用于多读的场景。
(2)悲观锁:认为竞争总是会发生,因此每次拿数据的时候都会上锁。
16. 实现一个死锁
死锁形成需要四个条件:
(1)一个资源每次只能被一个进程使用。
(2)一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)进程已获得的资源,在没有使用完之前,不能强行剥夺。
(4)若干进程之间形成一种头尾相接的循环等待资源关系。
考虑如下情形:
(1)线程A当前持有互斥所锁lock1,线程B当前持有互斥锁lock2。
(2)线程A试图获取lock2,因为线程B正持有lock2,因此线程A会阻塞等待线程B对lock2释放。
(3)如果此时线程B也在试图获取lock1,同理线程也会阻塞。
(4)两者都在等待对方所持有但是双方都不释放的锁,这时便会一直阻塞形成死锁。
//存放两个资源等待被使用
public class Resource {
public static Object obj1 = new Object();
public static Object obj2 = new Object();
}
//线程1
public class DeadThread1 implements Runnable {
@Override
public void run() {
synchronized (Resource.obj1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized (Resource.obj2) {
System.out.println("DeadThread1 ");
}
}
}
}
//线程2
public class DeadThread2 implements Runnable {
@Override
public void run() {
synchronized (Resource.obj2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized (Resource.obj1) {
System.out.println("DeadThread2 ");
}
}
}
}
//主函数中调用
Thread t1 = new Thread(new DeadThread1());
Thread t2 = new Thread(new DeadThread2());
//启动两个线程
t1.start();
t2.start();
17. 线程类的构造方法、静态块是被哪个线程调用的
被new这个线程类所在的线程所调用的,而run方法里面的代码才是被线程自身所调用的。
18. 锁粗化是什么意思
Java虚拟机中存在着一种叫做锁粗化的优化方法,即同步范围变大。
比方说StringBuffer,它是一个线程安全的类,反复append字符串意味着要进行反复的加解锁,这对性能不利,因为JVM在这条线程上要反复地在内核态和用户态之间切换,因此JVM会将多次append方法调用的代码进行一个锁粗化的操作,将多次的append的操作扩展到append方法的头尾,变成一个大的同步块,从而提升代码执行效率。