基础面试题篇1

文章介绍了Java的基础概念,包括JVM、线程、HashMap的底层实现、Integer缓存优化、BigDecimal的特性、阻塞队列和线程池的工作原理,以及CountDownLatch和CyclicBarrier的同步作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

今天很晚了,就用手机编辑吧,持之以恒。前文介绍了jvm和线程,本文将介绍一些java基础相关。

基础面试题篇1

  1. jdk8中HashMap的底层实现

基础结构:使用数组和链表的结构。每个数组元素(bucket)都包含一个链表,用于存储具有相同哈希值的键值对。
红黑树:当链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,以提高查找效率。红黑树是一种自平衡的二叉查找树,可以在对数时间内完成查找操作。
哈希函数:使用了一种基于除法和加权的哈希函数,以提高哈希分布的均匀性,减少哈希冲突。
动态调整:支持动态调整大小。当哈希表的使用率超过一定阈值(默认为0.75)时,会触发扩容操作。扩容时,会将数组大小翻倍。
加载因子:使用了一个默认的加载因子(0.75),它决定了哈希表的空间使用情况。加载因子越高,空间利用率越高,但查找性能可能会降低。

扩容使用2的N次方
1哈希计算与取模运算优化:当 capacity 是 2 的 N 次方时,取模运算可以转换为位运算(hash & (capacity - 1)),这是因为 2 的 N 次方减 1 的二进制表示是一串连续的 1,对于任意的 hash 值,hash & 7 相当于取 hash 的低 3 位,其效果等同于 hash % 8。
2扩容时元素迁移的便利性:这可以通过判断元素哈希值对应二进制位的某一位是 0 还是 1 来确定,避免了重新计算哈希和取模的过程,提高了扩容的效率。假设原容量 capacity = 8,扩容后 newCapacity = 16。对于某个元素的哈希值 hash,hash & 7 确定其在原数组中的位置,hash & 15 确定其在新数组中的位置。hash & 7 和 hash & 15 的区别在于多了一位二进制位的判断,只需要根据这一位是 0 还是 1 就能确定元素在新数组中的位置是不变还是需要移动到原位置加上旧容量的位置。
3均匀分布元素:使用 2 的 N 次方作为容量有助于元素在数组中更均匀地分布。哈希函数的目的是将不同的键均匀地映射到数组的各个位置上,减少哈希冲突。
2. Integer的底层缓存

Integer 类的底层缓存是指 IntegerCache。它是为了提供一种优化机制,避免频繁地创建和销毁 Integer 对象而设计的。
IntegerCache 是一个范围在 -128 到 127 之间的缓存池,用于存储 Integer 对象。当我们在代码中直接使用数字字面量时,例如 Integer i = 127;,Java 会首先检查 IntegerCache 中是否有可用的对象,如果有,则直接返回该对象,而不是创建一个新的对象。这样可以减少对象创建和垃圾回收的开销,提高性能。
当我们在代码中使用的数字字面量超出了 IntegerCache 的范围时,Java 会创建一个新的 Integer 对象。例如,使用大于 127 或小于 -128 的数字字面量时,会创建新的 Integer 对象。
需要注意的是,IntegerCache 只适用于直接使用数字字面量的场景。如果使用其他方式创建 Integer 对象,例如通过调用 Integer 类的静态方法 valueOf() 或 new Integer(),则不会利用 IntegerCache 的优化机制。
说明:缓存范围可通过jvm参数调整。

  1. 重量级对象BigDecimal

BigDecimal 在 Java 中被设计为重量级对象(heavyweight object),这是因为 BigDecimal 对象包含了一个数值和一个 scale(小数点后的位数)。为了支持这些额外的信息,BigDecimal 对象需要更多的内存和计算资源,因此它们的创建和操作通常比轻量级对象(如基本数据类型或包装类)更耗费资源。
另外,BigDecimal 的设计初衷是为了进行高精度的浮点数运算,特别是涉及到金融和货币计算的场景。在这些场景中,精度和稳定性是非常重要的,因此 BigDecimal 提供了更高精度的计算能力,同时也需要更多的资源来支持这种能力。
为了减少创建 BigDecimal 对象的开销,可以考虑重用已经创建的对象,而不是频繁地创建新的对象。例如,可以将常用的 BigDecimal 值存储在变量中,并在需要时重复使用这些变量,而不是每次都创建新的 BigDecimal 对象。此外,还可以使用 BigDecimal 的子类,如 BigInteger 和 BigDecimal 的某些静态方法(如 valueOf())来创建对象,这些方法可能会使用缓存机制来减少对象创建的开销。

  1. 介绍一下阻塞队列

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空;当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
以下是几种实现:
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

  1. 介绍一下线程池的提交和执行流程

创建线程池对象:首先需要创建一个线程池对象,可以通过调用 ThreadPoolExecutor 类或其他线程池实现类的构造方法来完成。线程池的创建可以根据需要进行参数配置,例如核心线程数、最大线程数、任务队列类型等。
提交任务:使用线程池对象的 submit() 或 execute() 方法来提交任务。这些方法接受一个 Runnable 或 Callable 对象作为参数,这些对象代表要执行的任务。当任务被提交后,线程池会将其放入任务队列中等待执行。
任务排队与调度:线程池会根据配置的策略将新提交的任务放入任务队列中。当线程池中的线程数量小于核心线程数时,线程池会创建新的线程来执行任务;当线程数达到核心线程数后,新任务会被放入任务队列中等待执行。如果任务队列已满,线程池会根据配置的最大线程数创建新的线程来处理任务。
任务执行:当线程池中的某个线程从任务队列中取出任务后,会开始执行该任务。任务的执行方式是异步的,不会阻塞提交任务的线程。任务的执行逻辑由 Runnable 或 Callable 对象的 run() 或 call() 方法定义。
任务完成与返回结果:当任务执行完成后,线程池会根据需要返回结果给提交任务的线程。如果提交的是 Callable 对象,可以使用 Future 对象来获取任务的返回值。如果提交的是 Runnable 对象,则无法获取任务的返回值。
需要注意的是,如果提交的任务是计时器任务或者定时任务,那么它们通常会使用 ScheduledThreadPoolExecutor 类来处理,而不是普通的 ThreadPoolExecutor 类。此外,在使用线程池时需要注意合理配置线程池的参数,避免资源浪费或性能瓶颈。
Spring中还有ThreadPoolTaskExecutor。

  1. 介绍一下CountDownLatch和CyclicBarrier

CountDownLatch 是一个同步辅助类,允许一个或多个线程等待其他线程完成操作。它有一个计数器,初始值为一个正整数,每次调用 countDown() 方法计数器减一,当计数器达到零时,所有等待的线程被唤醒并继续执行。CountDownLatch 常用于多线程并发执行的场景,其中一些线程需要等待其他线程完成某个阶段的工作后才能继续执行。
CyclicBarrier 是一个同步工具类,允许一组线程互相等待,直到所有线程都到达某个屏障点(barrier point)再一同继续执行。每个 CyclicBarrier 实例都有一个屏障(barrier),当一个线程到达屏障点时,它会等待其他线程也到达屏障点,然后所有线程一同继续执行。CyclicBarrier 通常用于多线程并发测试、多线程数据同步等场景。
简单来说,CountDownLatch 用于让一组线程等待某个条件成立后再继续执行,而 CyclicBarrier 用于让一组线程在某个点上同步执行。

  1. LongAddr
    LongAdder 是 Java 并发包 java.util.concurrent.atomic 下的一个类,在 Java 8 中引入,用于在多线程环境下高效地进行数值累加操作,它是 AtomicLong 的增强版。

使用场景

在多线程环境下,如果需要对一个数值进行频繁的累加操作,AtomicLong 可以保证操作的原子性,但在高并发场景下,多个线程同时竞争同一个原子变量的更新权,会导致大量的线程自旋重试,从而影响性能。LongAdder 则更适合这种高并发的累加场景,它通过分散热点,减少线程竞争,提高了并发性能。
原理
LongAdder 的核心思想是将一个值分散到多个变量中,每个变量被称为一个 Cell。当多个线程并发进行累加操作时,不同的线程可以更新不同的 Cell,从而减少了线程之间的竞争。最终获取累加结果时,将所有 Cell 中的值以及基础值(base)相加即可。
具体来说,LongAdder 内部维护了一个 base 变量和一个 Cell 数组。当线程竞争不激烈时,直接对 base 变量进行累加操作;当线程竞争激烈时,会将累加操作分散到 Cell 数组中的不同 Cell 上。
8. java8 中的ConcurrentHashMap
ConcurrentHashMap 是 Java 集合框架中的一个线程安全的哈希表实现,在 Java 8 中进行了重大的改进和优化。
数据结构
Java 8 中的 ConcurrentHashMap 采用了数组 + 链表 + 红黑树的结构。具体来说:

  • 数组:作为整个数据结构的基础,用于存储哈希桶。每个哈希桶可以存储一个链表或红黑树的头节点。
  • 链表:当多个键的哈希值相同时,会将这些键值对以链表的形式存储在同一个哈希桶中。在 Java 8 中,链表采用尾插法插入新节点。
  • 红黑树:当链表的长度超过一定阈值(默认为 8),并且数组长度达到 64 时,链表会转换为红黑树。红黑树是一种自平衡的二叉搜索树,在查找、插入和删除操作上具有更好的时间复杂度,可以提高性能。

线程安全机制
Java 8 的 ConcurrentHashMap 使用 CAS(Compare - And - Swap)和 synchronized 关键字来保证线程安全:

  • CAS:在进行数组初始化、扩容等操作时,使用 CAS 操作来保证操作的原子性。例如,在初始化数组时,通过 CAS 操作来确保只有一个线程可以成功初始化数组。
  • synchronized:在对链表或红黑树进行插入、删除等操作时,使用 synchronized 关键字对哈希桶的头节点进行加锁。这样可以将锁的粒度细化到每个哈希桶,减少线程之间的竞争,提高并发性能。

性能提升
相较于 Java 7 及以前的版本,Java 8 的 ConcurrentHashMap 在性能上有了显著提升:

  • 锁粒度更小:Java 7 采用分段锁机制,将整个哈希表分成多个段,每个段都有一个独立的锁。而 Java 8 采用了更细粒度的锁,直接对哈希桶的头节点加锁,减少了锁的竞争范围。
  • 红黑树优化:当链表长度过长时,将链表转换为红黑树,将查找、插入和删除操作的时间复杂度从 O ( n ) O(n) O(n) 降低到 O ( l o g n ) O(log n) O(logn),提高了性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值