多线程面试题
1、实现一个阻塞队列(生产者消费者模式)?
// 以下的代码保证了同一时刻只有一个消费者或者生产者在执行业务
public class ProviderConsumer<T> {
private int length;
private Queue<T> queue;
private ReentrantLock lock = new ReentrantLock();
private Condition provideCondition = lock.newCondition();
private Condition consumeCondition = lock.newCondition();
public ProviderConsumer(int length){
this.length = length;
this.queue = new LinkedList<T>();
}
public void provide(T product){
// 这里上把锁,consume方法的线程也得阻塞
lock.lock();
try {
while (queue.size() >= length) {
// 执行await方法后,线程会释放锁并且挂起
// 当被唤醒时,线程先尝试获取锁,获取不到就会循环尝试获取
provideCondition.await();
}
queue.add(product);
// 唤醒消费者
consumeCondition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public T consume() {
// 这里上把锁,provide方法的线程也得阻塞
lock.lock();
try {
while (queue.isEmpty()) {
consumeCondition.await();
}
T product = queue.remove();
// 唤醒生产者
provideCondition.signal();
return product;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return null;
}
}
2、实现多个线程顺序打印abc?
public class PrintABC {
ReentrantLock lock = new ReentrantLock();
Condition conditionA = lock.newCondition();
Condition conditionB = lock.newCondition();
Condition conditionC = lock.newCondition();
volatile int value = 0;
// 打印多少遍abc
private int count;
public PrintABC (int count) {
this.count = count;
}
public void printABC() {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
new Thread(new ThreadC()).start();
}
class ThreadA implements Runnable{
@Override
public void run() {
lock.lock();
try {
for (int i = 0; i < count; i++) {
// 只有value是3的倍数时,才会打印a
while (value % 3 != 0) {
conditionA.await();
}
System.out.print("a");
conditionB.signal();
value ++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
class ThreadB implements Runnable{
@Override
public void run() {
lock.lock();
try {
for (int i = 0; i < count; i++) {
while (value % 3 != 1) {
conditionB.await();
}
System.out.print("b");
conditionC.signal();
value ++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
class ThreadC implements Runnable{
@Override
public void run() {
lock.lock();
try {
for (int i = 0; i < count; i++) {
while ( value % 3 != 2) {
conditionC.await();
}
System.out.println("c");
conditionA.signal();
value ++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
PrintABC printABC = new PrintABC(15);
printABC.printABC();
}
}
3、线程的生命周期在java中的定义?
(1)New(初始状态):线程刚创建出来,还没有调用start方法
(2)Runnable(可运行状态):调用start方法后,线程进入就绪状态,等待cpu调度
(3)Blocked(阻塞状态):线程试图获取一个对象锁而被阻塞
(4)Waiting(等待状态):线程进入等待状态,需要被唤醒才能继续执行
(5)Timed Waiting(含等待时间的等待状态):线程进入等待状态,但指定了时间,超时后会被唤醒,也可提前唤醒
(6)Terminated(终止状态):线程执行完成或因异常退出
4、线程的创建方式?
(1)实现Runnable接口,重写run方法,无返回值,无法抛出异常
(2)实现Callable接口,重写call方法,有返回值,可以抛出异常
(3)继承Thread类,重写run方法,调用start方法开启线程
(4)通过线程池创建线程
5、线程间通信?
(1)通过共享变量进行通信
(2)通过同步机制
Synchronized:上锁保证线程安全
ReentrantLock:配合Condition提供await和signal方法,等待和唤醒线程
BlockingQueue:通过阻塞队列实现生产者和消费者模式
CountDownLatch:可以允许一个或者多个线程等待,直到其他线程执行完操作
CyclicBarrier:可以让一组线程互相等待,到达某个屏障点
Semaphore:信号量可以控制对特定资源的访问线程数
Volatile:确保变量可见性,防止指令重排 Object类中有wait、notify和notifyAll方法实现线程的等待唤醒
6、如何防止死锁?
破坏死锁发生的条件
死锁发生的条件:
(1)互斥条件:同一时间只能有一个线程拥有锁资源
(2)不可剥夺条件:一个线程已经拥有锁,在释放前不会被其他线程抢占
(3)请求和保持条件:存在其他线程在不停申请锁资源,并且不会释放自己拥有的锁资源
(4)循环等待条件:多个线程在互相申请对方拥有的锁资源,等待对方释放
破坏这四个条件:
(1)互斥条件无法被破坏
(2)破坏不可剥夺条件:一个线程在申请资源时,将自己需要的所有资源全部申请
(3)破坏请求和保持条件:在出现资源申请不到的情况,放弃占有的资源
(4)破坏循环等待条件:所有线程按照顺序申请资源,申请到A后,才能申请B
7、wait和sleep方法的区别?
(1)wait方法是Object类提供的,sleep方法是Thread类提供的
(2)wait方法会释放锁,需要其他线程调用notify或者notifyAll方法唤醒
(3)sleep方法不会释放锁,等待一段时间后会自动被唤醒
(4)执行wait方法前需要先获取锁,sleep方法不用
8、notify和notifyAll的区别?
notify是唤醒某个线程,notifyAll是将等待池中所有线程移动到锁池中去竞争锁。
锁池:锁池中存放的是没有获取到锁资源,但是一直在尝试获取的线程
等待池:等待池中是调用wait或者await方法释放锁资源并且阻塞的线程
9、Thead.sleep(0)的作用?
将线程睡眠0秒,其作用是释放CPU资源,线程进入就绪状态,等待CPU再次调度
10、Synchronized和Lock的区别?
(1)Synchronized是关键字,Lock是接口
(2)Synchronized只支持非公平锁,Lock支持公平和非公平锁
(3)Synchronized获取锁的方式单一,Lock获取锁的方式多样,比较灵活
(4)Synchronized不用考虑释放锁的代码,Lock需要程序员手动释放锁
11、CountDownLatch的用法?
(1)初始化CountDownLatch时,先根据业务需要给count参数赋值。让主线程await,业务线程进行业务处理,处理完成后调用CountDownLatch.countDown方法,等count参数变成0时,主线程被唤醒
(2)让业务线程await,主线程处理完业务后调用countDown方法,业务线程被唤醒后去处理后的数据
12、父子线程如何传递数据?
使用InheritableThreadLocal类。因为InheritableThreadLocal类重写了ThreadLocal类的createMap方法,在创建线程时将父线程的inheritableThreadLocals对应的ThreadLocalMap复制一份,赋值给子线程的inheritableThreadLocals,完成数据的传递。
13、ReentrantLock的实现原理?
(1)ReentrantLock中有一个静态内部类Sync,继承了AbstractQueuedSynchronizer类。通过重写AQS中的方法,实现了可重入锁,支持公平和非公平两种方式
(2)内部有一个volatile修饰整型变量state作为锁标志,通过CAS方式对state进行增减操作,作为加锁和释放
(3)还有一个同步队列,是双向链表,抢锁失败的线程会被封装成Node节点放到队列尾部
14、介绍下AQS?
AQS是一个抽象类,通过对外提供一些方法来让JUC包中的类实现线程同步的功能。AQS中有一个volatile修饰的整型变量state,通过它来表示锁的状态。JUC中一些类通过CAS方式对state进行增减操作,作为加锁和释放锁。AQS中提供了对应的方法,例如:tryAcquire,tryRelease等。除此之外,AQS中有一个Node内部类,其维护了一个双向链表。当有线程竞争锁失败时,会将其放到链表的尾部,等待前面的节点唤醒它。
15、Java线程池执行任务时发生异常,如何知道是哪个线程以及什么异常?
(1)使用ThreadPoolExecutor线程池,可以自定义线程工厂,在newThread方法中,调用线程对象的setUncaughtExceptionHandler方法可以打印异常。
(2)编写线程要执行的任务逻辑,要有try...catch...异常捕获
(3)使用submit执行任务,异常封装在Future对象中,调用get方法会抛出异常
注意:线程执行任务发生异常,如果没有正常捕获,该线程会被线程池销毁。
16、Timer类的用法?
(1)Timer类可以延迟执行任务,也可以周期性的执行任务
(2)使用schedule方法可以延迟执行任务
(3)使用scheduleAtFixedRate方法可以周期性执行任务
17、守护线程与普通线程的区别?
(1)当只剩下守护线程在执行任务时,JVM会立即退出,不管守护线程的任务有没有执行完成;但是JVM会等所有的普通线程执行完任务后再退出
(2)守护线程一般在后台执行任务,普通线程通常执行主要任务
(3)正常创建的线程都是普通线程,可以通过setDaemon方法,将普通线程设置为守护线程
18、synchronized锁升级?
(1)当有线程执行synchronized代码块时,获取到的是偏向锁
(2)当有其他线程来获取锁时,如果两个线程没有发生锁竞争,那么偏向锁会升级为轻量锁。后续如果一直没有发生锁竞争,会一直保持是轻量锁
(3)当发生锁竞争,轻量锁会升级为重量锁。
(4)线程在获取重量锁时,如果获取失败,并不会立即挂起。而是先自旋尝试获取锁,如果自旋一定次数依然失败,才会挂起。
(5)当重量锁被释放后,锁会降级到偏向锁。
19、ThreadLocalMap中的key为什么设置为弱引用?
出于防止内存泄漏的考虑,将ThreadLocalMap中的key设置为弱引用,value设置为强引用。如果ThreadLocalMap的key为强引用,当ThreadLocal设置为null,想要被回收时,由于ThreadLocalMap生命周期与线程相同,导致ThreadLocalMap中key不会被回收,即ThreadLocal不会被回收,以至于内存泄漏。key设置成弱引用,那么就会被回收。这样会引起另一个问题,key被回收了,value是强引用,不会被回收,依然会有内存泄漏。所以,在想要回收ThreadLocal前,要先调用remove方法,清空ThreadLocalMap,这样就不会有内存泄漏的问题啦。