1. 线程状态有哪些?
在 Java 中,线程的生命周期可以细化为以下几个状态:
- New(初始状态):线程对象创建后,但未调用 start() 方法。
- Runnable(可运行状态):调用 start() 方法后,线程进入就绪状态,等待 CPU 调度。
- Blocked(阻塞状态):线程试图获取一个对象锁而被阻塞。
- Waiting(等待状态):线程进入等待状态,需要被显式唤醒才能继续执行。
- Timed Waiting(含等待时间的等待状态):线程进入等待状态,但指定了等待时间,超时后会被唤醒。
- Terminated(终止状态):线程执行完成或因异常退出。
2. wait 和 sleep 区别
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()
:用于让线程暂停执行一段时间,通常用于控制线程的执行频率或模拟延时。
3. 线程池有哪些参数?一般如何设置
线程池是一种池化技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。
它几个关键的配置包括:核心线程数、最大线程数、空闲存活时间、工作队列、拒绝策略。
主要工作原理如下:
- 默认情况下线程不会预创建,任务提交之后才会创建线程(不过设置 prestartAllCoreThreads 可以预创建核心线程)。
- 当核心线程满了之后不会新建线程,而是把任务堆积到工作队列中。
- 如果工作队列放不下了,然后才会新增线程,直至达到最大线程数。
- 如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行拒绝策略。
- 如果线程空闲时间超过空闲存活时间,并且当前线程数大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置 allowCoreThreadTimeOut 为 true 可以回收核心线程,默认为 false)。
Java 并发库中提供了 5 种常见的线程池实现 ,主要通过 Executors
工具类来创建。
1)FixedThreadPool:创建一个固定数量的线程池。
线程池中的线程数是固定的,空闲的线程会被复用。如果所有线程都在忙,则新任务会放入队列中等待。
适合负载稳定的场景,任务数量确定且不需要动态调整线程数。
2)CachedThreadPool:一个可以根据需要创建新线程的线程池。
线程池的线程数量没有上限,空闲线程会在 60 秒后被回收,如果有新任务且没有可用线程,会创建新线程。
适合短期大量并发任务的场景,任务执行时间短且线程数需求变化较大。
3)SingleThreadExecutor:创建一个只有单个线程的线程池。
只有一个线程处理任务,任务会按照提交顺序依次执行。
适用于需要保证任务按顺序执行的场景,或者不需要并发处理任务的情况。
4)ScheduledThreadPool:支持定时任务和周期性任务的线程池。
可以定时或以固定频率执行任务,线程池大小可以由用户指定。
适用于需要周期性任务执行的场景,如定时任务调度器。
5)WorkStealingPool:基于任务窃取算法的线程池。
线程池中的每个线程维护一个双端队列(deque),线程可以从自己的队列中取任务执行。如果线程的任务队列为空,它可以从其他线程的队列中"窃取"任务来执行,达到负载均衡的效果。
适合大量小任务并行执行,特别是递归算法或大任务分解成小任务的场景。
线程池的线程数设置需要看具体执行的任务是什么类型的。
任务类型可以分:CPU 密集型任务和 I/O 密集型任务。
CPU 密集型任务
CPU 密集型任务,就好比单纯的数学计算任务,它不会涉及 I/O 操作,也就是说它可以充分利用 CPU 资源(如果涉及 I/O,在进行 I/O 的时候 CPU 是空闲的),不会因为 I/O 操作被阻塞,因此不需要很多线程,线程多了上下文开销反而会变多。
根据经验法则,CPU 密集型任务线程数 = CPU 核心数 + 1
。
I/O 密集型任务
I/O 密集型任务,有很多 I/O 操作,例如文件的读取、数据库的读取等等,任务在读取这些数据的时候,是无法利用 CPU 的,对应的线程会被阻塞等待 I/O 读取完成,因此如果任务比较多,就需要有更多的线程来执行任务,来提高等待 I/O 时候的 CPU 利用率。
根据经验法则,I/O 密集型任务线程数 = CPU 核心数 * 2
或更多一些。
(这句话一定要和面试官说)以上公式仅是一个纯理论值,仅供参考!在生产上,需要考虑机器的硬件配置,设置预期的 CPU 利用率、CPU负载等因素,再通过实际的测试不断调整得到合理的线程池配置参数。
4. Error 和 Exception 有什么区别
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
不应该被程序捕获或处理,因为一般出现这种错误时程序无法继续运行。
5.Synchronized 和 ReentrantLock 有什么区别?
Synchronized 是 Java 内置的关键字,实现基本的同步机制,不支持超时,非公平,不可中断,不支持多条件。
ReentrantLock 是 JUC 类库提供的,由 JDK 1.5 引入,支持设置超时时间,可以避免死锁,比较灵活,并且支持公平锁,可中断,支持多条件判断。
ReentrantLock 需要手动解锁,而 Synchronized 不需要,它们都是可重入锁。
一般情况下用 Synchronized 足矣,比较简单,而 ReentrantLock 比较灵活,支持的功能比较多,所以复杂的情况用 ReentrantLock 。
6. 说下 AQS 原理
简单来说 AQS 就是起到了一个抽象、封装的作用,将一些排队、入队、加锁、中断等方法提供出来,便于其他相关 JUC 锁的使用,具体加锁时机、入队时机等都需要实现类自己控制。
它主要通过维护一个共享状态(state)和一个先进先出(FIFO)的等待队列,来管理线程对共享资源的访问。
state 用 volatile 修饰,表示当前资源的状态。例如,在独占锁中,state 为 0 表示未被占用,为 1 表示已被占用。
当线程尝试获取资源失败时,会被加入到 AQS 的等待队列中。这个队列是一个变体的 CLH 队列,采用双向链表结构,节点包含线程的引用、等待状态以及前驱和后继节点的指针。
AQS 常见的实现类有 ReentrantLock、CountDownLatch、Semaphore
等等。
7. 分布式锁
分布式锁需要实现多个应用实例之间的临界资源竞争,因此它需要依赖三方组件才能实现这样的功能。
常见依赖 Redis、ZooKeeper 来实现分布式锁。
8. 发现数据库查询很慢,如何排查
平时进行 SQL 调优,主要是通过观察慢 SQL,然后利用 explain 分析查询语句的执行计划,识别性能瓶颈,优化查询语句。
9. 索引数据结构 b+树 ,什么时候索引无效?
一般有以下几种情况不推荐建立索引:
1)对于数据量很小的表
2)频繁更新的表
3)执行大量的 SELECT
4)低选择性字段(高度重复值的列)
5)低频查询的列
6)长文本字段
10. CAS 在高并发的情况下会有问题吗
CAS 是一种硬件级别的原子操作,它比较内存中的某个值是否为预期值,如果是,则更新为新值,否则不做修改。
工作原理:
- 比较(Compare):CAS 会检查内存中的某个值是否与预期值相等。
- 交换(Swap):如果相等,则将内存中的值更新为新值。
- 失败重试:如果不相等,说明有其他线程已经修改了该值,CAS 操作失败,一般会利用重试,直到成功。
11. 乐观锁和悲观锁有什么区别?使用场景是?
悲观锁(Pessimistic Locking) :
- 假设会发生冲突,因此在操作数据之前就对数据加锁,确保其他事务无法访问该数据。常见于对数据一致性要求较高的场景。
- 实现方式:使用行级锁或表级锁,例如可以使用
SELECT ... FOR UPDATE
或LOCK IN SHARE MODE
语句来加锁。
乐观锁(Optimistic Locking) :
- 假设不会发生冲突,因此在操作数据时不加锁,而是在更新数据时进行版本控制或校验。如果发现数据被其他事务修改,则会拒绝当前事务的修改,需重新尝试。
- 实现方式:通常通过版本号或时间戳来实现,每次更新时检查版本号或时间戳是否一致。
12. 数据库分库分表
分库分表是数据库性能优化的一种方法,通过将数据分散存储在多个数据库或表中,来提高系统的可扩展性、性能和可用性。
如果还想进一步了解 为什么需要分库分表,可以浏览面试鸭,获取扩展知识:美团后端一面
13. 接口耗时较高,怎么排查?(除了业务优化)
看服务器相关监控
- CPU 和内存使用率:检查服务器的 CPU 和内存使用率是否过高,导致接口响应变慢。可以使用 top、htop 或者 vmstat 等工具进行监控。
- 磁盘 I/O:如果接口依赖于磁盘操作,如数据库查询或文件读写,磁盘 I/O 的瓶颈会影响接口性能。可以使用 iostat 或者 iotop 工具检查磁盘 I/O 情况。
- 数据库查询性能:检查数据库查询是否存在慢SQL或索引未命中情况。可以通过数据库自带的慢查询日志或 EXPLAIN 语句分析 SQL 语句的执行计划。
- 线程池监控:如果接口使用线程池处理请求,线程池配置不合理(如线程数过少或队列过长)可能导致请求等待时间过长。需要检查线程池的大小和队列情况,并根据负载适当调整。
- 连接池监控:如果使用了连接池(数据库连接池、HTTP 连接池等),需要检查连接池的配置是否合理。连接池大小过小可能导致连接等待时间过长,大小过大则可能耗尽资源。
日志追踪
- 分布式追踪:使用分布式追踪工具(如 Zipkin、Jaeger)来跟踪接口调用的各个阶段,找出耗时较长的环节。
- 日志分析:启用详细的日志记录,包括请求接收时间、处理时间、外部依赖(如数据库、外部服务)调用时间。通过分析日志,确定耗时主要集中在哪个阶段。
- APM 工具:使用应用性能监控(APM)工具(如 New Relic、AppDynamics、Prometheus)来监控接口的性能指标,实时分析性能瓶颈。
14. Object 里都有什么?
以下是 Object
类中的主要方法及其作用:
1. public boolean equals(Object obj)
- 作用:用于比较两个对象是否相等。默认实现比较对象的内存地址,即判断两个引用是否指向同一个对象。
- 使用:通常会重写此方法来比较对象的内容或特定属性,以定义对象的相等性。
2. public int hashCode()
- 作用 :返回对象的哈希码,是对象的整数表示。哈希码用于支持基于哈希的集合(如
HashMap
和HashSet
)。 - 使用 :如果重写了
equals
方法,则通常也需要重写hashCode
方法,以保证相等的对象具有相同的哈希码。
3. public String toString()
- 作用:返回对象的字符串表示。默认实现返回对象的类名加上其哈希码的十六进制表示。
- 使用:通常会重写此方法以提供对象的更有意义的描述。
4. public final Class<?> getClass()
- 作用 :返回对象的运行时类(
Class
对象)。此方法是Object
类中的一个 final 方法,不能被重写。 - 使用:可以用来获取对象的类信息,常用于反射操作。
5. public void notify()
- 作用:唤醒在对象的监视器上等待的一个线程。该方法需要在同步块或同步方法中调用。
- 使用:用于在多线程环境中进行线程间的通信和协调。
6. public void notifyAll()
- 作用:唤醒在对象的监视器上等待的所有线程。该方法需要在同步块或同步方法中调用。
- 使用 :与
notify()
相似,但唤醒所有等待线程,用于处理多个线程之间的协作。
7. public void wait()
- 作用 :使当前线程等待,直到其他线程调用
notify()
或notifyAll()
方法。此方法需要在同步块或同步方法中调用。 - 使用:用于线程间的通信,线程会等待直到被唤醒或超时。
8. public void wait(long timeout)
- 作用:使当前线程等待,直到指定的时间到期或被唤醒。超时后线程会自动被唤醒。
- 使用:用于实现带有超时的等待机制。
9. public void wait(long timeout, int nanos)
- 作用:使当前线程等待,直到指定的时间和纳秒数到期或被唤醒。
- 使用:用于实现更精细的等待控制,允许指定等待时间的精确到纳秒。
10. protected Object clone()
- 作用:创建并返回当前对象的一个副本。默认实现是进行浅拷贝。
- 使用:通常会重写此方法来实现深拷贝,以确保克隆对象的完整性。
11. protected void finalize()
- 作用:在垃圾回收器确定不存在对该对象的更多引用时调用,用于进行资源释放等清理工作。
- 使用 :不建议使用,因为它依赖于垃圾回收器的实现,可能会导致不确定的性能问题。推荐使用
try-with-resources
和AutoCloseable
接口进行资源管理。
更多面经及答案可以打开面试鸭阅读学习 ➡️ :www.mianshiya.com/