前言
今天面试不咋顺利。
面试完后得知一个同事去了百度,我既替他高兴,但我自己也很难受。我面试了好多家都不咋顺利,他一说他base只有18,我就知道原来我要价太高了。
还是挺难受的,我觉得我并不比他差。但也只能接受,有时候面试就是看运气还有眼缘的。
复盘
算法题
算法题是最后问的,我挪到前面来。
1 | static class Flight { |
面试官就发了一道这种题目。
我就写成了直达做法,但实际上面试官是要考察包括中转的情况。有点尴尬。。我就说怎么这么简单。其实应该用BFS(广度优先)去做的。
也是请教了GPT.
1 | public static List<Flight> searchLowestPriceFlightCombination( |
实际上可以使用笛卡斯特拉算法(Dijkstra)来实现。其实就是改成优先级队列去排序。
1 | public static List<Flight> searchLowestPriceFlightCombination( |
看了一下,好像仅仅是队列的区别。
GPT的回答:
BFS 适用于无权图或所有边权重相等的图,简单高效。
Dijkstra 适用于加权图,特别是边权重不等的图,更加通用但稍微复杂。
项目介绍
问了下项目做啥的。
还问了代码规模。仔细想了想好像核心代码不过百行,哈哈哈。。。
项目里用了哪些java相关技术
spring redis rabbitmq…
Java异步框架?
之前了解过vert.x框架。jdk自带的有CompletableFuture,还有经常听到的Netty。
如何理解异步编程,阻塞非阻塞,同步非同步
异步编程是一种编程方式,它允许程序在执行某些操作(如I/O操作、网络请求或长时间运行的计算任务)时,不阻塞当前线程,从而可以同时处理其他任务。这种方式可以显著提高程序的性能和响应能力,尤其是在需要处理大量并发任务时
同步 vs 异步:
- 同步: 当一个任务开始时,必须等待其完成后才能继续执行后续任务。执行是按顺序进行的,任务之间存在依赖性。
- 异步: 当一个任务开始时,可以继续执行其他任务而不必等待该任务完成。当该任务完成时,会通过回调、消息或其他机制通知主程序。
阻塞 vs 非阻塞:
- 阻塞: 在等待某个操作(如I/O操作)完成之前,线程被暂停,无法执行其他操作。
- 非阻塞: 线程在等待操作完成时仍然可以继续执行其他操作。
异步编程的机制
- 回调(Callback): 传递一个函数作为参数,当异步操作完成时调用该函数。例如,JavaScript中的异步函数经常使用回调。
- Promise/Future: 表示一个将来可能会完成的操作,并允许你在操作完成时获取结果。Java的
CompletableFuture
和 JavaScript 的Promise
是典型例子。 - Async/Await: 提供一种更简洁和可读的方式来编写异步代码,通过将异步操作的结果等待并返回。JavaScript和C#中都有这种机制。
Java NIO
Java NIO(New Input/Output)是Java 1.4中引入的一组API,也叫做Non-blocking I/O。用于替代传统的Java I/O。它提供了一种更高效的I/O操作方式,特别适合处理大量并发连接和大数据量的读写操作。NIO主要通过非阻塞I/O和缓冲区来提高I/O操作的效率和性能。
主要概念和组件
- 缓冲区(Buffer): 一个线性数组,用于存储数据。NIO中的所有数据都是用缓冲区处理的。常见的缓冲区类型有
ByteBuffer
、CharBuffer
、IntBuffer
等。 - 通道(Channel): 一个比传统的
InputStream
和OutputStream
更高效的I/O抽象。通道可以异步地读写数据。常见的通道有FileChannel
、SocketChannel
、ServerSocketChannel
和DatagramChannel
。 - 选择器(Selector): 一个对象,可以检测一个或多个通道的状态(如是否准备好读、写等)。选择器使得单个线程可以管理多个通道,从而实现高效的非阻塞I/O操作。
- 选择键(SelectionKey): 选择器和通道之间的连接器。当通道准备就绪时,选择键会被选择器选择并返回。
工作原理
NIO通过以下几个核心组件协同工作来实现非阻塞I/O:
- 通道(Channel)和缓冲区(Buffer):
- 数据读写都是通过缓冲区进行的。通道读取数据到缓冲区,或者将缓冲区中的数据写入通道。
- 缓冲区有几个重要的属性:容量(capacity)、位置(position)和限制(limit),用于控制数据读写的范围和进度。
- 非阻塞模式:
- 通道可以配置为非阻塞模式,这意味着I/O操作(如读写)可以立即返回,而不会阻塞当前线程。如果操作不能立即完成,它会返回零或负数,而不是阻塞等待。
- 选择器(Selector):
- 选择器允许一个线程管理多个通道。通过注册通道到选择器并在通道准备好执行某些操作时被通知,选择器使得服务器可以有效地处理大量并发连接。
select 和 epoll
select
和 epoll
是两种不同的I/O多路复用机制,用于在一个线程中管理多个I/O操作。它们都用于监视一组文件描述符(如网络套接字)并等待其中的一个或多个准备好进行I/O操作。
select
是一个较早期的I/O多路复用机制,几乎在所有Unix-like系统上都可以使用,包括Linux和BSD。它通过一个固定大小的文件描述符集合来检测哪些描述符准备好进行I/O操作。
特点:
- 简单且广泛支持:几乎所有的Unix-like系统都支持
select
,因此具有很好的兼容性。 - 固定大小的描述符集合:
select
使用一个固定大小的数组来存储文件描述符,在Linux上通常限制为1024个文件描述符。 - 性能问题:当文件描述符数量很大时,
select
的性能会下降,因为它每次都需要扫描整个描述符集合。
epoll
是Linux特有的I/O多路复用机制,是select
和poll
的改进版本,旨在提高大规模并发连接的性能。epoll
使用事件驱动的机制,适用于处理大量的文件描述符。
特点:
- 高效性:
epoll
在处理大量文件描述符时效率更高,因为它只在文件描述符状态发生变化时才进行处理,而不是扫描整个描述符集合。 - 动态大小的描述符集合:
epoll
支持动态大小的文件描述符集合,没有固定的限制。 - 边缘触发和水平触发:
epoll
提供两种工作模式,边缘触发(edge-triggered, ET)和水平触发(level-triggered, LT),其中边缘触发模式适用于高性能场景。
适用场景
select
:适用于需要广泛兼容性的应用程序,或文件描述符数量较少的场景。epoll
:适用于Linux系统上需要处理大量并发连接的高性能服务器应用程序。
多线程例子
用线程池管理线程。
线程池的参数
核心线程数?QPS依据?
CPU密集型任务一般为核心数+1,IO密集型任务一般2N-4N,具体根据系统负载,QPS等依据灵活调整。
其他参数之前复盘都有写过。
线程安全的工具
synchronized,java.util.concurrent中的AQS。
其中AQS的核心原理之前也看过,这里再让GPT总结下:
AQS 的核心原理是基于一个 FIFO(先入先出)等待队列来管理线程的获取和释放锁的操作。它通过内置的 state
变量以及一些低层的 CAS 操作和锁条件变量实现高效的线程同步。
State 变量:
state
是一个int
类型的变量,表示共享资源的状态。其含义取决于具体的同步器实现。例如,在ReentrantLock
中,state
表示锁的持有计数;在CountDownLatch
中,state
表示倒计时计数。- 访问和修改
state
需要通过 AQS 提供的getState
、setState
和compareAndSetState
方法,这些方法保证了对state
的原子操作。
FIFO 队列:
- AQS 使用一个双向链表来实现等待队列,每个节点(Node)表示一个等待线程。节点中保存了线程的引用及其等待状态。
- 等待队列中的线程会被阻塞,当锁资源可用时,线程会被唤醒并重新竞争锁。
独占锁与共享锁:
- 独占模式(Exclusive Mode):一个线程独占资源。例如,
ReentrantLock
就是独占模式。 - 共享模式(Shared Mode):多个线程可以共享资源。例如,
Semaphore
和CountDownLatch
是共享模式。
ThreadLocal用过吗
讲了MDC的使用。
父子线程ThreadLocal
可以使用 InheritableThreadLocal
。但在线程池中,由于线程会被重用,InheritableThreadLocal
的值可能会被意外共享,导致不正确的行为。
可以使用阿里开源的transmittableThreadLocal
TransmittableThreadLocal 的特点和优势
- 线程池支持:
- 解决了
InheritableThreadLocal
在线程池中使用时可能出现的变量污染问题。TransmittableThreadLocal
可以在任务提交到线程池时传递上下文,并在任务执行结束后恢复原始上下文。
- 解决了
- 上下文一致性:
- 确保在线程池中使用时,上下文信息能够正确地传递到子线程,即使线程被复用也不会污染其他任务的上下文。
- 扩展性:
- 提供了
TtlRunnable
和TtlCallable
类,用于包装任务,确保上下文的正确传递。
- 提供了
ThreadLocal的底层实现
ThreadLocalMap:
- 每个线程中有一个
ThreadLocal.ThreadLocalMap
实例来存储线程局部变量。ThreadLocalMap
是一个专门为线程局部变量设计的内存映射表,主要通过线程的ThreadLocal
对象作为键,通过ThreadLocalMap
存储实际的值。
ThreadLocalMap 结构:
ThreadLocalMap
是一个数组,每个元素是一个
ThreadLocalMap.Entry
对象。Entry类中包含两个字段:
ThreadLocal
对象的引用(作为键)- 线程局部变量的值(作为值)
最新的JDK有什么特性吗
回答了虚拟线程。追问有什么优势。线程间切换会快一点。
G1特性
分代收集:
- G1 是基于分代收集的,即堆内存被划分为多个区域(Region)。这些区域可以属于年轻代(Young Generation)、老年代(Old Generation)或永久代(Metaspace)。G1 通过动态调整这些区域的大小和数量来优化内存回收。
区域化堆内存:
- 堆内存被划分成多个相等大小的区域(Region),这些区域用于不同的目的(年轻代、老年代、和空闲区域)。这种区域化使得 G1 可以根据需求进行灵活的内存管理。
增量式收集:
- G1 采用增量式的垃圾回收策略。它将堆分成多个区域,并按需回收这些区域,而不是一次性回收整个年轻代或老年代。这种方式减少了垃圾回收的暂停时间,降低了应用程序的停顿时间。
并行和并发收集:
- G1 支持多线程并行和并发回收。这意味着垃圾回收工作可以并行执行,从而减少了垃圾回收的总停顿时间。G1 的并发标记阶段(Concurrent Marking)减少了应用程序的停顿时间。
预测性停顿时间:
- G1 设计目标之一是提供预测性停顿时间。G1 可以通过配置参数(如
-XX:MaxGCPauseMillis
)来控制垃圾回收的最大停顿时间。这使得 G1 可以在一定范围内保证垃圾回收的停顿时间不会超出指定的阈值。
回收优先级:
- G1 使用了回收优先级策略,它会首先回收那些垃圾最多的区域,从而提高垃圾回收效率。G1 会评估每个区域的回收收益,以决定哪个区域最值得回收。
混合回收:
- G1 在进行年轻代垃圾回收时,可以同时回收老年代的部分区域。这种混合回收机制有助于减少老年代中的垃圾量,从而减轻后续的老年代回收负担。
全堆回收:
- 在需要全堆回收的情况下,G1 会执行一次全堆回收(Full GC)。G1 的全堆回收也会尽可能地减少停顿时间,并且会回收年轻代和老年代的所有垃圾。
自适应调整:
- G1 可以自适应调整堆的区域划分,以优化垃圾回收性能。它会根据堆的使用情况和垃圾回收的需求来动态调整区域的大小和数量。
要使用 G1 垃圾回收器,需要在 JVM 启动时指定 -XX:+UseG1GC
。此外,还有一些配置参数可以用来调整 G1 的行为:
-XX:MaxGCPauseMillis=<n>
: 目标最大垃圾回收停顿时间(毫秒)。-XX:G1HeapRegionSize=<size>
: 指定 G1 区域的大小(如 1m、2m、4m、8m)。-XX:ParallelGCThreads=<n>
: 设置并行垃圾回收线程的数量。-XX:ConcGCThreads=<n>
: 设置并发标记阶段的线程数量。-XX:G1ReservePercent=<n>
: 设置 G1 在堆中保留的区域百分比,用于防止频繁的 Full GC。
java的内存分区
1. 程序计数器 (Program Counter Register)
- 功能: 程序计数器(PC 寄存器)是一个指向当前执行线程的字节码指令的指针。每个线程都有一个独立的程序计数器。
- 用途: 在多线程环境下,用于跟踪线程的执行位置。它不参与垃圾回收。
2. 虚拟机栈 (Java Stack)
- 功能: 虚拟机栈用于管理线程的局部变量、操作数栈、动态链接、方法返回地址等。每个线程都有自己的虚拟机栈。
- 用途: 支持方法调用和返回。每个方法在调用时会创建一个栈帧(Stack Frame),存储方法的局部变量、操作数栈等信息。
- 大小: 可通过
-Xss
参数设置栈大小。
3. 本地方法栈 (Native Method Stack)
- 功能: 本地方法栈用于支持 native 方法的执行,它与虚拟机栈类似,但专门用于处理 native 方法的调用。
- 用途: 用于与本地方法(由 Java Native Interface, JNI 提供)进行交互。
- 大小: 可通过
-Xss
参数设置栈大小。
4. 堆内存 (Heap)
- 功能: 堆是 JVM 中最大的一块内存区域,用于存储所有的对象实例和数组。垃圾回收器主要在堆上进行垃圾回收。
- 分区:
- 年轻代 (Young Generation) 包含新创建的对象,分为三个部分:
- Eden 区: 新生对象首先被分配到 Eden 区。
- From Survivor 区: 对象经过一次或多次垃圾回收后,从 Eden 区晋升到 From Survivor 区。
- To Survivor 区: 另一个 Survivor 区,用于交换对象。
- 老年代 (Old Generation): 包含经过多次垃圾回收仍然存活的对象。长期存活的对象最终会被晋升到老年代。
- 永久代 (PermGen) / 元空间 (Metaspace)
- PermGen: Java 8 之前用于存储类元数据和常量池。
- Metaspace: 从 Java 8 开始取代 PermGen,用于存储类的元数据,动态生成的类和其他元数据。Metaspace 存储在本地内存中,而不是堆中。
- 年轻代 (Young Generation) 包含新创建的对象,分为三个部分:
- 用途: 支持对象的动态分配和垃圾回收。
- 大小: 可以通过
-Xmx
和-Xms
参数设置堆的最大值和初始值,-XX:MaxPermSize
用于设置 PermGen 大小(Java 7 及之前)。
5. 运行时常量池 (Runtime Constant Pool)
- 功能: 运行时常量池是方法区的一部分,用于存储类、字段、方法、字符串等常量。
- 用途: 支持类的常量值和字符串常量。
- 大小: 由 JVM 管理,通常不需要手动设置。
6. 直接内存 (Direct Memory)
- 功能: 直接内存是 JVM 之外的内存区域,用于存储与 I/O 操作相关的数据,如 NIO 的缓冲区。
- 用途: 提供与操作系统的直接交互,减少内存复制,提高性能。
- 大小: 可以通过
-XX:MaxDirectMemorySize
参数设置最大直接内存大小。
堆外内存
堆外内存(Off-Heap Memory)指的是不属于 Java 堆内存的一块内存区域。与堆内存不同,堆外内存是由应用程序直接管理的,并且不受到 Java 垃圾回收器的管理。堆外内存通常用于存储需要大量内存或需要频繁访问的高性能数据,例如大缓存、直接内存缓冲区等。
Java NIO 提供了 ByteBuffer
类,可以通过 ByteBuffer.allocateDirect()
方法分配直接内存。这种方法分配的内存不受 Java 堆管理,并且可以直接用于 I/O 操作。
字节码技术
JVMTI?JVMTI 全程 JVM Tool Interface,它是Java虚拟机定义的一个开发和监控JVM使用的程序接口(programing interface),通过该接口可以探查JVM内部的一些运行状态,甚至控制JVM应用程序的执行。
修改字节码可以使用ASM库或 Javassist,还有动态代理中使用cglib也是一种字节码技术,但底层依旧使用的ASM库。
平常会写单侧(UT)吗
junit,Mockito
SPI机制
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
- 需要遍历加载所有的实现类,不能做到按需加载,这样效率还是相对较低的。
- 当多个
ServiceLoader
同时load
时,会有并发问题。
Redis用过哪些数据结构
1.字符串 (String)
- 描述: 最基本的数据类型,可以存储任意形式的数据,例如文本、数字、二进制数据等。
- 操作
SET key value
: 设置键的值。GET key
: 获取键的值。INCR key
: 对键的值进行递增操作。APPEND key value
: 在现有值的末尾追加字符串。
2. 哈希 (Hash)
- 描述: 存储键值对的集合,适合存储对象的属性数据。
- 操作
HSET key field value
: 设置哈希表中的字段值。HGET key field
: 获取哈希表中指定字段的值。HGETALL key
: 获取哈希表中所有字段及其值。HDEL key field
: 删除哈希表中的指定字段。
3. 列表 (List)
- 描述: 有序的字符串集合,可以在头部和尾部添加元素,适合用作队列或栈。
- 操作
LPUSH key value
: 将值插入到列表的左侧。RPUSH key value
: 将值插入到列表的右侧。LPOP key
: 移除并获取列表的左侧第一个元素。RPOP key
: 移除并获取列表的右侧第一个元素。LRANGE key start stop
: 获取列表中指定范围的元素。
4. 集合 (Set)
- 描述: 无序的字符串集合,集合中的元素是唯一的。
- 操作
SADD key member
: 向集合添加元素。SREM key member
: 从集合中移除元素。SMEMBERS key
: 获取集合中的所有元素。SISMEMBER key member
: 检查元素是否存在于集合中。
5. 有序集合 (Sorted Set)
- 描述: 类似于集合,但每个元素都关联一个浮点数的分数。元素按分数排序,并且元素是唯一的。
- 操作
ZADD key score member
: 向有序集合添加元素和分数。ZRANGE key start stop [WITHSCORES]
: 获取有序集合中指定范围的元素。ZREM key member
: 从有序集合中移除元素。ZSCORE key member
: 获取元素的分数。
6. 位图 (Bitmap)
- 描述: 用于处理大量的二进制位,通常用于统计、标记等。
- 操作
SETBIT key offset value
: 设置位图中指定偏移量的位。GETBIT key offset
: 获取位图中指定偏移量的位。BITCOUNT key [start end]
: 计算位图中设置为1的位的数量。
7. 超日志 (HyperLogLog)
- 描述: 用于估算唯一元素的数量,具有固定的内存占用,不受数据量大小的影响。
- 操作
PFADD key element [element ...]
: 将元素添加到 HyperLogLog 中。PFCOUNT key [key ...]
: 获取 HyperLogLog 的基数估算值。
8. 地理位置 (Geospatial)
- 描述: 用于存储地理位置数据和进行地理空间操作。
- 操作
GEOADD key longitude latitude member
: 添加地理位置数据。GEOPOS key member [member ...]
: 获取地理位置的坐标。GEORADIUS key longitude latitude radius unit [WITHDIST|WITHCOORD|WITHHASH]
: 查询地理位置数据。
9. 流 (Stream)
- 描述: 用于处理日志数据和消息队列,支持高效的消息流处理。
- 操作
XADD key id field value [field value ...]
: 向流中添加一条记录。XREAD [BLOCK milliseconds] [COUNT count] STREAMS key [key ...]
: 从流中读取数据。XDEL key id [id ...]
: 从流中删除一条记录。
10. 事务 (Transaction)
- 描述: 允许将多个 Redis 命令打包成一个事务并一起执行,保证事务的原子性。
- 操作
MULTI
: 开始事务。EXEC
: 执行事务中的所有命令。DISCARD
: 放弃事务,清除事务中的所有命令。
11. 发布/订阅 (Pub/Sub)
- 描述: 实现消息的发布和订阅,允许消息在不同客户端之间传递。
- 操作
PUBLISH channel message
: 向频道发布消息。SUBSCRIBE channel [channel ...]
: 订阅频道,接收消息。UNSUBSCRIBE [channel [channel ...]]
: 取消订阅频道。
sorted set底层的数据结构
我回答了最大堆最小堆,笑死。
实际上是调表和哈希表。
跳表 (Skip List)
- 描述: 跳表是一种基于概率的数据结构,它是一种带有多级索引的链表,用于在有序序列中快速查找、插入和删除元素。跳表可以视为一种分层的链表,每一层都是一个有序的链表。
- 作用: 在 Redis 的有序集合中,跳表用于存储集合中的元素及其分数,并支持快速的排序操作。
- 优势
- 平均时间复杂度: 跳表的查找、插入和删除操作平均时间复杂度为 O(log N),其中 N 是跳表中的元素数量。
- 空间复杂度: 跳表的空间复杂度为 O(N),在存储多层索引的情况下略高于线性链表。
反问:岗位职责,技术团队架构
人家做BFF的,其实就是聚合层。负责前后端的对接,将后端多个服务的数据整合返回给前端。
后话
复盘完,今天是7月31号,是我离职的第57天,心中苦闷的心情又增加了,很难受。一个是对未来的迷茫,另一个是对自己找工作的苦恼。有人问我后悔当初离职吗,我想我既不后悔也有点后悔,一是每当想到原来的工作我就难以接受,后悔也是源自于自己找工作一个月(实际上应该是3周左右)没有收获的后悔。但是综合来说我并没有后悔当初选择离职,就是头铁哈哈哈哈。
26年人生至今令我感到后悔的事情其实屈指可数,我一直觉得既然做出了选择那么就为自己的选择买单,这是一个成年人应该有的责任,而不是事后去后悔。
与之相对的,是我在6月11号写的离职感悟,那时候我写到自己的压力还可以,但是我并不知道后面会发生什么,也没经历过离职,也不知道原来重新找工作真的很难很累,更不知道直接离职再找工作是多么艰辛,不过有一说一,让我在职去寻找工作那岂不是更难。
现在压力逐渐上来了,虽然我并不是特别缺钱,但是一是长时间没工作不太习惯(但是挺爽,也会写点代码),二是很多HR也都会提前问是不是离职了,离职原因是啥,三是听前同事说某某公司又降薪了,其实也是侧面反馈环境不太好,岗位越来越少。
心态已经有点不正了,我也总怀疑从上班的时候我就已经抑郁了,如今很可能加重了。但是人生不应该只有上班,前领导也劝我要不去考个研,实在不行明年可以再回来,我没回复他,我知道我不能像某个同事那样,出去了然后再二进宫,我拉不下脸。即便很难我也会默默撑下去。
机会总会有的,我相信。