总结:3道算法+八股面经+docker安装和实验报告
括号生成
给一个n代表括号的对数,设计一个函数,用于能够生成所有可能的并且有效的括号组合。
有效:
- 全局:左括号总数 = 右括号总数 = n;
- 任意前缀:已出现的左括号数 ≥ 右括号数(否则会出现
())这种「右括号没人配」的死局)。
思路:使用 回溯 + 剪枝
递归过程中只维护三个量:
| 变量 | 含义 | 决策 |
|---|---|---|
open | 已经用了几个( | open < n→ 还能放( |
close | 已经用了几个) | close < open→ 才能放) |
len = open + close | 当前长度 | len == 2n→ 收集答案 |
class Solution {
public List generateParenthesis(int n) {
Listres=new ArrayList();
dfs(res,new StringBuilder(),0,0,n);
return res;
}
private void dfs(Listres,StringBuilder s,int left,int right,int n){
if(left+right==2*n){
res.add(s.toString());
return;
}
if(left<n){
s.append(‘(’);
dfs(res,s,left+1,right,n);
s.deleteCharAt(s.length()-1);
}
if(right<left){
s.append(‘)’);
dfs(res,s,left,right+1,n);
s.deleteCharAt(s.length()-1);
}
}
}
分隔回文串
给你一个字符串s,将s分割成一些 子串,使每个子串都是回文串。返回s所有可能的分割方案。
思路:
需要一个指针start:当前要处理的起始下标。
- 从
start出发,枚举结束位置end ∈ [start, n-1]; - 如果
s[start..end]是回文,就把它切成一段,然后递归处理end+1; - 如果
start == n,说明整串被切完了,且每一段都经过回文检验 → 收集答案。
class Solution {
List<List>res=new ArrayList<>();
Listpath=new ArrayList<>();
char[] c;
int n;
public List<List> partition(String s) {
c=s.toCharArray();
n=s.length();
dfs(0);
return res;
}
private void dfs(int start){
if(start==n){
res.add(new ArrayList<>(path));
return;
}
for(int end=start;end<n;end++){
String temp=new String(c, start, end - start + 1);
if(ishuiwen(temp)){
path.add(temp);
dfs(end+1);
path.remove(path.size()-1);
}
}
}
private boolean ishuiwen(String s){
int n=s.length();
int l=0;
int r=n-1;
while(l<r){
if(s.charAt(l)!=s.charAt®)return false;
l++;
r–;
}
return true;
}
}
- 长度/下标 API:
- 数组:
arr.length(属性,无括号) - 字符串:
s.length()、s.substring(l, r)(r 不含) - 集合:
list.size()、list.remove(index) - char[] 截取:
new String(ch, offset, count)(第三个是长度)
2.res.add(new ArrayList<>(path))为什么要拷贝
path是同一个对象,回溯过程中会不断 add/remove。如果写res.add(path),列表里存的是引用,最后你会得到一堆空列表或残缺数据。加new ArrayList<>(...)相当于当场拍一张快照存进去。这是所有「回溯收集 List<List<>>」题目的最高频 bug。
1.什么是 AOP
AOP是一种编程思想,它把那些与核心业务逻辑无关,但被多处模块共同依赖的代码(如日志、事务、权限)抽取出来,形成独立的“切面”,然后通过一种机制(织入)将这些切面应用到业务方法上。
核心目的:解耦。让业务代码只关心业务,让通用逻辑(切面)独立维护。
核心概念:
切面(Aspect):通用逻辑的模块化,比如一个日志切面。
连接点(Join Point):程序执行过程中的一个点,比如一个方法的调用。
切入点(Pointcut):一个表达式,用来匹配哪些连接点需要被增强。
通知(Advice):在切入点前后要执行的具体动作,如
@Before、@After、@Around。
2.AOP实现方式:
基于代理的 AOP(Spring AOP):
JDK 动态代理:基于接口。要求目标类必须实现至少一个接口。代理类和目标类实现同一个接口。
CGLIB 动态代理:基于继承。通过生成目标类的子类来创建代理,因此无法代理
final类或final方法。Spring AOP 默认会根据目标类是否实现了接口来自动选择 JDK 或 CGLIB 代理。
基于 AspectJ 的 AOP:
- AspectJ 是一个独立的、功能更强大的 AOP 框架。Spring 可以集成 AspectJ 的注解(如
@Aspect、@Pointcut)和切点表达式语言,但底层实现仍然使用 Spring AOP 的动态代理机制。这是目前最常用的方式。
- AspectJ 是一个独立的、功能更强大的 AOP 框架。Spring 可以集成 AspectJ 的注解(如
3.Spring AOP 的原理
Spring AOP 的底层原理是动态代理。
代理创建:当 Spring 容器启动时,它会扫描所有被
@Aspect注解的类,并为那些需要被增强的目标 Bean 创建代理对象。代理选择:
如果目标类实现了接口,默认使用JDK 动态代理。
如果目标类没有实现接口,则使用CGLIB 动态代理。
调用拦截:当调用代理对象的方法时,调用会被转发到
InvocationHandler(JDK)或MethodInterceptor(CGLIB)。通知执行:在调用处理器中,在目标方法执行前后插入相应逻辑(如开启事务、记录日志),然后通过反射调用目标方法。
4.Java 中的锁有哪些
可以从不同维度分类:
按锁的实现:
synchronized:JVM 内置关键字,基于 Monitor 机制。
ReentrantLock:JUC 包下的 API,基于 AQS 实现。
按锁的特性:
公平锁 / 非公平锁:是否按请求顺序获取锁。
可重入锁:同一线程可重复获取同一把锁。
独享锁 / 共享锁:独享锁一次只能一个线程持有(如 ReentrantLock),共享锁可多个线程持有(如 ReadWriteLock 的读锁)。
乐观锁 / 悲观锁:乐观锁假设无冲突,操作时检查(如 CAS);悲观锁假设总有冲突,操作前先加锁(如 synchronized)。
偏向锁 / 轻量级锁 / 重量级锁:synchronized 的锁升级状态。
5.synchronized 加锁的流程
synchronized的锁状态会随着竞争情况升级,不可逆:
偏向锁:当只有一个线程访问时,锁会偏向该线程。在对象头中记录线程 ID,该线程再次进入时无需任何同步操作。
轻量级锁:当有第二个线程尝试获取锁时,偏向锁升级为轻量级锁。线程通过 CAS 自旋的方式尝试获取锁。
重量级锁:当自旋超过一定次数或第三个线程加入竞争时,轻量级锁升级为重量级锁。此时,未获取到锁的线程会被阻塞,进入操作系统层面的等待队列。
6.什么是锁消除
锁消除是 JVM 的即时编译器(JIT)在编译时进行的一种优化。
原理:JIT 编译器会通过逃逸分析判断一个锁对象是否只会被一个线程访问。如果确定锁对象不会“逃逸”出当前线程(即不会被其他线程访问),那么 JIT 就会认为这个锁是多余的,从而消除对应的加锁、解锁操作。
7.ReentrantLock 公平锁是怎么实现的
ReentrantLock 的公平锁是通过AQS(AbstractQueuedSynchronizer)实现的。
核心机制:
CLH 队列:AQS 内部维护一个 FIFO 的双向队列(CLH 队列),所有等待获取锁的线程都会被包装成 Node 节点加入队列。
公平性判断:当线程尝试获取锁时,公平锁会先检查队列中是否有前驱节点。只有当当前线程是队列的第一个等待者时,才会尝试获取锁。
与非公平锁的区别:非公平锁在尝试获取锁时,会直接尝试 CAS 抢占,而不关心队列中是否有等待的线程,这可能导致“插队”。
8.CAS 的缺点是什么,如何避免
CAS(Compare-And-Swap)是一种乐观锁实现,包含三个操作数:内存位置当前值 V、预期原值 A、新值 B。当且仅当 V 的值等于 A 时,才将 V 更新为 B。
缺点:
ABA 问题:一个值从 A 变成 B,又变回 A。CAS 检查时以为没变,但实际变了。
自旋开销大:如果 CAS 长时间失败,线程会一直自旋,消耗 CPU。
只能保证一个变量的原子操作:无法直接保证多个变量操作的原子性。
如何避免:
ABA 问题:使用版本号或时间戳。Java 提供了
AtomicStampedReference类,它内部维护了一个版本号,每次更新都会递增版本号。自旋开销:可以设置自旋次数上限,超过后转为阻塞(如
synchronized)。或者使用LongAdder等高并发场景下性能更好的类。多变量原子性:可以封装成一个对象,使用
AtomicReference来保证整个对象的原子性。或者直接使用锁。