最小路径和
给定一个包含非负整数的 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
二维dp:表示从0.0 到i.j 的最小路径和
class Solution {
public int minPathSum(int\[\]\[\] grid) {
int m=grid.length;
int n=grid0.length;
int\[\]\[\] dp=new intmn;
dp00=grid00;
for(int i=0;i<m;i++)
for(int j=0;j<n;j++){
if(i==0&&j-1>=0)dpij=dpij-1+gridij;
else if(j==0&&i-1>=0)dpij=dpi-1j+gridij;
else
if(i-1>=0&&j-1>=0)dpij=Math.min(dpij-1,dpi-1j)+gridij;
}
return dpm-1n-1;
}
}
编辑距离
给两个单词 word1 和 word2, 返回将 word1 转换成 word2 所使用的最少操作数
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
**输入:**word1 = "horse", word2 = "ros"
**输出:**3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
思路:二维dp
状态定义
设 dp[i][j] = 将 word1 的前 i 个字符 (即 word1[0..i-1])转换成 word2 的前 j 个字符 (word2[0..j-1])所需的最少操作数。
-
答案:
dp[m][n],其中m = word1.length(),n = word2.length() -
空串也纳入状态:
dp[0][j]表示把空串变成word2前 j 个字符
状态转移方程(核心)
考虑 word1[i-1] 和 word2[j-1] 这两个字符:
情况 1:字符相同 word1[i-1] == word2[j-1]
无需操作,直接继承:
dpij = dpi-1j-1
情况 2:字符不同
三种操作对应三种「来源状态」,取最小:
| 操作 | 含义 | 来源 | 转移 |
|---|---|---|---|
| 删除 | 删掉 word1[i-1] |
dp[i-1][j] |
已经匹配好 word1 前 i-1 和 word2 前 j |
| 插入 | 在 word1 插入一个字符(等价于删 word2[j-1]) |
dp[i][j-1] |
|
| 替换 | 把 word1[i-1] 换成 word2[j-1] |
dp[i-1][j-1] |
text
dpij = 1 + min(dpi-1j, // 删除
dpij-1, // 插入
dpi-1j-1) // 替换
dpij = (word1i-1 == word2j-1)
? dpi-1j-1
: 1 + min(dpi-1j-1, dpi-1j, dpij-1)
class Solution {
public int minDistance(String word1, String word2) {
int m=word1.length();
int n=word2.length();
int\[\]\[\] dp=new intm+1n+1;
for(int i=0;i<=m;i++)dpi0=i;
for(int i=0;i<=n;i++)dp0i=i;
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dpij=dpi-1j-1;
}
else
dpij=Math.min(dpi-1j-1,Math.min(dpi-1j,dpij-1))+1;
}
return dpmn;
}
}
1.介绍一个做过的 Java 后端项目
项目中的第二个领趣生活就是一个传统的java后端项目,该项目是一个本地生活服务平台,支持商户的查询,评论等功能,我最主要参与的是里面的一个优惠券秒杀模块,主要从缓存优化,避免超卖和确保一人一单,接口限流等维度进行开发,比如,为了避免超卖和确保一人一单,我从最开始的直接在数据库进行判断,转为引入Redis+Lua脚本进行判断,降低了数据库的访问压力,同时引入MQ去异步的落库,将判断和下单操作进行解耦,在Redis中存储优惠券的相关信息,比如库存,以优惠券业务key作为前缀,如果当前库存数大于0,则扣减库存,等于0则显示库存不足,同时利用Set数据结构存储下过订单的用户信息,利用其天然去重的特性,如果没下过单,就加入,下过单,则显示只能一个用户只能下一单,同时为了保证判断超卖和一人一单操作的原子性,引入Lua脚本将这些操作打包,并利用Redis核心线程的单线程特性,避免了高并发下分布式锁的开销
2.系统的核心架构是怎样的
系统基于传统的三层架构开发,包括controller,service,mapper
3.高并发下怎么保性能和稳定性
1.在核心接口秒杀优惠券入口引入限流注解,限制入口流量
2.搭建多级缓存架构,降低数据库访问压力
3.利用Redis+lua脚本避免分布式锁的开销
4.数据库在高压力下是否出现过瓶颈?怎么优化的?
我举一个场景,在高并发下,为了避免超卖和确保一人一单,我从最开始的直接在数据库进行判断,转为引入Redis+Lua脚本进行判断,降低了数据库的访问压力,同时引入MQ去异步的落库,将判断和下单操作进行解耦,在Redis中存储优惠券的相关信息,比如库存,以优惠券业务key作为前缀,如果当前库存数大于0,则扣减库存,等于0则显示库存不足,同时利用Set数据结构存储下过订单的用户ID,利用其天然去重的特性,如果没下过单,就加入,下过单,则显示只能一个用户只能下一单,同时为了保证判断超卖和一人一单操作的原子性,引入Lua脚本将这些操作打包,并利用Redis核心线程的单线程特性,避免了高并发下分布式锁的开销
后续利用jemeter进行了简单的压测,发现在保证不超卖和一人一单的情况下,QPS确实有较大幅提升,证明方案取得了效果
5.在项目中具体负责了哪些模块?有没有遇到特别难的问题?
1.滑动窗口限流注解开发
2.引入MQ异步扣减库存
3.优化库存超卖和一人一单逻辑判断
问题:
1.缓存击穿两套方案的取舍
queryWithLogicalExpire 逻辑过期 返回旧值 + 异步重建,可用性最高但数据弱一致,重建期间所有线程拿到的都是过期数据;queryWithMutex互斥锁 强一致,代价是需要阻塞等待,可用性较弱
综合考虑,在当前业务场景下,本地生活服务平台对于热点数据的访问更加注重可用性,而不是像金融交易场景下的数据强一致性,所以综合考虑,当前项目采用了逻辑过期方案解决缓存击穿问题
这让我明白方案的设计是跟随业务场景的变化而变化的,不同的方案适用于不同的场景,我们需要灵活的分析并应用
2.优化超卖和一人一单逻辑
在高并发下,为了避免超卖和确保一人一单,我从最开始的直接在数据库进行判断,转为引入Redis+Lua脚本进行判断,降低了数据库的访问压力,同时引入MQ去异步的落库,将判断和下单操作进行解耦,在Redis中存储优惠券的相关信息,比如库存,以优惠券业务key作为前缀,如果当前库存数大于0,则扣减库存,等于0则显示库存不足,同时利用Set数据结构存储下过订单的用户ID,利用其天然去重的特性,如果没下过单,就加入,下过单,则显示只能一个用户只能下一单,同时为了保证判断超卖和一人一单操作的原子性,引入Lua脚本将这些操作打包,并利用Redis核心线程的单线程特性,避免了高并发下分布式锁的开销
后续利用jemeter进行了简单的压测,发现在保证不超卖和一人一单的情况下,QPS确实有较大幅提升,证明方案取得了效果
合并2个有序列表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:建立一个哨兵节点,利用while循环,只有列表均不指向空,则每一次循环里判断2两个列表当前指向的值,如果表1小于或等于表2,则连接在哨兵节点后面,表1指针后移,哨兵指针也后移,反之,接入表2
如果表1表2两者长度不一致,在遍历完后,直接接入更长的后面的节点即可
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy=new ListNode(-1);
ListNode cur=dummy;
while(list1!=null&&list2!=null){
if(list1.val<=list2.val){
cur.next=list1;
list1=list1.next;
}
else{
cur.next=list2;
list2=list2.next;
}
cur=cur.next;
}
cur.next=(list1!=null)?list1:list2;
return dummy.next;
}
}
删除链表的倒数第N个节点
思路
1.直接遍历得到长度,在遍历长度-n;
2.利用快慢指针和哨兵节点
哨兵节点指向head,初始时fast和slow指向dummy,注意不是指向head
让fast先走n+1步,然后同步走fast和slow,当fast==null的时候就停止,此时slow指针指向要删除节点的前驱节点,利用slow.next=slow.next.next直接删除,然后返回dummy.next即可
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy=new ListNode(-1);
dummy.next=head;
ListNode fast=dummy;
ListNode slow=dummy;
for(int i=0;i<=n;i++){
fast=fast.next;
}
while(fast!=null){
fast=fast.next;
slow=slow.next;
}
slow.next=slow.next.next;
return dummy.next;
}
}
1.-
Java 内存模型 JMM线程之间的可见性、有序性、原子性是怎么保证的?
volatile 和 synchronized 的区别?
底层实现原理?
JMM:围绕并发编程的三大特性 来设计的:原子性、可见性、有序性,定义了一套规则,规范了多线程如何通过主内存和工作内存来交互
-
原子性:一个或多个操作要么全部执行,且不被中断,要么全部不执行。
- 保证方式 :
synchronized、Lock、java.util.concurrent包下的原子类。单纯的volatile不保证原子性 (比如i++)。
- 保证方式 :
-
可见性:一个线程修改了共享变量,其他线程能立刻看到。
- 保证方式 :
volatile(通过内存屏障强制刷新主内存并失效其他线程工作内存)。
- 保证方式 :
-
有序性:程序执行的顺序按照代码的先后顺序执行。
- 保证方式 :
volatile(禁止指令重排序)、synchronized(保证串行执行)。
- 保证方式 :
| volatile | synchronized | |
|---|---|---|
| 作用范围 | 变量 | 方法、代码块 |
| 原子性 | ❌ 不保证(i++ 会出问题) |
✅ 保证 |
| 可见性 | ✅ 保证 | ✅ 保证 |
| 有序性 | ✅ 禁止指令重排 | ✅ 保证 |
| 阻塞 | 不会阻塞 | 会阻塞 |
| 性能 | 轻量级,性能高 | 重量级,性能相对低 |
底层实现原理
-
volatile 的底层:
-
在汇编层面,写操作会加上
lock前缀指令。 -
lock指令会做两件事:① 将当前处理的数据写回主内存;② 这个写回操作会使其他 CPU 里缓存了该内存地址的数据失效(MESI 缓存一致性协议)。 -
同时,JMM 会在 volatile 读写前后插入内存屏障(LoadLoad、StoreStore 等),禁止指令重排序。
-
-
synchronized 的底层:
-
修饰代码块:通过
monitorenter和monitorexit指令实现。 -
修饰方法:通过方法访问标志
ACC_SYNCHRONIZED实现。 -
底层依赖 Monitor(管程) 对象,锁升级过程:无锁 → 偏向锁 → 轻量级锁(CAS 自旋) → 重量级锁(操作系统互斥量)。
-
-
集合框架HashMap 的底层实现,JDK 8 中为什么引入红黑树?
HashMap 底层实现
-
JDK 7:数组 + 链表。
-
JDK 8:数组 + 链表 + 红黑树。
-
为什么引入红黑树?
-
当哈希冲突严重时,链表会变得很长,查询效率退化为 O(n)。
-
红黑树将查询复杂度优化到 O(log n)。
-
触发条件:链表长度 ≥ 8 且数组长度 ≥ 64 时,链表转为红黑树。
-
-
ConcurrentHashMap 的分段锁机制和 JDK 1.8 之后的 CAS+红黑树实现。
-
JDK 7(分段锁):
相当于把整个 Map 分成多个小 Map,不同 Segment 之间可以并发读写,锁粒度是 Segment。
-
JDK 8(CAS + synchronized + 红黑树):
-
摒弃分段锁,改用 CAS + synchronized 锁定单个桶(数组的某个索引位置)。
-
结构:
Node数组 + 链表 + 红黑树。 -
当桶为空时,用 CAS 无锁插入;当桶不为空时,用 synchronized 锁住该桶的头节点,保证线程安全。这样锁的粒度更细,并发度更高。
-
-
线程池参数的含义?
-
如何选择合适的线程池?
-
submit() 和 execute() 的区别?
-
线程池满了之后会发生什么?如何避免?
线程池参数含义
-
corePoolSize:核心线程数,即使空闲也不会被回收
-
maximumPoolSize:最大线程数。
-
keepAliveTime:非核心线程空闲存活时间。
-
unit:时间单位。
-
workQueue :任务队列,常用
ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。 -
threadFactory:线程工厂,自定义线程名、优先级等。
-
handler:拒绝策略。
如何选择合适的线程池?
-
CPU 密集型:线程数 = CPU 核数 + 1。
-
IO 密集型:线程数 = CPU 核数 × 2 或 (1 + 平均等待时间/平均计算时间)。
-
实际生产中,建议根据压测结果动态调整,而不是死记公式。
submit() 和 execute() 的区别
-
execute():只能提交Runnable,无返回值,无法捕获异常(异常会直接抛到线程池的UncaughtExceptionHandler)。 -
submit():可以提交Runnable或Callable,返回Future,可以通过Future.get()获取结果或捕获异常。
线程池满了之后会发生什么?如何避免?
-
发生过程:
-
核心线程满 → 任务入队。
-
队列满 → 创建非核心线程。
-
线程数达到
maximumPoolSize且队列也满 → 触发拒绝策略。
-
-
四种拒绝策略:
-
AbortPolicy(默认):显示抛出RejectedExecutionException。 -
CallerRunsPolicy:让提交任务的线程自己执行。 -
DiscardPolicy:静默丢弃。 -
DiscardOldestPolicy:丢弃队列最老的任务。
-
-
如何避免:
-
使用有界队列 ,避免
LinkedBlockingQueue默认无界导致 OOM。 -
根据业务量合理设置核心线程数和最大线程数。
-
监控线程池状态(活跃线程数、队列大小),及时告警。
-
-
线上 JVM 调优的经历:如何分析 OOM、频繁 Full GC 的问题?
如何分析 OOM?
-
第一步:保留现场 。启动参数加上
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump。 -
第二步:分析 dump 文件 。用
jvisualvm或jhat打开,查看:-
哪些对象占用了最多内存(Dominator Tree)。
-
是否有内存泄漏(Leak Suspects 报告)。
-
-
常见原因:
-
线程池队列无界导致任务堆积。
-
数据库连接、IO 流未关闭。
-
大量动态代理类生成。
-
2. 如何分析频繁 Full GC?
-
第一步:查看 GC 日志 。
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log。 -
第二步:用工具分析 。
GCViewer、GCEasy。 -
常见原因:
-
内存泄漏:老年代对象持续增长,每次 Full GC 后回收效果差。
-
大对象直接进入老年代 :
-XX:PretenureSizeThreshold设置不当。
-
-
解决:调整堆大小、新生代比例、选择合适的垃圾回收器(如 G1、ZGC)。
-
MySQL 索引的底层数据结构(B+ 树),为什么不用 B 树?
-
B+ 树特点:
-
非叶子节点只存索引,不存数据,所以一个节点能存更多索引,树更矮胖,磁盘 IO 次数更少。
-
所有数据都在叶子节点,且叶子节点用链表相连,范围查询效率极高。
-
-
为什么不用 B 树:
-
B 树的非叶子节点也存数据,导致每个节点能存的索引数量少,树更高,IO 次数多。
-
B 树范围查询需要中序遍历,效率不如 B+ 树的链表遍历。
-
-
索引失效的几种情况。
在索引列上使用函数 或运算
使用 LIKE 以 % 开头(如 %abc)
隐式类型转换
联合索引不满足最左前缀原则
OR 连接的条件中有一个没有索引
-
事务的四大特性
-
原子性(Atomicity) :事务要么全部成功,要么全部失败。由
undo log实现。 -
一致性(Consistency):事务执行前后,数据完整性约束不被破坏。
-
隔离性(Isolation) :并发事务之间互不干扰。由
锁和MVCC实现。 -
持久性(Durability) :事务提交后,数据永久保存。由
redo log实现。
- 你对 JVM 了解吗?
JVM 是 Java 程序运行的基础,它屏蔽了底层操作系统的差异,实现了"一次编写,到处运行"。我主要从三个维度理解 JVM:
1. 运行时数据区(内存结构)
-
线程私有:
-
程序计数器:记录当前线程执行字节码的行号,是唯一不会 OOM 的区域。
-
虚拟机栈 :每个方法执行时创建栈帧,存储局部变量表、操作数栈、动态链接等。栈深度超限会抛
StackOverflowError。 -
本地方法栈:为 Native 方法服务。
-
-
线程共享:
-
堆:存放对象实例,是 GC 的主战场。分新生代(Eden + Survivor)和老年代。
-
方法区:JDK 8 之后由**元空间(MetaSpace)**实现,使用本地内存,存放类元信息、常量、静态变量等。
-
2. 垃圾回收(GC)
-
核心是判断对象是否存活(可达性分析),然后由不同的垃圾回收器进行回收。
-
常见回收器:Serial、Parallel、CMS、G1、ZGC。
3. 类加载机制
-
包括加载、验证、准备、解析、初始化五个阶段。
-
核心是双亲委派模型,保证类的唯一性和安全性。
4. 执行引擎
- 解释器逐行执行字节码,JIT 编译器将热点代码编译为机器码,提升性能。
- 垃圾回收器,你知道哪些呢?
- CMS(Concurrent Mark Sweep) :以最短停顿时间为目标,采用标记-清除算法。缺点:产生内存碎片、并发阶段占用 CPU。
- G1(Garbage First) :JDK 9 之后的默认回收器。将堆划分为多个 Region,采用标记-整理算法,可预测停顿时间。适合大堆(6GB 以上)。
- 双亲委派模型你知道吗?
双亲委派模型是类加载机制的核心,它规定了类加载器之间的协作关系。
1. 类加载器层次
-
Bootstrap ClassLoader :启动类加载器,加载
JAVA_HOME/lib下的核心类(如rt.jar),由 C++ 实现,Java 中获取为 null。 -
Extension ClassLoader :扩展类加载器,加载
JAVA_HOME/lib/ext下的类。 -
Application ClassLoader :应用类加载器,加载
classpath下的类,也就是我们写的代码。 -
自定义 ClassLoader :用户继承
ClassLoader实现的加载器。
2. 工作流程
-
当一个类加载器收到加载请求时,它不会自己先加载 ,而是委派给父加载器。
-
父加载器再委派给它的父加载器,直到 Bootstrap。
-
如果父加载器无法完成加载(在自己负责的路径下找不到),子加载器才会尝试自己加载。
3. 好处
-
保证类的唯一性:同一个类在 JVM 中只会被加载一次,避免重复加载。
-
保证安全性 :防止用户自定义的类(如
java.lang.String)覆盖核心类库。
- 双亲委派模型它可以被打破吗?