1. 什么是进程、什么是线程?
- 线程是CPU调度的最小单位,进程是线程的集合,一个进程包含多个线程以及共享变量空间
- 举个例子:当我们执行Main启动一个Java程序也就是启动一个进程,而至少会有两个线程启动起来,一个是main方法的主线程,还有就是GC线程,堆空间、元空间都是进程在管理
2. CPU和线程是什么关系
- CPU是计算机的核心计算单元,负责执行指令并进行数据处理
- 线程是CPU执行的基本单位,可以被调度到CPU内核上运行
- 同一时刻CPU只会执行一个线程的任务,并发情况下一个线程会通过上下文切换在多个CPU分段调度
3. 什么是上下文切换?
- 上下文切换是指CPU在并发执行多个线程时,需要保存和恢复他们的运行状态的过程,如保存当前任务上下文信息、更新调度器信息、加载新的任务信息,而在切换过程中,会造成时间、缓存、调度的开销,所以我们需要尽量避免上下文的频繁切换
- 比如在设置线程池的核心线程数时,如果是CPU密集型任务,可以设置与CPU核心数相同的线程数
4. 什么是并发,什么是并行,他们有什么区别?
- 并行:同一时刻,多个任务同时执行
- 并发:一个CPU通过交替调度执行多个任务
- 主要区别:同一时刻,并发只有一个任务在执行,并行有多个任务在执行
- 举例:
- 饭堂打饭有3个窗口,并行就是有3个阿姨给大家打饭,并发就是1个阿姨给大家打饭
- 阿姨是CPU、3个窗口就是3个线程
5. 并发3要素
- 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到
- 原因:CPU缓存引起
- 现象:
- A线程定义了变量i=0,并修改i的值,i=10
- B线程访问变量i
- 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
- 原因:分时复用引起
- 有序性:即程序执行的顺序按照代码的先后顺序执行
- 原因:重排序引起
代码参考资料:Java 并发 - 理论基础
6. 线程安全的实现方式有哪些
- 阻塞同步(synchronized 和 ReentrantLock)
- 非阻塞同步(CAS)
- 无同步的方案 (栈封闭:方法局部变量、线程隔离:ThreadLocal)
7. 为什么要使用多线程
首先CPU、内存、I/O 设备的速度是有极大差异的,而我们日常的作业都是要操作到内存和IO的,如果没有多线程这个时候CPU就是处于空闲状态,使用多线程就是合理的利用CPU资源,从而提升作业效率
8. (重要)线程有哪些状态,他们之间是怎么相互转换的?
- 线程的6个状态
- 正常运转的线程: New --> Runnable --> Terminated
- 在Runnable和Terminated之间:
- 获取锁阻塞:Bloked
- 等待唤醒:Waiting、TimeWaiting
- 阻塞和等待的区别:
- 阻塞是被动的,它是在等待获取一个排它锁。
- 等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入
- 状态详细描述
- New 新建:创建后未启动 Thread thread = new MyThread()
- Runnable 可运行: thread.strat() 可能在运行,也可能等待CPU调度
- Terminated 死亡:线程结束或异常退出
- Blocking 阻塞: 等待获取一个排它锁,如果拿到锁就会结束该状态
- Waiting 无限期等待:等待其他线程显示唤醒,可以在一定时间后自动唤醒
进入方法 | 退出方法 |
Object.wait() | Object.notify() / Object.notifyAll() |
Thread.join() | 被调用的线程执行完毕 |
LockSupport.park() | - |
6. TimeWaiting 限期等待:无序其他线程显式唤醒
进入方法 | 退出方法 |
Thread.sleep() | 时间结束 |
设置了超时时间的Object.wait() | 时间结束 / Object.notify() / Object.notifyAll() |
设置了超时时间的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
LockSupport.parkNanos() | |
LockSupport.parkUntil() |
9. 如何开启多线程
- 继承Threa类,实现run方法,调用start方法
- 实现Runnable接口,实现run方法
- 实现Callable接口,实现call方法(有返回值),返回值通过FutureTask包装
在实际应用上,使用较多的是通过new Thread通过lambda表达式实现任务 然后通过线程池的方式进行调度管理 |
10. 什么是Deamon线程,守护线程
- 守护线程就是在程序运行时后台提供服务的线程,比如GC线程
- 当所有非守护线程结束,程序也会终止,同时所有守护线程也会结束
- 使用Thread的实例方法thread.setDaemon(true); 可以设置线程为守护线程
11. 线程中断 interrupt()
- 实例方法:
- interrupt():
- 方法作用:尝试中断该线程
- 影响:
- 如果线程处于Waiting状态:wait()、join()、sleep()方法阻塞,会抛出InterruptedException异常
- 其他情况下线程正常运行,可以通过检查isInterrupted()检查中断标志进行处理
- isInterrupted():
- 方法作用:检查线程是否被中断,中断返回true,不清除中断状态
- interrupt():
- 静态方法:
- interrupted():检查线程是否被中断,清除中断标识
12. 线程之间的协作
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调 |
join():等待目标线程结束
- 在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束
wait() notify() notifyAll()
- 调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起
- 当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程
Java |
Thread.sleep()
wait()和sleep()都是进入线程等待状态,他们的区别
- wait()是Object的实例方法,而sleep()是Thread的静态方法
- wait()会释放锁,sleep()不会释放锁
- wait()需要在同步代码块中使用
await() signal() signalAll()
- java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调
- 可以在 Condition 上调用 await() 方法使线程等待
- 其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程
- 相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活
Java |
13. 为什么要使用线程池
- 如果每次任务执行都要创建线程销毁线程,会造成cpu资源浪费和性能的开销
- 使用线程池可以实现线程的服用,提高执行效率
- 通过线程池可以更好管理线程的执行,避免无限制的创建线程
14. Executors提供的线程池以及弊端
- FixedThreadPool:
- 使用固定数量的线程。线程数量固定后不会改变,超出的任务将会在队列中等待执行
- CachedThreadPool:
- 根据需要创建新线程的线程池。对于许多短期异步任务,这种线程池通常能够提高程序性能。如果线程闲置时间超过60秒,将被终止并从池中移除
- SingleThreadExecutor:
- 创建一个单线程执行的线程池。确保所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
- ScheduledThreadPool:
- 线程池可以调度命令在给定延迟后运行或定期执行任务
- WorkStealingPool:
- 基于工作窃取算法的线程池,能够有效处理不均匀负载。
- 它使用守护线程池,适用于大多数计算密集型任务。
弊端:没有限制阻塞队列容量,大量任务提交时会导致内存溢出
15. (重要)自定义线程池的参数
ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor( |
线程空闲时间和单位,当线程数超过核心线程后创建了非核心线程,会在存活多久后被销毁
- AbortPolicy(默认策略):
- 这是默认的拒绝策略。当线程池的任务队列已满且无法接受新任务时,会抛出一个 RejectedExecutionException 异常。
- CallerRunsPolicy:
- 当任务被拒绝时,会在调用者的线程中执行该任务。这种策略不会丢弃任务,但可能会导致调用线程阻塞。
- DiscardPolicy:
- 当任务被拒绝时,会直接丢弃该任务,不做任何处理。
- DiscardOldestPolicy:
- 当任务被拒绝时,会丢弃队列中最老的一个任务,然后尝试重新提交被拒绝的任务。
16. 当一个任务被提交到线程池运行时,是如何运作的
17. 线程池中线程异常的处理方案
- 手动try-catc,将异常任务记录下来,或者输出日志监控
- submit执行,Future.get() 接收异常
- 重写ThreadPoolExecutor的afterExecute方法,处理传递的异常引用
- 为工作线程设置UncaughtExceptionHandler处理异常
18. 线程池状态,怎么关闭
如果没有正确关闭线程池,可能导致任务未执行完成、资源泄露等情况
- ExecutorService 提供的方法
- shutdown() 停止接受新任务
- shutdownNow() :等待线程池中所有任务完成,或超时等待
- awaitTermination():强制关闭线程池
Java |
- ThreadLocal简单来说就是线程的全局变量,每个Thread都有自己的ThreadLocal,并且线程之间是隔离的
- ThreadLocal适用于每个线程需要自己独立的全局变量,存储需要在方法间透传的常用信息
20. ThreadLocal源码解读
- 每个线程都有自己的ThreadLocalMap,其中key是ThreadLocal实例,value是我们定义的值
Java |
- set方法
- 获取当前线程
- 获取ThradLocalMap对象
- 将值存入Entry中,key为ThreadLocal对象,value为存入的值
Java |
- get方法
- 获取当前线程
- 获取ThradLocalMap对象
- 获取ThradLocalMap中的Entry
Java |
21. ThreadLocal应用场景
- 用户信息存在ThreadLocal,随处可用
- 对于多数据源的项目,可以通过ThreadLocal实现数据源的动态切换
- 应用日志打印可以获取到当前线程的TraceId,方便日志追溯,traceId一般由前台传入,用于追溯用户体验监控,也可用于APM应用性能监控
- simpleDateFormatter线程不安全,如果要复用可以放在ThreadLocal
22. ThreadLocal设计与分析
Java的四种引用的级别: 强引用 > 软引用 > 弱引用 > 虚引用 经验:平常开发中一些大的任务,大对象在用完之后可以通过赋值null弱化引用,帮助GC进行垃圾回收 |
- 强引用(StrongReference)
- 强引用是最常见的引用,比如我们创建一个对象就是强引用
- 强引用对象不会被GC回收,当内存空间不足虚拟机会抛出OOM
- 软引用
- 如果对象只有软引用,空间足够时是不会回收它的,但如果空间不足就会回收
- 只要对象没有被回收就可以被程序使用,软引用适用于实现内存敏感的高速缓存
- 弱引用
- 虚引用
4种引用的区别:
级别 | 回收时机 | 用途 | 生存时间 |
强引用 | 从不回收 | 对象的一般状态 | JVM停止 |
软引用 | 内存不足 | 联合ReferenceQueue构造有效期短、占内存大、生命周期长的对象的二级高速缓冲器 | 内存不足 |
弱引用 | 垃圾回收时 | 联合ReferenceQueue构造有效期短、占内存大、生命周期长的对象的一级高速缓冲器(系统发生gc则清空) | gc后终止 |
虚引用 | 垃圾回收时 | 联合ReferenceQueue来跟踪对象倍垃圾回收器回收的活动 | gc后终止 |