Java 基础篇
- 静态代理和动态代理的区别
静态代理:本质就是每多一个实体类需要编写一个静态代理对象去做局部增强,简单代码例子:
// 静态代理类(经纪人)
class SingerProxy implements Singer {
private RealSinger target; // 代理知道要给谁当经纪人
public SingerProxy(RealSinger target) {
this.target = target;
}
@Override
public void sing(String song) {
System.out.println("💰 经纪人:收订金");
target.sing(song); // 让真正的歌手唱
System.out.println("💰 经纪人:收尾款");
}
}
public class Main {
public static void main(String\[\] args) {
RealSinger jay = new RealSinger();
SingerProxy proxy = new SingerProxy(jay); // 手动创建经纪人
proxy.sing("夜曲"); // 现在有增强效果了
}
}
当多创建一个 Dancer,那么要一个 DancerProxy,以此类推。
动态代理:为一个接口实现了 InvocationHandler,使用了 Object proxy、Method method(标识)、Object\[\] args(放置具体方法)来实现的,可以只用一段代码为多个类都生成代理类,简单代码实现:
// 1. 这是"增强逻辑"的工厂,专门负责在调用方法前后"加戏"
class DynamicProxyHandler implements InvocationHandler {
private Object target; // 这个 target 是谁?就是被代理的歌手 jay
public DynamicProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object\[\] args) throws Throwable {
// ⭐ 这里就是"增强"的地方:在真正唱歌之前执行
System.out.println("💰 动态经纪人:收订金");
// ⭐ 这行是核心:通过反射,真实地调用 jay.sing("夜曲")
Object result = method.invoke(target, args);
// ⭐ 在真正唱歌之后执行
System.out.println("💰 动态经纪人:收尾款");
return result;
}
}
追问:泛型和动态代理都是减少了重复代码的使用,所以动态代理的本质是泛型吗?
回答:1. 泛型它运用的周期是编译前,当开始编译的时候就会被擦除。实际例子:
原型:
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);
编译后:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
对于编译时,代码为 List<String> 还是 List<Integer> 没有区别,无法识别,是根据最后的强转实现的。
- 动态代理它是运行时生成。
例子可以延续上面的简单案例,在运行前,它就是这一段代码,依旧只存在 Object 类型,当你传递后才会自动根据代码创建对应类型的代理,最终的类型就类似于你自己手动写出了新类型的静态代理,可以实例化。
追问:JDK 动态代理和 CGLIB 动态代理的区别
回答:1. JDK 动态代理只能实现(implement)有接口的类,例子解释原因是因为 invoke(Object proxy, Method method, Object\[\] args) 中 Method method 是从接口中获取方法的,所以必须传入接口类,如果没有接口,这个会报错。
- CGLIB 可以实现接口与类的双重实现,因为两者实现的接口不一样,JDK 实现的是 InvocationHandler,而 CGLIB 继承(extend)的是 MethodInterceptor。
它是通过子类继承父类来实现的动态代理,所以它所创建的动态代理本质是父类的子类,所以可以通过父类的方法集知道有什么方法,所以 CGLIB 无法代理父类的 final 方法和 private 方法。
- IOC 和 AOP 是通过什么机制实现的
回答:1. AOP 本质是面向切面编程,作用是不修改内部方法增强函数方法,所使用的机制则是动态代理,JDK 动态代理中的 invoke 函数以及 CGLIB 中的 intercept 函数均是增强函数。
- IOC 容器控制主要是依赖反射来实现依赖翻转,从原来需要用户手动创建如 new User()(自己控制生成、使用、销毁等一系列过程)到直接使用 @Resource(生成、使用、销毁交给系统决定),源码伪代码大致如下:
Class<?> clazz = Class.forName("com.example.UserService");
Object instance = clazz.newInstance(); // 通过反射创建对象
// 第三步:依赖注入(仍然用反射)
Field field = clazz.getDeclaredField("userDao");
field.setAccessible(true); // 处理 private 字段
field.set(instance, userDaoInstance);
- Hashtable 如何实现的线程安全
回答:Hashtable 中所有的 public 方法都使用了 synchronized 进行修饰,所有方法的访问都需要排队。
- ConcurrentHashMap 使用的是悲观锁还是乐观锁
回答:若桶为空桶则使用 CAS 乐观锁,CAS 的原名为 compare and swap(比较并交换),这是一条 CPU 中的原子级别的命令,不受线程切换的影响。若不是空桶则使用 synchronized 悲观锁。
追问:已经用了 synchronized 为什么还要用 CAS 呢?
答:CAS 是属于无锁加入,性能极快,无需加锁。synchronized 属于兜底机制,需要加锁消耗性能。
追问:在 1.8 前 ConcurrentHashMap 使用的是什么?
答:在 1.8 前 ConcurrentHashMap 使用的是 Segment,Segment 中的最小颗粒度是桶,Segment0: 桶0、1、2,Segment1: 桶3、4、5 类似,当数条数据的 hash 值被分到 0、1、2 因为是坐落在同一个 Segment 中,所以这数条命令写操作都需要排队。
-
volatile 作用解释
-
可见性,强制线程从主内存中读取被 volatile 修饰的变量信息,如果不使用 volatile,可能修改变量但是别的线程无法感知,因为每个线程都有自己的工作内存。
-
阻止系统重排序。
-
HashMap 在多线程会出现的问题
-
扩容容易导致环形链表出现:
这是 JDK 1.7 最著名的 Bug。让我用具体例子演示:
初始状态(单线程正常情况)
text
HashMap 扩容前(容量=2):
桶0:A → B
桶1:空
扩容后(容量=4):
需要把 A 和 B 重新 hash 到新数组
假设 A 和 B 在新数组中都在 桶0
JDK 1.7 头插法扩容过程:
遍历旧链表:A → B
取出 A,头插到新数组:A
取出 B,头插到新数组:B → A
最终顺序反转:B → A
多线程并发扩容(死循环形成)
text
初始:桶0 链表:A → B
线程1 和 线程2 同时触发扩容
线程1 执行到 e = A, next = B(还没完成扩容)
线程2 先完成扩容,结果变成:B → A
线程1 继续执行:
线程1 的 e = A,准备把 A 移到新数组
线程1 发现 A.next 已经被线程2 改成了 B(但 B.next = A)
线程1 继续处理 B,发现 B.next = A
线程1 再处理 A,发现 A.next = B
⭐ 形成环形链表:A ↔ B(相互引用)
环形链表 → get() 操作会无限循环 → CPU 100% 💥
- 多线程同时使用 put 会导致 key 被覆盖:
初始状态:桶3 是空的
时间点 1:
线程1 计算出桶3 为空,准备插入 key1
线程2 计算出桶3 为空,准备插入 key2
时间点 2:
线程1 将 key1 插入桶3(成功)
线程2 也将 key2 插入桶3(成功)
时间点 3:
线程1 的 key1 被线程2 的 key2 覆盖!
或者线程2 的 key2 被线程1 的 key1 覆盖!
结果:只有一个 key 存在,另一个丢失 ❌
为什么不可以同时两个 key 都关联上呢?因为源码底层当判断为空的时候,会将桶直接 = node1 而不是关联,所以会被覆盖。
- 重写 HashMap 的 equals 和 hashCode 方法需要注意什么
准则如下:当 equals 相同时,hashCode 必须相同。当 hashCode 相同时,equals 不一定相同(哈希冲突)。
- HTTP 长连接和 WebSocket 有什么区别
答:全双工和半双工:TCP 协议本身是全双工的,但我们最常用的 HTTP/1.1,虽然是基于 TCP 的协议,但它是半双工的,对于大部分需要服务器主动推送数据到客户端的场景,都不太友好,因此我们需要使用支持全双工的 WebSocket 协议。应用场景区别:在 HTTP/1.1 里,只要客户端不问,服务端就不答。基于这样的特点,对于登录页面这样的简单场景,可以使用定时轮询或者长轮询的方式实现服务器推送(Comet)的效果。对于客户端和服务端之间需要频繁交互的复杂场景,比如网页游戏,都可以考虑使用 WebSocket 协议。
-
JWT 令牌泄露了怎么办
-
黑名单(会导致服务器要存储信息,降级成 Session)。
-
短有效期 + 自动续签:一个短期大概只有十五分钟,在 Cookie 中存储;长期有七天时间,在 HttpOnly Cookie 中存储。当过期使用长期的申请一个短期的,同时也会再次生成一个长期的,将旧长期废除。旧的废除本质也是使用了黑名单,但是这样尽可能避免直接使用黑名单导致降级。