(高频问题)281-300 计算机 Java后端 实习 and 秋招 面试高频问题汇总
281. ReentrantLock内部实现机制及其与重量级锁的关系
ReentrantLock
是 Java java.util.concurrent.locks
包中提供的一种显式锁实现,它依赖于 AbstractQueuedSynchronizer
(AQS) 作为其核心同步框架。
AQS 是一个用于构建锁和同步器的基础框架,其核心设计包括一个整型的 state
字段和一个先进先出 (FIFO) 的等待队列。对于 ReentrantLock
而言:
state
字段表示锁的持有次数。当state
为 0 时,锁未被任何线程持有;当一个线程获取锁后,state
变为 1。如果同一个线程再次获取该锁(即可重入性),state
的值会相应增加。每次释放锁,state
减 1,直到state
变为 0,锁才完全释放。- 等待队列用于管理那些尝试获取锁失败而被阻塞的线程。这些线程会以节点的形式进入队列排队,等待被前一个持有锁的线程唤醒。
ReentrantLock
的获取锁过程体现了从轻量级到重量级的逐步升级:
- 尝试CAS获取:当线程尝试获取锁时,首先会尝试通过CAS (Compare-And-Swap) 原子操作来修改
state
字段。如果成功,表示无竞争或竞争很小,这属于轻量级的锁获取方式。 - 自旋尝试:在某些实现或配置下,如果CAS操作失败,线程可能会进行短暂的自旋(循环尝试获取锁),以期锁能很快被释放。这仍然可以被视为轻量级锁的范畴。
- 阻塞与入队:如果CAS和自旋都未能获取到锁,表明存在较强的竞争。此时,线程会被构造成一个节点加入到AQS的等待队列中,并被挂起(阻塞)。线程的阻塞和唤醒需要操作系统的介入,这使得锁的操作进入了重量级锁的范畴,因为涉及到用户态到内核态的切换以及线程调度。
ReentrantLock
本身可以被认为是重量级锁,因为它在发生竞争时会涉及到线程的阻塞和唤醒,这依赖于操作系统的调度机制,开销较大。然而,它通过CAS和可能的自旋尝试,在低竞争情况下尽量减少了这种重量级操作的发生。
与此相对,synchronized
关键字在Java的早期版本中确实是纯粹的重量级锁,直接依赖操作系统的互斥量(mutex)。但随着JVM的优化,synchronized
引入了偏向锁、轻量级锁的优化机制。
- 偏向锁:在无竞争或只有一个线程反复获取锁的场景下,锁对象会“偏向”于该线程,后续该线程获取锁的开销极低。
- 轻量级锁:当有其他线程尝试获取偏向锁时,偏向锁会升级为轻量级锁。轻量级锁通过CAS操作尝试获取锁,避免了操作系统层面的互斥。
- 重量级锁:当轻量级锁的CAS竞争失败达到一定次数或有多个线程同时竞争时,锁会膨胀为重量级锁,此时行为与
ReentrantLock
竞争激烈时类似,线程会被阻塞。
因此,现代JVM中的 synchronized
在无竞争或低竞争时表现得非常轻量,只有在高竞争下才会表现为重量级锁。
282. 在MySQL中使用BLOB类型存储大文件的策略
在MySQL中,若需要在数据库中直接存储大文件(如图片、音频、视频或大型文档),可以使用二进制大对象 (Binary Large Object, BLOB) 数据类型。BLOB类型专门用于存储可变长度的二进制数据。
MySQL提供了几种不同容量的BLOB类型,以便根据实际文件大小选择合适的类型,从而优化存储空间和性能:
TINYBLOB
:最大存储容量为 255字节。适用于存储极小的二进制数据,如缩略图图标或非常短的二进制序列。BLOB
:最大存储容量为 65,535字节 (约64KB)。适用于存储中等大小的二进制数据,如小型图片或短音频片段。MEDIUMBLOB
:最大存储容量为 16,777,215字节 (约16MB)。适用于存储较大的二进制数据,如中等质量的图片或较长的音频文件。LONGBLOB
:最大存储容量为 4,294,967,295字节 (约4GB)。这是容量最大的BLOB类型,适用于存储非常大的二进制数据,例如高清图片、视频文件或大型文档。
在设计表结构时,应根据预期存储文件的最大尺寸选择最合适的BLOB类型。例如,如果需要存储可能达到几百MB的视频文件,则应选择LONGBLOB
类型。将文件数据作为字节流直接插入到相应的BLOB字段即可。
需要注意的是,虽然数据库提供了存储大文件的能力,但直接在数据库中存储非常大的文件可能会影响数据库的性能、备份恢复效率以及整体管理复杂度。在某些场景下,更常见的做法是将大文件存储在专门的文件存储系统(如分布式文件系统、对象存储服务)中,而在数据库中仅存储文件的元数据和指向文件存储位置的链接(URL或路径)。
283. 在Redis中高效查找具有相同前缀的大量Key
要在Redis的十亿级别数据中查找具有特定前缀的约十万个Key,直接使用KEYS prefix*
命令是绝对不推荐的,因为它会阻塞整个Redis服务器,直到扫描完所有Key,这对于生产环境是灾难性的。
正确的做法是使用 SCAN
命令 及其家族(HSCAN
, SSCAN
, ZSCAN
)。SCAN
命令用于增量式地迭代当前数据库中的键空间。
SCAN
命令的语法是 SCAN cursor [MATCH pattern] [COUNT count]
:
cursor
:这是一个游标,用于指示迭代的当前位置。初次调用时,cursor
设为0
。每次SCAN
命令执行后,会返回一个新的cursor
值,下一次调用SCAN
时应使用这个新的cursor
。当返回的cursor
再次为0
时,表示整个迭代过程完成。MATCH pattern
:这是一个可选参数,用于指定一个匹配模式。你可以使用通配符,例如MATCH your_prefix*
来匹配所有以your_prefix
开头的Key。COUNT count
:这是一个可选参数,用于提示Redis在单次迭代中返回的元素数量的近似值,默认是10。增大COUNT
值可以减少迭代次数,但可能会增加单次迭代的响应时间。选择合适的COUNT
值需要在迭代效率和单次操作影响之间做权衡。
使用SCAN
进行查找的步骤如下:
- 初始化
cursor
为0
。 - 循环执行
SCAN cursor MATCH "your_prefix*" COUNT desired_count
(例如desired_count
可以设为1000或更高以减少网络往返)。 - 处理每次
SCAN
命令返回的Key列表。 - 将返回的新
cursor
用于下一次SCAN
调用。 - 当返回的
cursor
为0
时,停止迭代。
这种方式的优点是:
- 非阻塞:
SCAN
命令的执行时间是有限的,不会长时间阻塞Redis。 - 增量迭代:逐步返回结果,客户端可以分批处理。
对于哈希表(Hash)、集合(Set)和有序集合(Sorted Set)内部元素的扫描,分别有对应的HSCAN
、SSCAN
和ZSCAN
命令,它们的使用方式与SCAN
类似,但作用于特定数据结构内部的元素。
284. Thread.sleep()
方法与锁的关系解析
调用Thread.sleep()
方法不需要获取任何锁,并且如果当前线程已经持有了某个锁,Thread.sleep()
方法也绝对不会释放该线程已经持有的任何锁。
Thread.sleep(long millis)
是一个静态方法,其作用是使当前正在执行的线程暂停(休眠)指定的毫秒数。在这个休眠期间,线程会暂时让出CPU的执行权,进入TIMED_WAITING
状态。当休眠时间结束,或者线程被中断时,它会重新变为RUNNABLE
状态,等待CPU调度器再次分配时间片。
sleep()
方法仅仅是暂停当前线程的活动,它与Java的同步机制(如synchronized
关键字或Lock
接口)是完全独立的。
与之形成对比的是Object.wait()
方法:
wait()
方法必须在当前线程已经获得该对象的监视器锁(即在synchronized
代码块或方法中)的情况下才能调用。- 调用
wait()
方法后,当前线程会释放其持有的该对象的监视器锁,并进入WAITING
或TIMED_WAITING
状态,直到被其他线程通过notify()
或notifyAll()
唤醒,或者等待超时。 wait()
方法主要用于线程间的协作和通信。
总结来说,Thread.sleep()
仅仅是让当前线程“睡一会儿”,不涉及任何锁的获取与释放。
285. 布隆过滤器及其变种对元素删除操作的支持
标准布隆过滤器 (Bloom Filter) 的一个显著特点是它不支持直接、安全地删除已添加的元素。
布隆过滤器使用一个位数组(bit array)和多个独立的哈希函数。当添加一个元素时,该元素会被多个哈希函数映射到位数组中的多个位置,这些位置的位会被设置为1。查询一个元素是否存在时,同样通过哈希函数计算出对应的位,如果所有这些位都为1,则认为元素可能存在(存在一定的假阳性概率);如果有任何一个位为0,则元素一定不存在。
删除操作的困难在于,多个不同的元素可能通过不同的哈希函数映射到相同的位上(哈希冲突)。如果简单地将被删除元素对应的位重置为0,那么可能会错误地影响其他共享这些位的、仍然存在的元素的判断,导致它们被误判为不存在(即引入了假阴性,这是标准布隆过滤器不允许的)。
为了解决这个问题,并支持删除操作,引入了一种布隆过滤器的变种——计数布隆过滤器 (Counting Bloom Filter)。计数布隆过滤器与标准布隆过滤器的主要区别在于,它使用的不是简单的位数组,而是一个计数器数组。数组中的每个槽位不再是单个比特,而是一个计数器(例如一个4位或8位的整数)。
计数布隆过滤器的工作机制如下:
- 插入操作:当插入一个元素时,该元素同样被多个哈希函数映射到计数器数组中的多个位置。但不是将位设为1,而是将这些位置上对应的计数器值加1。
- 查询操作:查询一个元素是否存在时,检查其哈希函数映射到的所有计数器位置。如果所有这些位置的计数器值都大于0,则认为元素可能存在。如果任何一个位置的计数器值为0,则元素一定不存在。
- 删除操作:当删除一个元素时,将其通过哈希函数映射到的所有计数器位置上的计数器值减1。
通过使用计数器,计数布隆过滤器可以安全地支持删除操作,同时仍然保持较低的假阳性率(取决于计数器大小和哈希函数数量)。当然,计数布隆过滤器相比标准布隆过滤器需要更多的存储空间,因为每个槽位存储的是一个计数器而不是单个比特。
289. 服务器直接重启时线程池的关闭行为与优雅关闭策略
当服务器被直接重启(例如通过操作系统命令 reboot
或因硬件故障等非正常关机),Java虚拟机(JVM)进程会被强制终止。在这种情况下,JVM默认不会自动执行线程池的优雅关闭流程,如调用 shutdown()
或 shutdownNow()
方法。
这意味着:
- 线程池中正在执行的任务可能会被突然中断。
- 等待队列中尚未开始执行的任务将会丢失。
- 线程池本身持有的资源(如线程)不会被 orderly 地释放。
常规的线程池关闭流程(在应用程序主动控制关闭时)通常是:
- 调用
shutdown()
方法:线程池进入关闭状态,不再接受新的任务提交。已经提交到队列中但尚未开始执行的任务,以及正在执行的任务,会继续执行完毕。 - 调用
shutdownNow()
方法:线程池立即尝试停止所有正在执行的任务(通过中断线程),并返回等待队列中所有未执行的任务列表。这是一种更激进的关闭方式,可能导致任务未能完成。 - 使用
awaitTermination(long timeout, TimeUnit unit)
方法可以等待线程池完成关闭,或者超时。
为了在服务器重启或应用程序退出时尽可能实现线程池的优雅关闭,最佳实践是注册一个 JVM关闭钩子 (Shutdown Hook)。
Runtime.getRuntime().addShutdownHook(new Thread(() -> { // 在这里执行线程池的关闭逻辑 yourExecutorService.shutdown(); // 或者 executorService.shutdownNow(); try { // 等待一段时间让任务完成 if (!yourExecutorService.awaitTermination(60, TimeUnit.SECONDS)) { // 超时后,可以尝试更强制的关闭 yourExecutorService.shutdownNow(); // 再次等待 if (!yourExecutorService.awaitTermination(60, TimeUnit.SECONDS)) { System.err.println(
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
曾获多国内大厂的 ssp 秋招 offer,且是Java5年的沉淀老兵(不是)。专注后端高频面试与八股知识点,内容系统详实,覆盖约 30 万字面试真题解析、近 400 个热点问题(包含大量场景题),60 万字后端核心知识(含计网、操作系统、数据库、性能调优等)。同时提供简历优化、HR 问题应对、自我介绍等通用能力。考虑到历史格式混乱、质量较低、也在本地积累了大量资料,故准备从头重构专栏全部内容