Java面试问题记录(三)

发布于:2025-09-14 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、JDK源码

Hashmap

1、HashMap的数据结构(1.7、1.8的区别)

在1.8以前,hashmap是链表+数组的结构,1.8之后是链表+数组/红黑树的结构。

2、HashMap的实现原理

当一个key存入hashmap时,先计算key的哈希值,这个值就是key在数组中的位置,如果数组中该位置已经有别的key了,那么会创建一个链表,将这个key放入链表中;1.8之后,如果链表的长度超过8,那么链表会自动转换为红黑树。

3、HashMap扩容为什么是2^n-1

1)利用位运算代替取模,提高运算效率

2)保证哈希值的低位参与运算,减少哈希冲突

3)简化扩容时索引重计算逻辑

总结一下就是说让哈希值分布更均匀,减少哈希冲突。

4、HashMap是线程安全的吗

不是线程安全的。

在多线程的场景下操作hashmap可能会导致以下三种情况

1)数据不一致:当多个线程同时操作hashmap时,会导致hashmap里的值不符合预期。

2)扩容时的死循环:在 JDK 1.7 及之前的版本中,HashMap 扩容过程中使用头插法转移元素,多线程并发扩容可能会导致链表形成环形结构,当后续操作该链表时就会陷入死循环,造成 CPU 占用率飙升。

3)get操作可能返回null:即使某个键值对已经被正确 put 到 HashMap 中,在多线程环境下,其他线程的 get 操作也可能无法获取到该值,返回 null。

5、HashMap、HashTable是什么关系?

简单地说,hashmap和hashtable应该是新版和老版的区别,现在hashtable已经很少用了,可以用ConcurrentHashMap来代替。

特性 HashMap Hashtable
线程安全性 非线程安全,多线程操作可能出现数据异常 线程安全,所有方法都用 synchronized 修饰
null 键 / 值支持 允许 null 作为键(仅允许一个 null 键)和多个 null 值 不允许 null 键或 null 值,否则抛出 NullPointerException
继承关系 继承自 AbstractMap 类 继承自古老的 Dictionary 类
扩容机制 初始容量 16,扩容时变为原来的 2 倍 初始容量 11,扩容时变为原来的 2 倍 + 1
迭代器类型 快速失败(fail-fast)迭代器 枚举器(Enumeration)是安全失败(fail-safe)的
性能 并发环境下性能更高(无同步开销) 并发性能差(全局锁导致多线程竞争激烈)

ThreadLocal

1、讲讲你对ThreadLocal的一些理解

含义:threadloacl是线程本地变量,如果创建了这个变量,在多线程的场景下,每个线程内部都会有一个该变量的副本,当线程需要操作这个变量时,它操作的是自己本地副本,从而起到线程隔离的作用。

实现原理:在Thread类中有一个ThreadLocalMap的变量,每个线程都有属于自己的ThreadloaclMap,在这个map中维护了一个数组,key就是Threadloacl对象,属于弱引用,value是Thread的泛型值,每个线程往Threadloacl中存值实际上都是往这个map中存,ThreadLoacl本身并不存取值,只是作为一个key从该map中存取值。

2、ThreadLocal有哪些应用场景

1)保存线程上下文信息:在多层调用时,例如controller -> service -> dao,需要传递一些参数值,比如用户身份,请求ID,日志信息等,但不希望通过方法参数显式传递

2)解决线程安全问题:对于非线程安全但是需要频繁使用的对象,如SimpleDateFormat,Random,可通过ThreadLoacl创建副本,避免多线程竞争导致的异常

3)框架中的应用:在spring中存储了HttpServletRequest,在controller和service层可直接获取,无需参数传递;在spring事务管理中,TransactionSynchronizationManager 通过 ThreadLocal 绑定当前线程的事务资源;在mybatis中,使用了ThreadLoacl存储SqlSession对象,保证同一请求中数据库操作的都是同一个SqlSession。

如果错误使用ThreadLoacl,有可能会导致内存溢出,因为在ThreadLoacl内部有一个map,map的key是弱引用,只要触发GC,该值就会被回收,而map的value是强引用,当key被回收了,而value还在,那么会一直占用内存从而导致OOM,所以在使用完ThreadLoacl后,要即使调用remove方法进行清除。

3、了解过FastThreadLocal吗

FastThreadLocal 是netty提供的一个高性能线程本地存储实现,专为解决JDK中的ThreadLoacl性能瓶颈而设计的。它在多线程高并发的场景下性能远超ThreadLoacl,是netty内部优化的重要组件。

ThreadLoacl FastThreadLocal
数据结构 每个线程通过 Thread.threadLocals 维护一个哈希表(ThreadLocalMap),存储 <ThreadLocal, 变量副本> 键值对。查找时需计算哈希值并处理哈希冲突,性能随 ThreadLocal 数量增加而下降。 每个线程(Netty 中的 FastThreadLocalThread)持有一个数组(InternalThreadLocalMap),变量副本直接通过索引(index)访问。索引在 FastThreadLocal 初始化时分配,查找时无需哈希计算,直接通过 index 定位,时间复杂度为 O (1)。

ArrayList、LinkedList

1、是否保证线程安全

非线程安全

2、底层数据结构

1)ArrayList是基于动态数组实现的,其核心是一个可动态扩容的Object数组。

2)LinedList是基于双向链表实现的,每个元素通过节点之间的引用串联在一起。

3、插入和删除是否受元素位置的影响

ArrayList插入和删除受元素位置的影响,而LinkedList不会。

操作类型 ArrayList(动态数组) LinkedList(双向链表)
尾部插入 / 删除 O (1)(高效) O (1)(高效)
中间 / 头部插入 / 删除 O (n)(需移动大量元素) O (n)(需遍历查找位置,但修改本身 O (1))
位置对效率的影响 影响极大(越靠前效率越低) 影响较小(主要成本在查找,而非修改)

4、是否支持快速随机访问

ArrayList支持,LinkedList不支持

5、内存空间占用

ArrayList底层是数组的结构,占用空间是连续的,而LinkedList底层是链表的结构,占用空间不连续。

场景 ArrayList LinkedList
内存开销 较低(无节点指针开销) 较高(每个元素多 2 个指针)
内存分配方式 连续空间,可能有预分配冗余 分散空间,无冗余但碎片化
元素数量较少时 可能因预分配导致少量浪费 节点开销占比更高
元素数量较多时 预分配冗余影响减小 总指针开销累积更明显

6、如何进行扩容的,默认初始化空间是多少

ArrayList初始容量是10,每次扩容1.5倍。

LinkedList底层是链表,没有初始容量,也不存在扩容的概念。

String StringBuffer StringBuilder

1、有什么区别

1)String不可变,StringBuffer、StringBuilder 可变

2)String和StringBuffer线程安全,StringBuilder线程不安全

3)String性能最差,StringBuilder性能最好,StringBuffer居间

2、是线程安全的吗

String和StringBuffer线程安全,StringBuilder线程不安全

jdk1.8的新特性

1、lambda表达式

2、Functional Interfaces

3、Optionals

4、Stream 流

5、Parallel-Streams 并行流

二、并发编程

volatile

1、volatile 的作用和使用场景

主要有两个作用:有序性和可见性

有序性是指操作被volatile修饰的变量,不会被重排序

可见性是指当一个共享变量被线程修改后,其他线程会立即感知到

可用于状态标记变量:多线程之间传递状态,例如停止信号,可确保线程能感知到变化

双重检查锁定实现单例模式:防止指令重排序导致的半初始化对象问题

2、volatile 如何保证指令重排

内存屏障

内存屏障类型 作用(限制的重排方向) 对应场景
LoadLoad 屏障 禁止后续 “读操作” 重排到当前 “读操作” 之前 读 volatile 变量前
StoreStore 屏障 禁止后续 “写操作” 重排到当前 “写操作” 之前 写 volatile 变量前
LoadStore 屏障 禁止后续 “写操作” 重排到当前 “读操作” 之前 读 volatile 变量后
StoreLoad 屏障 禁止后续 “读操作” 重排到当前 “写操作” 之前(最严格) 写 volatile 变量后

3、什么情况下会发生指令重排

1)不影响单线程语义:重排后,单线程的执行结果和重排前一样

2)存在可优化的执行间隙:指令之间无依赖关系,或者依赖关系可通过重排消除

synchronized

1、一般用在什么场景

用于线程同步的场景

1)保护对象级或类级的共享资源;

2)保证复合操作的原子性;

3)实现临界区或简单的线程间通信。

2、实现原理

synchronized修饰代码块时,jvm采用了monitorenter和monitorexit两个指令来实现同步,前者指向同步代码块的开始位置,后者指向结束位置
synchronized修饰同步方法时,jvm采用了一个标记符来实现同步,标明这个方法是一个同步方法

3、锁升级过程(无锁、偏向锁、轻量级锁、重量级锁)

1)无锁:当没有线程访问同步代码块,或者对象刚创建,此时处于无锁状态,对象头中的MarkWord锁状态标志位为01,偏向锁标志位为0。

2)偏向锁:当线程访问同步资源时,会先检查对象头中的标志位,如果是无锁状态,则会通过CAS操作修改偏向锁标志位为当前线程ID,后续该线程再次进入时,有两种情况,当偏向锁标志位为当前线程ID,则无需CAS操作,直接进入;当偏向锁标志位不是当前线程ID则会触发偏向锁撤销并升级轻量级锁。

3)轻量级锁:JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord复制到锁记录中,然后线程尝试使用CAS将对象头中的MarkWord替换为指向所记录的指针,如果成功,当前线程获得锁,如果失败,则通过自旋去获取锁,如果自旋超过阈值还没获取到锁,则升级到重量级锁。

4)重量级锁:当锁处于这个状态时,其他线程试图获取锁时都会被阻塞住,当持有锁的线程释放后会唤醒这些线程,被唤醒的线程就会进行新一轮的抢夺锁的过程。

4、这是JVM层面锁,还是JDK层面锁

JVM层面的。

5、这是一种悲观锁还是乐观锁

自适应的锁机制。

在竞争轻微时,以乐观锁的方式高效处理

在竞争激烈时,以悲观锁的方式避免CPU空转

lock

1、这是JVM层面锁,还是JDK层面锁

JDK层面,底层依赖AQS实现。

2、这是一种悲观锁还是乐观锁

悲观锁。

1)当线程需要访问共享资源时必须要先获得锁,如果锁已被获取,则该线程阻塞等待

2)获得锁后,线程可以安全的操作资源,其他线程无法同时访问

3)操作完成后,线程释放锁,其他线程竞争获取

3、是可重入锁吗

可以是也可以不是,Lock并不强制子类实现可重入的机制。

子类ReentrantLock 是典型的可重入锁:一个线程在持有锁的情况下,再次调用需要获取同一把锁的方法时,能够直接获取锁而不被阻塞。

ReentrantLock

1、与synchronized相比较有什么不同

对比维度 synchronized ReentrantLock
实现层面 JVM 层面锁:由 JVM 内置实现,通过字节码指令 monitorenter/monitorexit 控制锁的获取与释放,底层依赖对象头 Mark Word 和锁升级机制。 JDK 层面锁:基于 Java 代码实现(依赖 AQS 抽象队列同步器框架),属于 API 级别的锁,逻辑完全由 JDK 代码控制。
可移植性 依赖 JVM 实现,不同 JVM(如 HotSpot、J9)可能有细微差异,但主流 JVM 均完美支持。 纯 Java 代码实现,不依赖 JVM 底层细节,可移植性更强(理论上可在非 JVM 环境中模拟)。
锁的本质 隐式锁(自动加锁 / 解锁)。 显式锁(需手动调用 lock()/unlock() 控制生命周期)。

2、ReentrantLock 与 Lock 的关系

ReentrantLock是Lock的子类。

3、锁过程中是否可中断,与之对应的synchronized可中断吗

ReentrantLock锁过程中支持中断,synchronized不支持。

ReentrantLock提供了 lockInterruptibly() 方法,允许线程在等待锁的过程中响应中断(Thread.interrupt()),主动放弃等待并抛出 InterruptedException,避免线程永久阻塞。

synchronized在等待锁的过程中不支持中断。即使对等待锁的线程调用 interrupt(),线程也不会立即响应中断,而是会继续阻塞等待锁,直到获取到锁后才会处理中断状态(抛出 InterruptedException 或标记中断状态)。

CAS

1、Unsafe 类的作用

Unsafe是Java中一个特殊的底层类,提供了一系列低级别,不安全的操作,可直接绕过Java的内存模型和安全机制直接与操作系统或底层硬件交互,其核心作用是为了给JDK内部类库提供底层支持,不建议开发者在业务代码中直接使用。

2、CAS 的理解

主要由三个部分,变量的内存地址,要修改的新值,将被修改的预期值。

主要流程:首先从变量的内存地址中读取值与预期值比较,如果相等,则修改为新值;如果不相等,则不做任何操作。

CAS是一种高效的原子操作机制,通过比较和交换的指令实现了多线程下的安全数据修改,是乐观锁实现的核心。

3、什么是ABA问题

当一个线程通过CAS操作将一个变量从A修改为B,另一个线程又将该变量从B修改为A,那么第三个线程执行CAS操作时,无法发现该变量已经被修改过两次了。

针对上述问题,可通过添加版本号去解决,每次修改时递增版本号,CAS比较时同时比较版本号和预期值。

4、CAS的实现有什么

AtomicInteger,ReentrantLock等。

AQS

1、基本原理是什么

AQS主要是处理线程同步,是很多并发锁和同步工具类的实现基础。

在它的内部维护了一个由volatile修饰的变量state,用来维护线程状态,状态的意义由子类赋予,并且还维护了一个FIFO的双向链表,属性head和tail分别表示这个链表的头部和尾部,获取锁失败后,就会通过CAS操作放到链表的末尾。

2、实现类有哪些

ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier

3、实现了AQS的锁有哪些

自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物。

在这些锁里,虽然他们是基于AQS实现的,但是并没有直接继承AQS,而是在他们的内部定义了一个Sync类去继承AQS,之所以要这么做,是因为,锁面向的是用户,而同步器面向的是线程控制,那么在锁实现中聚合同步器而不是直接继承AQS就可以很好的去隔离两者之间所关注的事情。

多线程

1、线程池的种类

newCachedThreadPool(缓存线程池)

newFixedThreadPool(固定大小线程池)

newScheduledThreadPool(周期性线程池)

newSingleThreadExecutor(单线程线程池)

2、线程池的核心参数

核心线程数(corePoolSize),最大线程数(maximumPoolSize),任务队列(workQueue),拒绝策略(rejectedExecutionHandle)

3、线程的状态

新建,就绪,运行,等待,等待队列,阻塞,终止

三、JVM

1、GC 优化

1)监控:收集GC日志和堆快照

2)分析:如果是频繁GC,需要考虑是否对象创建过多,老年代空间不足,内存泄露等问题;如果是GC停顿时间过长,需要考虑是否是堆太大,垃圾回收器选择错误或者是CPU资源不足等问题

3)调优:JVM参数调优是治标,从代码层面调优是治本,尽量减少不必要的内存分配和回收压力。

4)验证:主要验证优化前后的对比

2、JVM 逃逸分析

JVM 的逃逸分析(Escape Analysis) 是一种编译器优化技术,用于分析对象的生命周期是否被限制在方法内部(未 “逃逸” 到方法外),从而对未逃逸的对象进行一系列优化,减少内存分配和垃圾回收的开销。

方法逃逸:对象的引用被作为方法返回值返回,或被存储到方法外部可访问的全局变量、静态变量中。

线程逃逸:对象的引用被传递到其他线程(如放入多线程共享的队列),可能被其他线程访问。

3、类的对象头都包括什么

1)Mark Word:动态存储对象运行时的状态信息,包含哈希码,GC分代年龄,锁状态等。

2)类型指针:该对象所属的类在方法区中的地址。

3)数组长度(仅在数组对象中):用于快速获取数组的长度。

4、new Object() 初始化都做了什么

1)类加载检查:先判断类是否被加载,如果没有,则调用类加载器加载字节码文件,并在方法区生成对应的类元数据,然后验证字节码的合法性,为静态变量分配内存并设置默认值,然后解析。执行类的静态代码块和对类的静态变量赋值。

2)内存分配

3)对象初始化:先初始化对象头,再设置实例变量默认值,最后执行构造方法

5、Java的内存模型以及GC算法

1)Java内存模型是一种抽象的模型,用来屏蔽各种硬件和操作系统的内存访问差异,java内存模型定义了线程和主内存之间的抽象关系,线程之间的共享变量存在主内存中,每一个线程都有一个私有的本地内存,本地内存存储了该线程以读写共享变量的副本

2)GC算法

判断对象是否存活的算法

引用计数器算法:给对象加一个引用计数器,当对象增加一个引用时计数器+1,引用失效时计数器-1,当计数为0的对象可被回收(存在循环引用的问题)。

可达性分析算法:通过定义GC Roots的对象作为起点进行搜索,将能够到达的对象视为可用,不可达的对象是为不可用。

垃圾收集算法

标记-清除算法:遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象;清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉,与此同时,清除那些被标记过的对象的标记,以便于下次回收。

标记-整理算法:标记的过程和上一个相同,整理的过程是移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。

复制算法:为了解决效率问题,复制收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完,需要进行GC时,就将存活的对象复制到另一块上面,然后将第一块内存全部清除。

分代收集算法:

根据对象存活周期的不同,将内存划分为几块。一般是把 Java 堆分为新生代和老年代,针对各个年代的特点采用最适当的收集算法。年轻代使用复制算法,老年代使用标记-整理或者标记-清除算法。

6、JVM内存区域

方法区:储存常量,静态变量,即时编译器编译后的代码缓存等数据
程序计数器:用于记录程序执行的位置
java虚拟机栈:线程私有,java方法执行的主要区域
本地方法栈:本地方法(native)执行的区域
堆:所有对象存储位置