【面试复习】Java2023最新多线程面试题

线程池中提交一个任务的流程是怎样的?

1.使用execute()方法提交一个Runable对象
2.先判断当前线程数(workerCount())是否大于等于corePoolSize
	2.1 如果小于,则创建一个新的线程(addWorker()),并将该Task作为该线程的第一任务。
	2.2 如果大于等于,则尝试加入到阻塞队列中
3.判断阻塞队列是否已满(workQueue.offer()返回TRUE则加入到队列成功,如果返回FALSE,则说明阻塞队列已满,加入失败。)
	3.1 队列已满,判断当前线程数(workerCount())是否>=最大线程数
		3.1.1 当前线程数<最大线程数,则创建一个新的线程(addWorker())执行任务。
		3.1.2 当前线程数>=最大线程数,则执行拒绝策略(默认抛出异常)
	3.2 队列未满,将任务加入队列中等待执行任务。

线程池有几种状态?分别是如何变化的?

线程池有5种状态

分别是:

状态 描述
RUNNING 接收新的任务,并且处理队列中的任务
SHUTDOWN 调用shutdown()方法, 不会接收新的任务,但处理队列中的任务,任务完成后会中断所有的线程
STOP 调用shutdownNow()方法,不会接收新的任务,也不会处理队列中的任务,并且会中断所有的线程
TIDYING 所有的线程中断后,线程池就会处于TIDYING状态,一旦处于该状态,线程池就会调用线程池的terminated()方法
TERMINATED 调用线程池的terminated()方法执行完成之后(terminated()为一个可拓展方法,我们可以根据需要去重写),就会转变为TENMINATED状态
转化情况
转化前 转化后 转换条件
RUNNING SHUTDOWN 手动调用shutdown()方法,或者在线程池对象GC时候调用fianlize()方法,从而调用shutdown()方法
RUNNING STOP 手动调用shutdownNow()方法
SHUTDOWN STOP 手动先调用shutdown()方法,然后紧接着调用shutdownNow()方法
SHUTDOWN TIDYING 线程池所有线程都停止后自动触发
STOP TIDYING 线程池所有线程都停止后自动触发
TIDYING TERMINATED 线程池自动调用terminated()方法后触发

为什么不建议使用Executors来创建线程池

FixedThreadPool

Executors创建FixedThreadPool时,对应的构造方法如下:

public static ExecutorService newFixedThreadPool(int nThreads) {
	return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLIONSECONDS,
								  new LinkedBlockingQueue<Runnable>());
}

可以看到创建的队列是LinkedBlockingQueue,无界阻塞队列,如果使用该线程池执行任务,在任务过多的时候,队列中就会不断的添加任务,任务越多,占用的内存就越多,最终可能导致内存耗尽OOM。

SingleThreadExecutor

Executors创建SingleThreadExecutor时,对应的构造方法如下:

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

这里也是LinkedBlockingQueue,也可能会造成内存耗尽OOM

总结: 使用Executors来创建线程池,除了可能会造成OOM外,也不能自定义线程名称,不利于问题排查,所以建议直接使用ThreadPoolExecutor。

应该突然出现OOM异常,应该怎么排查?

对于还可以正常运行的系统:
  1. 可以使用jmap来查看jvm中各个区域的使用情况。
  2. 可以通过jstack来查看各个线程的运行情况,比如那些线程遇到了阻塞,有没有出现死锁。
  3. 可以通过jstart命令来查看垃圾回收的情况,特别是fullGC,如果发现fullgc频繁的话,就特别需要调优了。
  4. 通过各个命令的结果,或者通过jvisualvm等工具来进行分析。
  5. 如果是频繁的fullgc,首先推测一下原因,如果频繁发生fullgc,但是没有造成内存溢出,那么表示fullgc实际上是回收了很多对象的,那么我们应该让这些对象最好在年轻代就被回收掉,尽量避免进入老年代中。如果这些存活时间短的对象比较大的话,导致年轻代放不下,从而进入到老年代中,可以尝试加大年轻代的内存,如果改完之后,fullgc减少,则证明修改是有效的。
  6. 也可以通过找占用CPU资源最多的一个线程,定位到具体的方法,优化一下这个方法的执行,看能否尽量避免某些对象的创建,以此来节约内存。
对于已经发生了OOM的系统:
  1. 一般的系统就会设置发生OOM的时候会产生当时的Dump文件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
  2. 我们可以里用jvisualvm等工具来分析dump文件。
  3. 根据分析dump文件来找到异常的实例对象,异常的线程(占用CPU高),定位到具体的代码然后进行分析和调试。

synchronized 和 ReentrantLock 有哪些区别

synchronized ReentrantLock
Java中的一个关键字 JDK提供的一个类
自动加锁和释放锁 需要手动加锁和释放锁
JVM层的锁 API层面的锁
非公平锁 公平锁和非公平锁
锁的是对象,锁信息保存在对象头中 int类型的state来标识锁的状态
底层有锁升级过程 没有锁升级过程

ReentrantLock分为公平锁和非公平锁,底层是如何实现的

不管是公平锁还是非公平锁,他们的底层都是使用AQS来排队的,他们的区别在于使用lock()方法进行加锁的时候:

  1. 如果是公平锁,在加锁的时候,会先判断AQS队列中有没有排队的线程,如果有线程排队,那么当前线程也会进行排队
  2. 如果是非公平锁,在加锁的时候,则不会判断队列中是否有排队的线程,而是直接竞争锁
    ps: 不管是公平锁还是非公平锁,在没有竞争的到锁时候,都会重新进行排队。当锁释放时候,也是唤醒队列最前面的线程,所以非公平锁只是体现在线程的加锁的阶段,而不是线程被唤醒的阶段。ReentrantLock是可重入锁,不管是公平锁还是非公平锁

synchronized的锁升级过程是怎么样的

  1. 偏向锁:在锁对象的对象头中记录一下当前获取锁的线程的ID,该线程如果下次又来获取锁的话,则可以直接取到锁,也就是支持锁重入。
  2. 轻量级锁:由偏向锁升级而来,当一个线程获取到锁时候,这个锁是偏向锁,当有另外一个线程来竞争锁的时候,锁会升级成轻量级锁,之所叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程。
  3. 如果自旋多次都没有获取到锁,轻量级锁则会升级到重量级锁,重量级锁底层会调用操作系统指令来阻塞线程。
  4. 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞线程和唤醒线程这两个操作都是操作系统来执行的,比较耗费时间。自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了即表示获取到了锁,这个过程一直在运行中,相对而言没有使用过多的操作系统资源,所以比较轻量。

如何停止一个线程

  • new Thread().stop();
    stop() 方法会粗暴中断当前线程,即使任务没有执行完。
    stop() 会释放synchronized的锁,但是不会释放Lock的锁,Lock的锁需要手动释放。
  • new Thread().interrupt();
    interrupt() 会给线程发送一个中断信号,但是否会立刻停止线程还是需要看当前线程所执行的任务逻辑。任务逻辑中可以通过Thread.interrupted()来判断当前线程是否处于中断状态,并且我们可以在知道当前线程处于中断状态后,根据实际需要增加一些别的中断条件。
    interrupt()在和sleep()配合使用时候,如果没有处理抛出的异常,则会被清掉中断状态。

死锁如何避免

造成死锁的原因
  1. 一个资源同一个时刻只能被一个线程使用。
  2. 一个线程在阻塞等待某个资源时候,不会释放占有的资源。
  3. 一个线程在获得资源,并在使用完之前,不会被强剥离资源使用权。
  4. 多个线程形成收尾相接的循环等待资源的关系。
如何避免

这是造成死锁的必须要达到的四个条件,如果要避免死锁,只需要不满足其中一个条件即可,但前3个条件都是锁需要具备的必要条件,所以只能从第四个条件着手,避免出现循环等待资源关系。

从开发角度来讲:

  1. 要注意加锁的顺序,保证每个线程的加锁顺序都是一致的。
  2. 要注意加锁时限,可以设置一个超时时间。
  3. 要注意死锁检查,这是一种预防机制,确保第一时间发现死锁并进行解决。

ReentrantLock中的tryLock()和lock()方法的区别

  • tryLock() :非阻塞加锁,加到锁返回TRUE,反之返回FALSE。
  • lock() :阻塞加锁,无返回值,加锁时会处于阻塞状态,直到加到锁。

ps:复习记录,不定时更新,有问题还请各位大佬随时指出

你可能感兴趣的:(面试,java,后端)