(高频问题)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 的获取锁过程体现了从轻量级到重量级的逐步升级:

  1. 尝试CAS获取:当线程尝试获取锁时,首先会尝试通过CAS (Compare-And-Swap) 原子操作来修改 state 字段。如果成功,表示无竞争或竞争很小,这属于轻量级的锁获取方式。
  2. 自旋尝试:在某些实现或配置下,如果CAS操作失败,线程可能会进行短暂的自旋(循环尝试获取锁),以期锁能很快被释放。这仍然可以被视为轻量级锁的范畴。
  3. 阻塞与入队:如果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进行查找的步骤如下:

  1. 初始化 cursor0
  2. 循环执行 SCAN cursor MATCH "your_prefix*" COUNT desired_count (例如 desired_count 可以设为1000或更高以减少网络往返)。
  3. 处理每次SCAN命令返回的Key列表。
  4. 将返回的新cursor用于下一次SCAN调用。
  5. 当返回的cursor0时,停止迭代。

这种方式的优点是:

  • 非阻塞SCAN命令的执行时间是有限的,不会长时间阻塞Redis。
  • 增量迭代:逐步返回结果,客户端可以分批处理。

对于哈希表(Hash)、集合(Set)和有序集合(Sorted Set)内部元素的扫描,分别有对应的HSCANSSCANZSCAN命令,它们的使用方式与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()方法后,当前线程会释放其持有的该对象的监视器锁,并进入WAITINGTIMED_WAITING状态,直到被其他线程通过notify()notifyAll()唤醒,或者等待超时。
  • wait()方法主要用于线程间的协作和通信。

总结来说,Thread.sleep()仅仅是让当前线程“睡一会儿”,不涉及任何锁的获取与释放。

285. 布隆过滤器及其变种对元素删除操作的支持

标准布隆过滤器 (Bloom Filter) 的一个显著特点是它不支持直接、安全地删除已添加的元素

布隆过滤器使用一个位数组(bit array)和多个独立的哈希函数。当添加一个元素时,该元素会被多个哈希函数映射到位数组中的多个位置,这些位置的位会被设置为1。查询一个元素是否存在时,同样通过哈希函数计算出对应的位,如果所有这些位都为1,则认为元素可能存在(存在一定的假阳性概率);如果有任何一个位为0,则元素一定不存在。

删除操作的困难在于,多个不同的元素可能通过不同的哈希函数映射到相同的位上(哈希冲突)。如果简单地将被删除元素对应的位重置为0,那么可能会错误地影响其他共享这些位的、仍然存在的元素的判断,导致它们被误判为不存在(即引入了假阴性,这是标准布隆过滤器不允许的)。

为了解决这个问题,并支持删除操作,引入了一种布隆过滤器的变种——计数布隆过滤器 (Counting Bloom Filter)。计数布隆过滤器与标准布隆过滤器的主要区别在于,它使用的不是简单的位数组,而是一个计数器数组。数组中的每个槽位不再是单个比特,而是一个计数器(例如一个4位或8位的整数)。

计数布隆过滤器的工作机制如下:

  1. 插入操作:当插入一个元素时,该元素同样被多个哈希函数映射到计数器数组中的多个位置。但不是将位设为1,而是将这些位置上对应的计数器值加1
  2. 查询操作:查询一个元素是否存在时,检查其哈希函数映射到的所有计数器位置。如果所有这些位置的计数器值都大于0,则认为元素可能存在。如果任何一个位置的计数器值为0,则元素一定不存在。
  3. 删除操作:当删除一个元素时,将其通过哈希函数映射到的所有计数器位置上的计数器值减1

通过使用计数器,计数布隆过滤器可以安全地支持删除操作,同时仍然保持较低的假阳性率(取决于计数器大小和哈希函数数量)。当然,计数布隆过滤器相比标准布隆过滤器需要更多的存储空间,因为每个槽位存储的是一个计数器而不是单个比特。

289. 服务器直接重启时线程池的关闭行为与优雅关闭策略

当服务器被直接重启(例如通过操作系统命令 reboot 或因硬件故障等非正常关机),Java虚拟机(JVM)进程会被强制终止。在这种情况下,JVM默认不会自动执行线程池的优雅关闭流程,如调用 shutdown()shutdownNow() 方法。

这意味着:

  • 线程池中正在执行的任务可能会被突然中断。
  • 等待队列中尚未开始执行的任务将会丢失。
  • 线程池本身持有的资源(如线程)不会被 orderly 地释放。

常规的线程池关闭流程(在应用程序主动控制关闭时)通常是:

  1. 调用 shutdown() 方法:线程池进入关闭状态,不再接受新的任务提交。已经提交到队列中但尚未开始执行的任务,以及正在执行的任务,会继续执行完毕。
  2. 调用 shutdownNow() 方法:线程池立即尝试停止所有正在执行的任务(通过中断线程),并返回等待队列中所有未执行的任务列表。这是一种更激进的关闭方式,可能导致任务未能完成。
  3. 使用 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 问题应对、自我介绍等通用能力。考虑到历史格式混乱、质量较低、也在本地积累了大量资料,故准备从头重构专栏全部内容

全部评论

相关推荐

评论
2
1
分享

创作者周榜

更多
牛客网
牛客企业服务