Cyan Blog Cyan Blog
首页
  • Java (opens new window)
  • JUC (opens new window)
  • JVM (opens new window)
  • Redis

    • Redis安装 (opens new window)
    • Redis基础 (opens new window)
    • Redis实战 (opens new window)
    • Redis集群安装 (opens new window)
    • Redis分布式缓存 (opens new window)
    • Redis多级缓存 (opens new window)
    • Redis原理 (opens new window)
  • 管理工具

    • Maven (opens new window)
    • Git (opens new window)
  • SSM

    • Spring (opens new window)
    • SpringBoot (opens new window)
    • Mybatis (opens new window)
    • MybatisPlus (opens new window)
  • 微服务

    • Docker (opens new window)
    • RabbitMQ (opens new window)
    • SpringCloud (opens new window)
    • Dubbo (opens new window)
    • MongoDB (opens new window)
    • Zookeeper (opens new window)
  • Java面试题 (opens new window)
  • JUC面试题 (opens new window)
  • JVM面试题 (opens new window)
  • Linux面试题 (opens new window)
  • SQL面试题 (opens new window)
  • Maven面试题 (opens new window)
  • Redis面试题 (opens new window)
  • SSM面试题 (opens new window)
  • SpringCloud面试题 (opens new window)
  • Linux (opens new window)
  • C++ (opens new window)
  • 数据库

    • MySQL (opens new window)
    • NoSQL (opens new window)
  • 软件测试

    • 软件测试 (opens new window)
  • 加密解密 (opens new window)
  • bilibili字幕提取 (opens new window)
  • 道理 (opens new window)
  • 关于博主

    • Github (opens new window)
    • CSDN (opens new window)
  • 关于本站

    • 如何搭建博客网站 (opens new window)
首页
  • Java (opens new window)
  • JUC (opens new window)
  • JVM (opens new window)
  • Redis

    • Redis安装 (opens new window)
    • Redis基础 (opens new window)
    • Redis实战 (opens new window)
    • Redis集群安装 (opens new window)
    • Redis分布式缓存 (opens new window)
    • Redis多级缓存 (opens new window)
    • Redis原理 (opens new window)
  • 管理工具

    • Maven (opens new window)
    • Git (opens new window)
  • SSM

    • Spring (opens new window)
    • SpringBoot (opens new window)
    • Mybatis (opens new window)
    • MybatisPlus (opens new window)
  • 微服务

    • Docker (opens new window)
    • RabbitMQ (opens new window)
    • SpringCloud (opens new window)
    • Dubbo (opens new window)
    • MongoDB (opens new window)
    • Zookeeper (opens new window)
  • Java面试题 (opens new window)
  • JUC面试题 (opens new window)
  • JVM面试题 (opens new window)
  • Linux面试题 (opens new window)
  • SQL面试题 (opens new window)
  • Maven面试题 (opens new window)
  • Redis面试题 (opens new window)
  • SSM面试题 (opens new window)
  • SpringCloud面试题 (opens new window)
  • Linux (opens new window)
  • C++ (opens new window)
  • 数据库

    • MySQL (opens new window)
    • NoSQL (opens new window)
  • 软件测试

    • 软件测试 (opens new window)
  • 加密解密 (opens new window)
  • bilibili字幕提取 (opens new window)
  • 道理 (opens new window)
  • 关于博主

    • Github (opens new window)
    • CSDN (opens new window)
  • 关于本站

    • 如何搭建博客网站 (opens new window)
  • 简历项目
  • Java面试题

    • Java面试题
    • JUC面试题

      • JUC基础知识
      • 线程并发安全
      • 线程池
      • JUC面试题
        • 谈谈你对ThreadLocal的理解
        • ThreadLocal的底层原理实现吗?
        • ThreadLocal会导致内存溢出
        • ThreadLocal有哪些使用场景
        • volatile关键字有什么用
        • 如何理解线程安全
        • 什么是守护线程?
        • 伪共享的概念以及如何避免
        • DCL单例模式为什么需要volatile关键字修饰
        • 请你说一下对Happens-Before的理解
    • JVM面试题

    • Java集合面试题
  • 后端开发
  • 数据库面试题
  • Maven
  • MyBatis面试题
  • Spring面试题
  • SpringBoot面试题
  • SpringCloud面试题

  • Redis面试题

  • 设计模式
  • 中间件

  • 场景题
  • Other

  • Random

  • 项目面试题

  • 面试
  • Java面试题
  • JUC面试题
2025-03-21
0
0
目录

JUC面试题

# 谈谈你对ThreadLocal的理解

ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享,不同方法可以共享同一个 ThreadLocal 变量。

底层原理

# ThreadLocal的底层原理实现吗?

ThreadLocal其实是线程Thread类在内部维护了一个 ThreadLocalMap 类型的成员变量,用来存储资源对象

当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中

当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值

当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值

# ThreadLocal会导致内存溢出 (opens new window)

ThreadLocal 如果使用不当,确实可能导致内存泄漏,核心原因是 ThreadLocalMap 的 key 是弱引用,而 value 是强引用,加上线程复用(如线程池),无效的 Entry 无法被及时清理。

ThreadLocal 的数据存储在 线程的 ThreadLocalMap 中,key 是 ThreadLocal 对象本身, 它是弱引用,会被 GC 回收变为 null,但 Value 仍被 Entry 强引用,导致无法回收。

当线程一直存在不被清除时,比如线程池复用时,ThreadLocalMap 会一直存在

ThreadLocal有两种情况导致OOM:

当 ThreadLocal 是局部变量,用完被清除,失去强引用,key 因弱引用被 GC 回收变为null,而value会一直存在导致OOM。 当ThreadLocal 是静态变量。虽然 key 不会被回收(静态变量是强引用),但如果线程复用时不调用 remove(),多次 set() 会导致旧 value 无法释放(例如线程池任务中重复使用同一个 ThreadLocal)。

解决方法:

threadLocalMap有清除机制,会在调用set() / get() 时自动清除key为null的数据。 但是使用ThreadLocal 时通常把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。

总:

不恰当使用 ThreadLocal 可能导致内存泄漏。主要原因在于其底层 ThreadLocalMap 的 key 是一个弱引用(WeakReference)。弱引用的特性是:无论是否存在直接引用关系,只要 ThreadLocal 实例没有其他强引用关联,垃圾回收(GC)时 key 就会被回收。从而导致 key 变为 null,但 value 仍被强引用关联,造成这块内存无法访问和回收,出现内存泄漏的问题

规避内存泄漏的两种方法:

扩大 ThreadLocal 变量的作用域(例如声明为 static 或全局变量),避免 ThreadLocal 实例被 GC 回收,不过若后续线程不再访问该 key,value 仍会长期占用内存,最终可能导致内存溢出(OOM)。 在数据使用完成后主动调用remove方法清除 Entry,这是在实际使用中最好的方式

# ThreadLocal有哪些使用场景

ThreadLocal是一种多线程隔离机制,通过多线程环境下对共享变量的副本存储,解决了线程安全问题,避免了多线程竞争加锁的开销。其使用场景包括:

  • 线程的上下文传递

    • 在处理请求的过程中保持用户特定的数据(如用户的登录信息)。通过 ThreadLocal 可以方便地在同一个线程内的不同方法调用之间共享这些数据,而不用担心线程安全问题。
  • 数据库连接管理

    • 在多线程应用中,每个线程可以使用 ThreadLocal 来独立管理自己的数据库连接,避免线程之间的竞争与冲突。如mybatis的sqlsession
  • 事务管理等

    • 使用 ThreadLocal 可以让每个线程拥有独立的事务上下文,保证事务的隔离性。Spring 的 TransactionSynchronizationManager 就使用 ThreadLocal 来存储当前线程的事务资源(如数据库连接)。

在使用ThreadLocall时,需要注意避免内存泄漏的问题。

# volatile关键字有什么用

volatile关键字有两个作用 可以保证多线程环境下,对共享变量的可见性, 可以通过增加内存屏障,去防止多个指令之间的重排序,

可见性就是当一个线程对共享变量的修改,其他线程可以立刻看到修改之后的值, 可见性问题,是在多线程环境下,每个线程都有自己的工作内存(缓存),线程操作变量时,可能会先读取缓存中的副本,而不是直接访问主内存。这可能导致一个线程修改了变量,但另一个线程看不到最新值。

  • 当一个线程修改 volatile 变量时,会立即将新值写回主内存。
  • 其他线程读取该变量时,会强制从主内存重新加载最新值,而不是使用本地缓存。

指令重排序就是指令在编写的顺序和执行顺序是不一致的,为了提高性能,JVM 和 CPU 可能会对指令进行重排序(在不改变单线程执行结果的前提下)。但在多线程环境下,重排序可能导致线程安全问题(如单例模式的双重检查锁失效)。

  • 通过内存屏障(Memory Barrier) 禁止 JVM 对 volatile 变量的读写操作进行重排序。
  • 确保 volatile 变量的写操作一定在读操作之前完成(happens-before 原则)。

# 如何理解线程安全

线程安全是指在多线程环境下,当多个线程同时访问共享资源时,程序能够正确、一致地工作。这要求对共享数据的访问必须是原子的、可见的和有序的。

原子性:一个线程访问共享资源时的操作不能被中断,如果被中断,可能会导致执行结果不一致的问题,CPU上下文切换是导致原子性问题的一个核心,jvm提供了synchronized关键字来解决原子性问题。 可见性:一个线程对共享变量的修改能够及时被其他线程看到,导致可见性问题的问题有很多,比如像CPU的高速缓存、CPU的指令重排序、编译器的指令重排序 有序性:程序编写的指令顺序和CPU最终运行的指令顺序可能不一致,这种现象称为指令重排序

可见性和有序性问题可以通过volatile关键字解决

实现线程安全的常见方法包括:使用同步机制、使用线程安全的数据结构。但是过度同步会影响性能,因此需要根据具体场景选择合适的线程安全策略

# 保证线程安全

  • 在方法内使用,局部变量则是线程安全的
  • 使用线程安全的ArrayList和LinkedList

# SimpleDateFormat 是线程安全的吗

SimpleDateFormat 内部维护了一个 Calendar 对象引用,用于存储和解析日期信息。当多个线程共享同一个 SimpleDateFormat 实例时,会出现以下问题:

  1. 共享 Calendar 引用:多个线程同时操作同一个 Calendar 对象,导致数据竞争。
  2. 数据不一致:可能引发日期解析错误、格式化异常,甚至程序崩溃。

有四种方法可以解决这个问题 第一种把SimpleDateFormat定义成一个局部变量,每个线程调用这个方法的时候,都创建一个新的实例, 第二种我们可以使用ThreadLocal,把SimpleDateFormat变成一个线程私有的, 第三种加一个同步锁,在同一个时刻,只允许一个线程去操作SimpleDateFormat, 第四种,在java8里面呢引入了一些线程安全的日期API,比如说像localDataTimer,DateTimeFormatter等等

# 什么是守护线程?

守护线程(Daemon Thread)是一种在后台运行的线程,它的生命周期依赖于非守护线程(即用户线程)。当所有的非守护线程结束时,JVM 会自动退出,并终止所有仍在运行的守护线程,即使它们还未完成任务

守护线程的创建方法和普通线程是一样的,只需要调用setDaemon方法设置参数为true就表示创建一个守护线程

守护线程通常用于执行非关键任务,如垃圾回收、日志记录等,这个场景的特点就是当jvm 的进程结束后,守护线程的本身就没有存在的 意义了,不能因为进行垃圾回收就阻止jvm进程无法结束。而守护线程不能用于线程池或者IO的一些任务场景,因为一旦jvm退出后,守护线程就会直接退出,导致任务还没有执行完成或者资源没有释放等问题。

# 伪共享的概念以及如何避免

由于CPU采用缓存行(Cache Line,64字节)加载数据(基于空间局部性原理),若多个线程修改同一缓存行中的不同变量(如变量X和Y),会触发缓存一致性协议(如MESI),导致其他CPU的缓存行失效,这种不必要的竞争称为伪共享,会显著降低并发性能。

解决办法有两个

  1. 使用对齐填充,读取的目标数据,小于64个字节时,可以增加一些无意义的成员变量来填充到64个字节,
  2. 在java8提供了@Contented的注解,被@Contented的注解标记的类或者字段,JVM会自动填充缓存行

# DCL单例模式为什么需要volatile关键字修饰

DCL单例模式(Double-Checked Locking)通过双重检验机制实现线程安全的延迟初始化:

  1. 第一次检查:判断 instance 是否已初始化(避免已初始化时进入同步块,提升性能)。
  2. 同步锁:若未初始化,通过 synchronized 防止多线程并发创建实例。
  3. 第二次检查:在同步块内再次检查 instance(防止首次初始化时多个线程通过第一次检查后的重复创建)。

通过第二次检查的进程会执行 instance = new Singleton() 的初始化操作

但这个操作并非原子操作,实际会被JVM拆分为以下三条指令:

  1. 分配对象内存空间
  2. 初始化对象(调用构造函数)
  3. 将引用赋值给 instance

由于指令重排序(JVM优化),可能导致步骤3先于步骤2执行。此时若其他线程访问 instance,会获取到未初始化的对象(非 null 但状态不一致)。

所以需要使用volatile关键字修饰instance变量,volatile会在底层使用了一个内存屏障机制,来去避免指令重排序,并强制线程每次访问 instance 时从主内存读取最新值。

# 请你说一下对Happens-Before的理解

Happens-Before 是 Java 内存模型(JMM)提供的一种内存可见性保证机制。在多线程环境下,由于指令重排序和CPU缓存一致性问题,可能导致一个线程对共享变量的修改对另一个线程不可见。JMM 通过 Happens-Before 关系,向开发者提供了一种跨线程的内存可见性约束,确保某些操作的执行结果对其他操作可见。

Happens-Before 的核心概念:

  1. 可见性保证:如果操作 A Happens-Before 操作 B,那么 A 的执行结果对 B 可见。
  2. 不约束执行顺序:Happens-Before 仅规定可见性,并不强制要求指令的实际执行顺序。只要不影响最终结果,JVM 仍然可以优化指令顺序。

JMM中存在的happy before规则,

  1. 程序顺序规则:同一线程中的每个操作 happens-before 于该线程中任意后续操作,也就i是不管怎么改变,单线程的执行结果不能改变。
  2. 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C
  3. volatile变量规则:对一个 volatile 变量的写操作 happens-before 于任意后续对这个 volatile 变量的读操作
  4. 监视器锁规则:对一个锁的解锁 happens-before 于后续对这个锁的加锁操作
  5. 线程启动规则:线程 A 启动线程 B,那么线程 A 启动 B 之前的操作 happens-before 于线程 B 中的任意操作
  6. 线程终止规则:线程 A 中的任何操作都 happens-before 于其他 检测到线程 A 已经终止的 线程
#面试#Java
上次更新: 2025/5/19 20:55:20
线程池
JVM组成

← 线程池 JVM组成→

最近更新
01
项目优化
05-06
02
项目优化
05-06
03
延迟消息
05-05
更多文章>
Theme by Vdoing | Copyright © 2025-2025 Cyan Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式