摘要:New(初始状态):线程对象创建后,但未调用 start 方法。Runnable(可运行状态):调用 start 方法后,线程进入就绪状态,等待 CPU 调度。Blocked(阻塞状态):线程试图获取一个对象锁而被阻塞。Waiting(等待状态):线程进入等待
在 Java 中,线程的生命周期可以细化为以下几个状态:
New(初始状态):线程对象创建后,但未调用 start 方法。Runnable(可运行状态):调用 start 方法后,线程进入就绪状态,等待 CPU 调度。Blocked(阻塞状态):线程试图获取一个对象锁而被阻塞。Waiting(等待状态):线程进入等待状态,需要被显式唤醒才能继续执行。Timed Waiting(含等待时间的等待状态):线程进入等待状态,但指定了等待时间,超时后会被唤醒。Terminated(终止状态):线程执行完成或因异常退出。wait 和 sleep 都是用于暂停线程的操作,但它们有明显的区别(先说面试官最关心的):
1)使用要求不同:
wait 方法必须在同步块或同步方法内调用,否则会抛出 IllegalMonitorStateException。这是因为 wait 依赖于对象锁来管理线程的等待和唤醒机制。调用后,当前线程会释放它持有的对象锁,并进入等待状态。sleep 方法可以在任何上下文中调用,不需要获取对象锁。调用后,线程会进入休眠状态,但不会释放它持有的任何锁。2)方法所属类不同:
wait:属于 Object 类。sleep:属于 Thread 类。3)恢复方式不同:
wait:需要被其他线程通过 notify 或 notifyAll 显式唤醒,或被 wait(long timeout) 的超时参数唤醒。sleep:在指定时间后自动恢复运行,或通过抛出 InterruptedException 恢复。4)用途不同:
wait:通常用于线程间通信,配合 notify 或 notifyAll 来实现线程的协调工作。sleep:用于让线程暂停执行一段时间,通常用于控制线程的执行频率或模拟延时。线程池是一种池化技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。
它几个关键的配置包括:核心线程数、最大线程数、空闲存活时间、工作队列、拒绝策略。
主要工作原理如下:
默认情况下线程不会预创建,任务提交之后才会创建线程(不过设置 prestartAllCoreThreads 可以预创建核心线程)。当核心线程满了之后不会新建线程,而是把任务堆积到工作队列中。如果工作队列放不下了,然后才会新增线程,直至达到最大线程数。如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行拒绝策略。如果线程空闲时间超过空闲存活时间,并且当前线程数大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置 allowCoreThreadTimeOut 为 true 可以回收核心线程,默认为 false)。Java 并发库中提供了 5 种常见的线程池实现,主要通过 Executors 工具类来创建。
1)FixedThreadPool:创建一个固定数量的线程池。
线程池中的线程数是固定的,空闲的线程会被复用。如果所有线程都在忙,则新任务会放入队列中等待。
适合负载稳定的场景,任务数量确定且不需要动态调整线程数。
2)CachedThreadPool:一个可以根据需要创建新线程的线程池。
线程池的线程数量没有上限,空闲线程会在 60 秒后被回收,如果有新任务且没有可用线程,会创建新线程。
适合短期大量并发任务的场景,任务执行时间短且线程数需求变化较大。
3)SingleThreadExecutor:创建一个只有单个线程的线程池。
只有一个线程处理任务,任务会按照提交顺序依次执行。
适用于需要保证任务按顺序执行的场景,或者不需要并发处理任务的情况。
4)ScheduledThreadPool:支持定时任务和周期性任务的线程池。
可以定时或以固定频率执行任务,线程池大小可以由用户指定。
适用于需要周期性任务执行的场景,如定时任务调度器。
5)WorkStealingPool:基于任务窃取算法的线程池。
线程池中的每个线程维护一个双端队列(deque),线程可以从自己的队列中取任务执行。如果线程的任务队列为空,它可以从其他线程的队列中“窃取”任务来执行,达到负载均衡的效果。
适合大量小任务并行执行,特别是递归算法或大任务分解成小任务的场景。
线程池的线程数设置需要看具体执行的任务是什么类型的。
任务类型可以分:CPU 密集型任务和 I/O 密集型任务。
CPU 密集型任务,就好比单纯的数学计算任务,它不会涉及 I/O 操作,也就是说它可以充分利用 CPU 资源(如果涉及 I/O,在进行 I/O 的时候 CPU 是空闲的),不会因为 I/O 操作被阻塞,因此不需要很多线程,线程多了上下文开销反而会变多。
根据经验法则,CPU 密集型任务线程数 = CPU 核心数 + 1。
I/O 密集型任务,有很多 I/O 操作,例如文件的读取、数据库的读取等等,任务在读取这些数据的时候,是无法利用 CPU 的,对应的线程会被阻塞等待 I/O 读取完成,因此如果任务比较多,就需要有更多的线程来执行任务,来提高等待 I/O 时候的 CPU 利用率。
根据经验法则,I/O 密集型任务线程数 = CPU 核心数 * 2 或更多一些。
(这句话一定要和面试官说)以上公式仅是一个纯理论值,仅供参考!在生产上,需要考虑机器的硬件配置,设置预期的 CPU 利用率、CPU负载等因素,再通过实际的测试不断调整得到合理的线程池配置参数。
Exception 和 Error 都是 Throwable 类的子类(在 Java 代码中只有继承了 Throwable 类的实例才可以被 throw 或者被 catch)它们表示在程序运行时发生的异常或错误情况。
总结来看:Exception 表示可以被处理的程序异常,Error 表示系统级的不可恢复错误。
详细说明:
1)Exception:是程序中可以处理的异常情况,表示程序逻辑或外部环境中的问题,可以通过代码进行恢复或处理。
常见子类有:IOException、SQLException、NullPointerException、IndexOutOfBoundsException 等。
Exception 又分为 Checked Exception(编译期异常)和 Unchecked Exception(运行时异常)。
Checked Exception:在编译时必须显式处理(如使用 try-catch 块或通过 throws 声明抛出)。如 IOException。Unchecked Exception:运行时异常,不需要显式捕获。常见的如 NullPointerException、IllegalArgumentException 等,继承自 RuntimeException。2)Error:表示严重的错误,通常是 JVM 层次内系统级的、无法预料的错误,程序无法通过代码进行处理或恢复。例如内存耗尽(OutOfMemoryError)、栈溢出(StackOverflowError)。
Error 不应该被程序捕获或处理,因为一般出现这种错误时程序无法继续运行。
Synchronized 是 Java 内置的关键字,实现基本的同步机制,不支持超时,非公平,不可中断,不支持多条件。
ReentrantLock 是 JUC 类库提供的,由 JDK 1.5 引入,支持设置超时时间,可以避免死锁,比较灵活,并且支持公平锁,可中断,支持多条件判断。
ReentrantLock 需要手动解锁,而 Synchronized 不需要,它们都是可重入锁。
一般情况下用 Synchronized 足矣,比较简单,而 ReentrantLock 比较灵活,支持的功能比较多,所以复杂的情况用 ReentrantLock 。
简单来说 AQS 就是起到了一个抽象、封装的作用,将一些排队、入队、加锁、中断等方法提供出来,便于其他相关 JUC 锁的使用,具体加锁时机、入队时机等都需要实现类自己控制。
它主要通过维护一个共享状态(state)和一个先进先出(FIFO)的等待队列,来管理线程对共享资源的访问。
state 用 volatile 修饰,表示当前资源的状态。例如,在独占锁中,state 为 0 表示未被占用,为 1 表示已被占用。
当线程尝试获取资源失败时,会被加入到 AQS 的等待队列中。这个队列是一个变体的 CLH 队列,采用双向链表结构,节点包含线程的引用、等待状态以及前驱和后继节点的指针。
AQS 常见的实现类有 ReentrantLock、CountDownLatch、Semaphore 等等。
分布式锁需要实现多个应用实例之间的临界资源竞争,因此它需要依赖三方组件才能实现这样的功能。
常见依赖 Redis、ZooKeeper 来实现分布式锁。
平时进行 SQL 调优,主要是通过观察慢 SQL,然后利用 explain 分析查询语句的执行计划,识别性能瓶颈,优化查询语句。
一般有以下几种情况不推荐建立索引:
1)对于数据量很小的表
2)频繁更新的表
3)执行大量的 SELECT
4)低选择性字段(高度重复值的列)
5)低频查询的列
6)长文本字段
CAS 是一种硬件级别的原子操作,它比较内存中的某个值是否为预期值,如果是,则更新为新值,否则不做修改。
工作原理:
比较(Compare):CAS 会检查内存中的某个值是否与预期值相等。交换(Swap):如果相等,则将内存中的值更新为新值。失败重试:如果不相等,说明有其他线程已经修改了该值,CAS 操作失败,一般会利用重试,直到成功。悲观锁(Pessimistic Locking):
假设会发生冲突,因此在操作数据之前就对数据加锁,确保其他事务无法访问该数据。常见于对数据一致性要求较高的场景。实现方式:使用行级锁或表级锁,例如可以使用 SELECT ... FOR UPDATE 或 LOCK IN SHARE MODE 语句来加锁。乐观锁(Optimistic Locking):
假设不会发生冲突,因此在操作数据时不加锁,而是在更新数据时进行版本控制或校验。如果发现数据被其他事务修改,则会拒绝当前事务的修改,需重新尝试。实现方式:通常通过版本号或时间戳来实现,每次更新时检查版本号或时间戳是否一致。分库分表是数据库性能优化的一种方法,通过将数据分散存储在多个数据库或表中,来提高系统的可扩展性、性能和可用性。
如果还想进一步了解 为什么需要分库分表,可以浏览面试鸭,获取扩展知识:美团后端日常一面 - 美团 Java 面经 - 面试鸭 - 程序员求职面试刷题神器
来源:程序员鱼皮