一、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)执行的区域
堆:所有对象存储位置