1. Thread 和 Runnable 的区别是什么?

ThreadRunnable
继承类,单继承接口,多继承
实现实现了Runnable接口顶级接口
是真正处理的线程相当于一个任务

Runnable 表示线程要执行的任务,Thread 是真正意义上的线程实现。线程池里,提交一个任务传递的类型是Runnable类型。

Thread 实现了 Runnable 接口并做了拓展。

2. 什么是守护线程,它有什么特点?

守护线程是一种专门为用户线程提供服务的线程,它的生命周期依赖于用户线程。只有在JVM中仍然存在用户线程在运行,守护线程才有存在的意义,否则守护线程会结束。

守护线程不会组织JVM退出,但是用户线程会。

守护线程创建方式和用户线程一样,只是在创建后调用 setDaemon(true) 方法。

适用于后台通用服务场景,比如JVM的垃圾回收线程。不能用在线程池或I/O任务。

3. BLOCKED 和 WAITING 两种线程状态有什么区别?

都属于线程的阻塞状态。

BLOCKED状态是指线程在等待监视器锁时的阻塞状态。多个线程竞争 synchronized 锁时,没有竞争到锁资源,就会进入BLOCKED状态。

WAITING状态是指线程在等待其他线程的通知时的阻塞状态。比如调用了 Object.wait()、Object.join()、LockSupport.park() 方法,线程就会进入WAITING状态。

需要通过 Object.notify()、Object.notifyAll()、LockSupport.unpark() 方法来唤醒线程。

所以,BLOCKED 是锁竞争失败后被动触发,WAITING 是主动触发。 BLOCKED 的唤醒是自动触发,WAITING 的唤醒是主动触发。

4. 为什么启动线程不能直接调用 run() 方法?调两次 start() 会有什么后果?

start() 方法是Java线程约定的内置方法,能够确保代码在新的上下文中运行。 start() 方法包含了创建新线程的代码逻辑,run() 中没有。

如果两次调用 start() 方法,会抛出IllegalThreadStateException异常。

5. 谈谈你对Java线程5种状态流转的理解?

先讲下JDK中3中自定义线程的方法

1. 继承 Thread 类,重写 run() 方法

public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码逻辑
    }
}

2. 实现 Runnable 接口,重写 run() 方法。

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码逻辑
    }
}

3. 实现 Callable 接口,重写 call() 方法。

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 线程执行的代码逻辑
        return "Hello, World!";
    }
}

优点是支持回调并得到返回值。

线程的状态

状态描述
NEW线程创建后,尚未启动。Thread thread = new Thread()
RUNNABLE就绪状态,thread.start() 线程正在执行,可能正在执行,也可能正在等待CPU调度。
RUNNING运行状态,线程获取CPU资源后,正在运行,只能由就绪状太进入到运行状态
BLOCKED阻塞状态,线程因为某种原因放弃CPU使用权,暂停运行。直到后续进入就绪状态,才有机会继续运行。阻塞有3种: 1. wait() 阻塞; 2. synchronized 阻塞;3. sleep(), join(), I/O
DEAD死亡状态,线程执行完毕或者异常终止。

6. 谈谈你对线程池的理解

一种池化技术,资源复用。设计目的:

  • 减少线程频繁创建和销毁带来的性能开销,因为线程创建涉及到CPU上下文切换,内存分配工作;
  • 线程池本身有参数控制线程创建的数量,可以避免无休止地创建线程,导致系统资源耗尽。

为了实现线程复用,线程池用了阻塞队列。线程池里的工作线程处于一直运行的状态,它会从阻塞队列里获取等待执行的任务,一旦队列空了,这个工作线程就会被阻塞,直到下次有新的任务进来。工作线程根据任务的情况实现阻塞和唤醒,从而达到线程复用。

线程池的资源限制,通过核心线程数和最大线程数控制。

7. Java 有哪些实现线程池的方式?

JDK默认提供了5种不同的线程池实现,分别是:CachedThreadPool, FixedThreadPool, SingleThreadExecutor, ScheduledThreadPool, WorkStealingPool。这些线程池都是通过 Executors 来创建,线程池内部的最终实现类是 ThreadPoolExecutor。

CachedThreadPool

处理大量短期突发流量,特点是

  • 对最大线程数没有限制,Inter.MAX_VALUE, 核心线程数是0
  • 线程存活时间是60s
  • 阻塞队列是 SynchronousQueue, 不存储任何元素,线程间直接交换

FixedThreadPool

核心、最大线程数都是固定值,任务较多就会加入到阻塞队列等待

SingleThreadExecutor

只有一个工作线程,保证任务按照FIFO的方式顺序执行

ScheduledThreadPool

实现定时调度

WorkStealingPool

内部构造一个ForkJoinPool, 利用工作窃取算法并行处理请求。

8. 线程池如何回收线程?

线程池中的线程分为核心线程和非核心线程。

核心线程的初始化方式

  • 向线程池里添加任务的时候,被动初始化
  • 主动调用 prestartAllCoreThreads() 初始化

当线程池里的队列满了,为了增加线程池的任务处理能力,线程池会增加非核心线程。当任务处理完成,工作线程处于空闲状态超过了keepAliveTime,回收非核心线程。

当阻塞队列 poll(timeout, unit) 返回null,终止当前线程,完成回收。

如果希望核心线程页被回收,设置 allowCoreThredTimeOut 为true。

9. 线程池如何实现线程复用的?

线程池采用了生产者-消费者模型来实现线程复用。通过一个中间容器来解耦生产者和消费者的任务处理过程。生产者不断生产任务并保存到容器中,消费者不断从容器中消费任务。

在线程池中,因为需要保证工作线程的复用,这些线程应该在有任务的时候执行,没有任务时等待并释放CPU资源。使用阻塞队列来实现这样的需求。如果阻塞队列中没有任务,这些工作线程会阻塞等待。直到有新的任务进入队列,这些工作线程被唤醒。

10. 线程池如何知道一个线程的任务已经执行完成?

在线程池内部,线程池调度工作线程来执行任务的run()方法,运行结束后,会调用 afterExecute(task, thrown)

在线程池外部如果想获取线程池内部任务的执行状态,有几种方式实现

  • 通过线程池的 isTermianated() 判断是否调用了 shutdown() 方法
  • 线程池的submit() 方法提供了 Future 返回值,通过 future.get() 方法等待任务执行结束
  • 重写 afterExecute()

11. 当任务数超过线程池的核心线程数时,如何让任务不进入队列?

当提交一个任务到线程池,它会

  1. 预热核心线程
  2. 把任务添加到阻塞队列
  3. 如果添加到阻塞队列失败,则创建非核心线程
  4. 如果非核心线程达到了阈值,触发拒绝策略

如果不希望任务进入队列,可以把阻塞队列改成 SynchronousQueue.

12. 什么是伪共享,如何避免伪共享?

CPU和内存有速度差异,CPU有三级缓存,在向内存发起I/O操作时,会读取64字节的数据作为一个缓存行。 一个long类型的变量占8字节(64位),它附件的位置也会被引用。不同的core缓存的数据可能不同了。

伪共享会导致缓存锁的竞争,从而影响并发效率。

解决办法:

  • 使用对齐填充
  • 使用 @Contented 注解,将被声明的类或字段加载到独立的缓存上

13. wait 和 notify 为什么要写在 synchronized 代码块中?

synochronized 实现互斥条件

14. wait 和 sleep 是否会触发锁的释放及CPU资源的释放?

Object.wait() 会释放锁和CPU资源。

Thread.sleep() 不释放锁,但会释放CPU。

15. volatile 关键字的作用是什么?它的实现原理是什么?

作用:

  • 保证多线程下共享变量的可见性
  • 通过设置内存屏障指令,禁止多线程环境下CPU指令重排

16. 谈谈你对 CompletableFuture 的理解

事件驱动的异步回调类。当使用异步线程去执行一个任务时,希望任务结束后触发另一个动作,CompletableFuture 可以实现此功能。

把异步任务组成一个具有先后关系的处理链

  • thenCombine 将两个任务组合,当任务都执行结束后触发
  • thenCompose 将任务串行
  • thenAccept 第一个任务的结果作为第二个任务的入参
  • thenApply 和 thenAccept 类似,但有返回值
  • thenRun 第一个任务执行结束后触发一个 Runnable 任务

17. 谈谈你对 ThreadLocal 实现原理的理解

在Thread类中有一个成员变量 ThreadLocalMap, 专门存储当前线程共享变量副本,后续线程对共享变量的操作,只在 ThreadLocalMap中变更,而不影响全局共享变量的值。

18. CountDownLatch 和 CyclicBarrier 的区别是什么?

CountDownLatch 是让多个线程持续等待,直到其他多线程执行的一组操作全部完成,这些等待的线程才会继续执行。

应用场景:

  • 让单个线程等待多个线程的场景,如并发计算,汇总结果。
  • 让多个线程等待的场景,比如模拟秒杀,实现最大并行。

CyclicBarrier 是一个同步辅助类,允许一组线程互相等待, 直到所有线程都到达一个公共的屏障点。这个屏障可以重复使用。

适用于多线程计算再合并结果的场景。

19. 谈谈你对 Happens-Before 的理解

Happens-Before描述结果的可见性,只要不对结果产生影响,就允许指令重排。

在JMM中的Happens-Before规则

  • 程序顺序规则
  • 传递性规则
  • volatile规则
  • 监视器锁规则
  • 线程启动规则
  • join规则