面试题
自己整理的一些常见面试题和知识点
Day1:Java 集合
目标:
1 | Collection |
1. 为什么 ArrayList 查询快,而 LinkedList 查询慢?
ArrayList 底层是连续内存空间,可以通过数组下标直接计算出元素地址,因此随机访问时间复杂度是 O(1)。LinkedList 底层是双向链表,每个节点只知道前后节点的位置,因此访问第 n 个元素需要从头或尾开始遍历,时间复杂度是 O(n)。
2. 为什么 LinkedList 插入快,但项目中却很少使用?
实际场景中基本都是查询和遍历,例如分页查询,获取数组中第几个元素什么的。链表虽然插入和删除快,但是也要先查找遍历到指定元素才可以。而且从中间插入的需求实际业务中很少遇到。我基本没有用过linkedList
2.1 那 LinkedList 真有比 ArrayList 快的时候吗?
有。
如果已经拿到了链表节点,例如 ListIterator 已经定位到了当前位置,或者频繁在队列头尾插入删除,那么 LinkedList 确实比 ArrayList 更合适。
但普通业务开发很少直接操作链表节点,所以实际项目中使用率很低。
3. ArrayList 为什么要自动扩容,而不是固定长度?
如果没有动态数组,就需要自己 System.arraycopy。
并且数组是 Java 语言的基础类型。
Java 希望:
数组:
简单
高效
固定长度
ArrayList:
自动扩容。
所以
数组:
负责性能。
ArrayList:
负责易用。
4. HashMap 为什么比遍历 List 查找快?
假设有100万人。现在找张三。
第一种:一个一个找O(n)
第二种:按姓氏放。张一个桶。李一个桶。王一个桶。
是不是快很多?
这就是:
Hash。
它本质就是:
通过计算规则,把数据快速定位到某个桶(Bucket)。
所以 HashMap 的第一层不是链表,而是:
1 | Bucket[] |
一个桶数组。
4.1 为什么 HashMap 不是按姓氏分组?
姓氏只是举例。
实际 HashMap 是调用 key 的 hashCode(),经过扰动函数计算,再通过 (n - 1) & hash 定位到桶,而不是按照某种业务规则分组。
5. 为什么 HashMap 不直接把冲突元素都放在链表里,而是后来又引入了红黑树?
链表查找只能一个一个遍历,太慢了。红黑树的查找效率为o(log2n)。
红黑树不是查找最快的树,但它在查找、插入、删除三者之间取得了最好的平衡,因此 Java 选择了红黑树。
6. ArrayList 为什么扩容 1.5 倍?
扩容太小会导致频繁复制数组,扩容太大会浪费内存,因此 JDK 采用 1.5 倍作为空间利用率和扩容成本之间的折中。
7. ArrayList 为什么线程不安全?
例如:
1 | list.add(a) |
两个线程:
同时:
add。
可能:
1 | size = 10 |
Day2:HashMap
目标:
1 | HashMap |
1. 为什么 HashMap 查找时不能只用 equals()?为什么还需要 hashCode()?
hashcode可以快速定位到hash表的某个桶,算法类似于取余(实际上是位运算)。定位到桶后再用equals确定具体的元素。
如果直接用equals,因为 equals 是 O(n),需要遍历整个集合。而 hashCode 可以通过哈希算法快速定位到某个 Bucket,把查找范围缩小到一个桶内,然后再调用 equals 比较,大部分情况下查找复杂度接近 O(1)。
2. 为什么重写了 equals(),就必须重写 hashCode()?如果不重写会发生什么?
因为hashmap是通过hashcode定位桶的,如果要保证hashmap正常工作,必须保证equals相同的话,hashcode也必须相同。
2.1 那 hashCode 相同,equals 一定相同吗?
不是。
例如:
1 | "Aa".hashCode() |
hash一样。但是equals为false。并且hash也有可能冲突。
3. HashMap 和 ConcurrentHashMap 最大的区别是什么?
HashMap 本身没有任何同步机制,多线程同时 put 或扩容可能导致数据覆盖、数据丢失等问题。ConcurrentHashMap 在 JDK8 中结合 CAS 和 synchronized 实现线程安全,并且采用更细粒度的同步策略,而不是对整个 Map 加锁,因此既保证了线程安全,又具有较好的并发性能。
4. 为什么 ConcurrentHashMap 的性能通常比给整个 HashMap 加一把大锁更高?
因为ConcurrentHashMap是分桶加锁的,操作A桶的时候,没有必要把B桶加锁。
锁粒度越小,并发越高。
5. 为什么增强 for 循环里直接 remove() 会抛 ConcurrentModificationException?
因为Iterator里面有一个expectedModCount,当进行遍历的时候,expectedModCount一直在和list的modCount进行比较。
遍历过程中集合结构发生变化,Iterator 无法保证遍历结果正确,因此直接失败。
6. 为什么使用 Iterator.remove() 就不会抛这个异常?
因为Iterator内部会进行维护expectedModCount。
7. HashMap容量都是16 32 64 128 (跟(n-1)&hash有关)
容量必须是 2 的幂,位运算才能均匀分布到每一个桶。
8. 为什么HashMap扩容都是2倍?
扩容迁移的时候很方便。只需要看扩容新增的那个高位是1还是0,0的话不用动,1的话移动到 原索引 + 旧容量的位置,不用重新计算hash
9. HashMap 为什么允许 null Key
HashMap规定了null.hashCode()=0。
ConcurrentHashMap不允许是因为会产生歧义,不知道是因为key不存在还是value为null。Hashmap也有这个问题,但是可以用containsKey。
ConcurrentHashMap value也不允许null,是为了降低实现的复杂度。ConcurrentHashMap 作为高并发容器,希望 API 尽量简单、一致,不提供 HashMap 中对 null 的特殊支持。
10. HashSet 为什么不重复
HashSet实际就是hashmap,value是固定的present(new Object())
HashSet 底层就是 HashMap,它利用 HashMap 的 Key 唯一性保证元素不重复。
Day3:并发
1 | Java 并发 |
目标:
1 | 并发 |
1. 为什么 Java 已经有 Thread,还要设计 Runnable?
Runnable还可以做到线程与任务解耦。任务可以复用线程。
1.1 什么叫任务和线程解耦?
创建一个Thread的时候,就把任务和线程绑定在了一起。
2. 为什么 Runnable 之后又设计了 Callable?
Runnable 没有返回值,如果需要返回执行结果,只能借助共享变量或回调等方式实现,不够优雅。因此 Java 又提供了 Callable,支持返回值和受检异常。
3. Future 到底是什么?为什么需要它?
Future 表示一个异步任务未来的执行结果。提交任务后,主线程不需要立即等待,可以在需要的时候通过 get() 获取结果。CompletableFuture 是 Future 的增强版本,支持任务编排、回调和组合,因此我项目中主要使用 CompletableFuture。
4. 为什么不能大量使用 new Thread()?
线程是系统资源,不能无限获取,并且创建、销毁、线程上下文切换耗费性能。
4.1 为什么线程切换耗性能?
cpu执行任务的时候需要寄存器、计数器、栈等信息,当发生线程切换的时候,需要保存现场,恢复现场等操作,这些都是耗时的。
5. 线程池最大的作用是什么?除了复用线程,还有什么作用?
- 最大的作用就是复用线程,防止创建线程的开销。还有一个主要目的就是控制线程数量。防止无限创建线程导致系统崩溃。可以统一管理,监控线程。比如线程名字,关闭线程等。
6. 结合你的 GIS 平台,说说为什么你选择 Spring Async + 自定义线程池,而不是直接在 Controller 里执行分析
上传下载shp/gdb,分析叠加等是一个耗时的操作,如果不用线程池,提交任务后一直等待,首先是用户操作层面就不合理。然后还会一直占用tomcat的线程,可能会导致其他请求阻塞。放到后台异步执行任务的话,不影响前台操作。
Day4:并发-线程池
目标:
1 | 并发 |
1. 为什么线程池不是核心满直接创建新线程,而是核心满先放队列?
线程属于昂贵资源,创建和销毁成本较高。所以java优先复用线程,先排队。并且任务很可能很快就执行完了。当队列也满了说明任务积压严重,需要提高线程数量。
线程池优先使用阻塞队列,是因为线程属于昂贵资源,创建和销毁成本较高。如果核心线程都忙碌,先让任务进入队列等待,很多情况下线程很快就会空闲并继续处理队列中的任务,这样可以减少线程数量,提高资源利用率。只有队列也满了,说明任务积压严重,才会继续创建非核心线程提高吞吐量
2. execute()和submit()最大的区别是什么?除了:Future。还有:什么?
submit 更关注任务生命周期。execute就是执行任务,不管任务是否成功失败。submit可以获取结果,停止任务,捕获异常。
3. 假设GIS线程池core=4,max=8,queue=100。突然来了500个空间分析。会发生什么?如果你是架构师。你怎么优化?
- 限制用户提交任务数量
- 增加mq削峰,慢慢消费
- 分布式部署
Day5:JUC
目标:
1 | JUC(java.util.concurrent) |
| AQS实现类 | state含义 |
|---|---|
| ReentrantLock | 锁重入次数 |
| Semaphore | 剩余许可证数量 |
| CountDownLatch | 剩余计数 |
| ReentrantReadWriteLock | 高16位读锁,低16位写锁(这个了解即可) |
1 | 并发安全 |
1. 为什么会出现线程安全问题?本质是什么?
多个线程同时竞争同一个资源。假如两个线程同时对同一个变量a=100进行操作,线程1对a-10,线程b对a-20。结果可能并不是70。因为线程会读取变量a到自己的线程工作内存里,再写回主线程,读到的可能是100,也可能是另一个线程操作后的结果,导致每个线程看到的共享变量可能不是最新值。
2. 为什么 Java 的锁是锁对象,而不是锁代码?
因为多线程操作的是资源,资源需要加锁。对象是资源,代码不是资源。
3. 为什么synchronized(new Object())几乎等于没加锁?
因为每次都是new出来的新对象,每次的锁都不是同一把锁。
4. 分布式系统为什么用分布式锁,不用jvm提供的锁?
synchronized 本质是 JVM 内部锁,它只能保证同一个 JVM 内多个线程互斥。如果系统部署到多台服务器,每个 JVM 都有自己的锁对象,它们彼此独立,因此无法实现跨 JVM 的互斥。而 Redisson 基于 Redis 实现分布式锁,所有节点竞争的是 Redis 中同一个 Key,因此能够保证整个集群范围内的互斥。
分布式系统不在一个jvm下面,获取到的锁对象肯定也不是同一个。根本锁不住。分布式锁可以达到不同的系统使用的同一把锁,例如redisson分布式锁。
5. 为什么synchronized可以重入(Reentrant)?
当发现是同一个线程获取锁的时候就放行了。Monitor 里面维护了 Owner 和 Count
当发现再次进入synchronized(lock){的时候发现Owner == 当前线程,就不阻塞,而是Count++。当退出同步代码块时进行Count–。当Count为0时释放锁。
1 | Java 的锁(Monitor)里面会记录两个东西: |
6. 为什么 Java 要设计可重入?
1 | public synchronized void A(){ |
如果不可重入,执行A的时候获取到锁,然后执行B的时候也需要同一把锁,然后死锁了。
6. 为什么volatile不能保证i++;线程安全?
i++;的执行步骤:
1 | ① 读取 i |
假设线程1读取i为100,进行+1;线程2也读取100,进行+1。结果是101,少了一次。
volatile只能保证线程每次都是从主内存读取的,但是问题不是读错,而是同时写回会覆盖。
volatile 保证的是每次读取都能看到最新值,但 i++ 并不是一个原子操作,它实际上包含”读取、计算、写回”三个步骤。即使每一步都能看到最新值,多个线程仍然可能在读取同一个值后分别计算并覆盖彼此的结果,因此 volatile 无法保证 i++ 的线程安全。
volatile保证看到最新的值。不是保证整个修改过程不能被别人打断。
7. 既然 synchronized 能保证可见性,为什么还需要 volatile?
因为成本问题,synchronized太重。
假设
1 | boolean running = true; |
只需要保证线程B能看到最新running。volatile就够了,根本不用加锁
只有可见性,没有互斥需求。
Day6:CAS
目标:
1 | CAS |
volatile
volatile 写 (volatile 写 = 发布(Publish))
1 | 线程A |
volatile 读 (volatile 读 = 同步(Synchronize))
1 | 线程B |
1. CAS为什么必须配合volatile?
因为比较的值必须是最新的值。
volatile写会在volatile写操作周围增加memory barrier,然后cpu执行barrier,cpu缓存一致性协议,让其他cpu的Cache line失效。
volatile读可以利用 ‘volatile写 happens-before volatile读’让其他线程得到最新的值。
2. CAS为什么叫乐观锁?
CAS 认为大部分情况下不会发生竞争,所以不会先加锁,而是先尝试修改。如果修改失败,再重新读取最新值继续尝试,因此称为乐观锁。
3. 为什么AtomicInteger内部不是if(){}而是while(true)
getAndIncrement是通过CAS改变的值,cas如果修改失败需要不断的重试,所以需要while。
4. ABA到底有什么危害?为什么版本号能解决?
加入一个变量x,一开始是A。然后线程1要使用cas把x变为B,比较的时候如果x是旧值A就改为B。但是在线程1修改之前,已经有其他线程修改了x,把x变为了B又变回了A。这时候如果不加版本号线程1就不知道x已经被修改过了,并不是他预期的A。
5. 为什么CAS没有线程切换却也可能CPU100%。为什么?
因为CAS不停的自旋读取、比较。而synchronized获取锁失败直接阻塞挂起,cpu反而空闲。CAS是拿 CPU 换线程切换。
6. 假设1000线程全部AtomicInteger.incrementAndGet();CAS是不是一定比synchronized快?为什么?
不一定,如果cas一直发生冲突,那么重试的次数太多,会导致cpu把算力浪费在比较上。
CAS 适合低竞争场景;锁适合高竞争场景
7. CAS 最大的缺点是什么?
CAS 只能保证一个共享变量的原子更新。
8. 超卖问题
假设
1 | AtomicInteger stock = new AtomicInteger(10) |
卖票:
1 | if(stock.get()>0){ |
请问有没有线程安全问题?
还是会有安全问题,虽然stock.get()是volatile的,每次if读取的时候也都是最新值。但是有可能,线程1读取为1,然后cas成功减为了0。同时线程2也读取为1,然后去执行减没有成功,然后重新读取为0,然后再减1,最终的值可能会比0小。
Day 7:AQS
关键流程图
1 | Thread |
1. AQS为什么一定要维护一个volatile int state; 为什么不是boolean?
因为AQS是一个抽象类,他还有很多实现类。各个实现类不只是用两个状态就可以解决问题。
2. 为什么AQS不用CAS一直while(true)而是park()?
因为一直while会浪费cpu,在竞争大的时候,很多情况都会失败。而park()会使线程休眠,释放cpu,当锁释放出来的时候再unpark进行一次cas。
3. ReentrantLock为什么叫Reentrant?它怎么做到可重入?
它会判断是否是当前线程持有锁,当进入加锁的同步代码时,如果是当前线程持有锁,他就会对state进行+1,当退出同步代码时,state会-1。
4. 为什么ReentrantLock功能比synchronized多?
因为ReentrantLock是利用AQS使用Java代码实现的。而synchronized的功能已经写死在了jvm。
5. 假设你的GIS平台。服务器16核。你希望最多只有4个空间分析同时运行。其它任务等待。请问你用AtomicInteger还是Semaphore还是ReentrantLock?
选择Semaphore。因为4个空间分析是4个线程。用ReentrantLock肯定只有一个线程获得锁。AtomicInteger只能保证一个元素是原子的,而启动任务肯定是多个操作,实现起来太复杂。
Semaphore使用起来更方便,启动一个任务就Semaphore-1
6. ReentrantLock: 为什么lock.unlock()必须放finally?
1 | lock.lock(); |
不管执行是否出现异常,都必须保证锁最终要释放。否则会造成死锁。
ReentrantLock 不像 synchronized 会自动释放锁,它必须由程序员手动调用 unlock()。如果中间发生异常而没有执行 unlock(),state 永远不会减到 0,其它线程将一直 park 在 AQS 队列中,导致死锁。
7. ReentrantLock: 为什么下面代码会死锁?
1 | lock.lock(); |
虽然是同一线程可重入,但是这个线程每次加锁的时候都会state++,而它加锁了两次最后只减了一次,所以锁没有释放。
8. 为什么ReentrantLock公平锁比非公平锁慢?
公平锁严格先进先出,即使刚来的线程刚好碰到state为0,也要必须排队。
非公平锁可以插队,刚来的线程碰到state为0可以直接拿锁,少一次拍断排队唤醒。
Day 8:JVM
目标:
1 | JVM |
GC Root有哪些?
- 虚拟机栈中局部变量引用的对象。
- 方法区(Metaspace)中静态变量引用的对象。
- 方法区中常量引用的对象。
- JNI(本地方法)引用的对象。
GC Root 一定在栈上吗?
不一定。
GC Root 的本质是 JVM 认为一定存活、作为可达性分析起点的对象引用。栈中的局部变量只是最常见的一类,静态变量、JNI 引用、活动线程、Class 对象、Monitor 等都可以成为 GC Root。
1. 为什么Java对象放Heap。引用放Stack?
对象放在堆中主要有两个原因。
第一,对象通常比较大,而栈空间比较小,栈主要用于方法调用,如果整个对象都放栈中,每次方法调用和返回都会产生大量数据复制,效率很低。
第二,对象的生命周期通常长于方法,例如对象可以作为返回值返回给调用者,如果放在栈中,方法结束对象就没了,所以对象必须放在堆中,而栈中只保存对象引用。
2. 为什么Java不用引用计数。而采用GC Root?
引用计数最大的缺陷是无法处理循环引用,例如 A 引用 B,B 引用 A,即使整个对象图已经不可达,引用计数仍然大于 0。而 GC Root 可达性分析是从根对象开始遍历,只要从任何 GC Root 都无法到达,就会被回收,因此不会受到循环引用影响。
3. 下面代码方法结束。什么时候User真正释放?立即?还是GC?为什么?
1 | public void test(){ |
方法结束后,局部变量 user 随栈帧销毁,不再是 GC Root。如果堆中的 User 对象已经不可达,那么它就成为垃圾对象。但垃圾对象不会立即释放,而是在下一次垃圾回收时被 GC 回收,具体时间由 JVM 决定。
4. 下面代码是不是立即回收?为什么?
1 | User user = new User(); |
user = null 只是断开了一个引用,并不会立即触发 GC。对象只是变成了”可能被回收”。真正什么时候回收,由 JVM 根据垃圾回收策略决定。
5. 你的GIS平台。分析一个3GBShape。分析结束为什么Java不会立刻释放3GB内存?为什么有时候你明明已经task = null;JVM内存还是3GB?
方法结束user引用随着栈帧一起被销毁,但是对象还在heap里,如果它真的不可达的话,会在下一次gc回收。但即使gc回收了,内存也不一定立刻把内存还给操作系统
6. 综合题
下面代码
1 | public void test() { |
请问:
1. System.gc() 会不会一定 GC?
System.gc() 只是向 JVM 发出一次”建议进行垃圾回收”(Suggestion),JVM 可以接受,也可以忽略,它并不能保证一定立即执行 GC。
这里要记住一句经典的话:
System.gc() ≠ 立即 GC。
2. User 对象会不会一定被回收?
如果 User 对象不可达,它就成为了垃圾对象(Eligible for GC),但什么时候真正回收,仍然由 JVM 决定。
3. System.gc() 是不是一定会 Full GC?
System.gc() 只是向 JVM 发出一次”建议进行垃圾回收”(Suggestion),JVM 可以接受,也可以忽略,它并不能保证一定立即执行 GC。
Day9:JVM
目标:
1 | 1. 对象创建过程 |
对象创建过程
第一步:检查类是否已经加载
第二步:在 Heap 中申请内存
第三步:对象初始化
第四步:执行构造函数
第五步:栈中保存引用
类加载过程
加载(把.class读进jvm)
链接(准备、准备内存、静态变量赋默认值)
初始化(执行静态代码块,静态变量初始化)
JDK9之后heap改变了
G1 不再固定划分新生代和老年代,而是将堆划分为多个 Region,每次优先回收回收收益最高(Garbage First)的 Region,从而更好地控制停顿时间,因此成为 JDK9 以后的默认垃圾回收器。
1. 为什么对象要先赋默认值,再执行构造函数?
JVM 会先把对象初始化为默认状态,保证所有成员变量都有合法初始值,然后再执行构造函数完成业务初始化。
2. 为什么下面代码不会报错?为什么输出的是0?
1 | class User{ |
不会报错,因为创建对象的时候会先对成员变量进行赋默认值再调用构造器。
局部变量不会赋默认值。
3. 为什么 JVM 要把 Heap 分成 Young 和 Old?
JVM 发现绝大多数对象生命周期都很短,因此采用分代思想,让不同生命周期的对象采用不同的回收策略。
4. 为什么 Minor GC 比 Full GC 快很多?
Young 区的大部分对象都是垃圾对象,存活率低,所以复制成本很低。
新生代(Young)最经典的是复制算法(Copying)。不是把垃圾对象删除,而是把活对象复制到另一块 Survivor 区。然后清空整个 Eden。
Full GC使用的标记整理(Mark-Compact)法。
5. 为什么 G1 比 CMS 更适合大内存服务器?
因为G1把heap分成了很多个区域,每次发生gc的时候会挑选垃圾对象较多的区域进行回收。可以更好的控制停顿时间。如果每次gc的时候都清理一整个大heap,停顿时间会很长。
6. 为什么很多线上服务建议 “-Xms = -Xmx”
因为jvm申请内存需要stop the world。
7. 为什么 Java 局部变量没有默认值,而成员变量有?
java创建对象的时候会先赋默认值,再调用构造器。
局部变量需要手动赋默认值,否则容易掩盖开发bug。
8. 为什么 Java 要把heap分代,而不是整个 Heap 全部统一GC?
为了更好的控制gc停顿。jvm发现大部分对象都是存活时间很短的对象,所以对不同生命周期的对象采用不同的回收策略。Minor GC复制成本低,所以快。
JVM 发现大部分对象都是朝生夕死的,因此采用分代思想。新生代对象存活率低,采用复制算法,只复制少量存活对象,回收效率很高;老年代对象存活率高,采用标记整理等算法,避免大量对象复制。因此分代不仅减少了 GC 停顿时间,也让不同生命周期的对象使用最适合的垃圾回收算法。
Day10:JVM
目标:
1 | 1. GC 算法(★★★★★) |
GC 算法
- 标记清除(Mark-Sweep)
产生内存碎片- 复制算法(Copying)
只能针对young区,存活对象少的合适。因为复制成本低。把存活对象存到survive区,eden区直接清空。- 标记整理(Mark-Compact)
old区使用的,会把存活对象进行整理,减少内存碎片。但是效率低。
双亲委派
保证了 Java 核心类不会被用户自定义类替换,保证了类加载的安全性和唯一性。
JVM 参数
1 | -Xms |
为什么标记清除容易OOM?
不是Heap不够。而是连续空间不足。
1.为什么 “static int a = 100;” 不是直接100?为什么先0再100?
因为jvm加载class的时候会先对static变量进行赋默认值,然后再执行静态代码块和初始化静态变量。
2. 为什么标记清除会产生内存碎片?
标记然后删除会产生空洞,这些空洞就是内存碎片。他们不是连续的内存空间,容易发生oom。
容易因为无法找到足够大的连续空间而导致内存分配失败。
3. 为什么Young用复制算法Old用标记整理?
因为young区的对存活率低,复制的对象少,复制成本低,所以使用复制算法。
old区的对象存活率高,使用复制的话每次都要复制很大的数据,效率低。而标记清除法容易产生内存碎片。标记整理也是删除,但是会移动整理,减少内存碎片。
JVM 根据对象不同的存活率,选择不同的垃圾回收算法。
4. 为什么G1比CMS停顿更容易控制?
因为G1把heap分成了很多个区,每次GC的时候会挑选垃圾比例高(Garbage First)的 Region进行回收。停顿时间更可控。
5. 为什么Java需要双亲委派?
防止开发者自己写了类似java.lang.String的类,导致整个Java错乱。
保证一个类在 JVM 中只有一个 Class 对象。
6. 为什么双亲委派可以防止重复加载?
因为ClassLoader加载类的时候会先询问自己上级有没有这个类,并且逐级向上,如果都没有才加载自己的。
已经加载过的类,子加载器不会重复加载,而是复用父加载器已经加载的 Class 对象。
7. String s = “abc”;和 String s = new String(“abc”);有什么区别?
String s = “abc” 会先检查字符串常量池,如果 “abc” 已经存在,则直接让引用指向常量池中的对象,不会重复创建。
new String(“abc”) 会先保证字符串常量池中存在 “abc”,然后再在堆中创建一个新的 String 对象,因此至少会创建一个堆对象,而字符串常量池中的 “abc” 可能是新建的,也可能是复用已有的。
创建流程:
1 | new String("abc") |
8. String s = new String(“abc”);创建了几个对象?
情况一:字符串常量池已经有 “abc”,就只在heap创建 String 对象。一共创建了一个对象。
情况而:字符串常量池没有 “abc”,先在字符串常量池创建“abc”,然后在heap里创建 String 对象。一共创建了两个对象。
Day11:Spring
目标
1 | Spring |
Bean 生命周期
1 | 1. 实例化(调用构造方法) |
AOP
AOP(面向切面编程)就是把日志、事务、权限等横切关注点从业务代码中抽离出来,统一增强目标方法。
1. 为什么Spring要IOC为什么比new好?
第一,降低耦合。对象不再依赖具体实现,而是依赖 Spring 容器管理。
第二,统一管理生命周期。Spring 可以统一管理 Bean 的创建、销毁、作用域(Singleton、Prototype 等)。
第三,方便扩展。例如以后 UserDao 换实现,或者增加代理、事务,都不用修改业务代码。
2. Bean生命周期五步是什么?
创建bean、注入依赖、初始化、使用、销毁
3. 为什么@PostConstruct里面@Autowired已经可以使用?
PostConstruct是在初始化之前,注入依赖之后。所以已经可以使用依赖的对象了。
构造方法里面不能保证@Autowired已经完成但是@PostConstruct一定可以。
4. 为什么AOP适合日志事务权限。为什么不适合业务?
aop适合通用的代码逻辑。具体的业务情况谁和谁都不一样。
5. 你的GIS平台如果不用AOP日志怎么写?用了AOP有什么好处?
如果不用aop就只能每个方法都手写一遍记录日志的代码。使用了aop后只需要写一次。
6. 为什么Spring默认Bean是Singleton为什么不是Prototype?
绝大部分 Bean 都是无状态的,比如service、dao,多个线程共享一个实例没有问题。
创建对象、GC会消耗性能,对象越多越占内存。
只有少数需要保存状态的对象才适合 Prototype。比如dto、entity。
7. 为什么SpringIOC底层大量使用反射为什么不用new UserService()
因为springIOC是在运行时创建的bean,Spring 在编译时根本不知道有哪些 Bean。
用new的话,要求编译时就知道类型。
8. 为什么@Autowired默认按类型(Type)注入而不是按名字(Name)?
原因一:面向接口编程
@Autowired可以注入接口或者抽象类。如果实现类只有一个,spring会把这个实现类注入进来。如果需要按名字注入可以用@Resource或者@Qualifier
原因二:名字容易变
@Autowired 默认按类型注入,是为了支持面向接口编程,降低实现类变更带来的影响。
9. 为什么Spring有时候可以解决循环依赖有时候又不能?
1 |
|
流程:
1 | 创建A |
Spring 能解决的是 Singleton Bean 的属性注入循环依赖。
那什么时候解决不了?
第一种:构造器注入
对象都创建不了
第二种:Prototype
每次都是new,没有三级缓存
Spring 能解决 Singleton Bean 的属性注入循环依赖,因为 Bean 实例化后会提前暴露到三级缓存,后续 Bean 可以先引用这个未完全初始化的对象;而构造器注入和 Prototype Bean 不支持这种机制,因此无法解决循环依赖。
Day12:事务
ACID
Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)
事务保证一组操作具有 ACID 特性,要么全部成功,要么全部失败。
1. 为什么Spring事务使用AOP为什么不用每个方法自己写commit?
事务属于横切逻辑。Spring利用aop已经完成了事务开启和事务提交回滚的通用代码。
事务属于横切关注点,每个业务方法都可能需要事务,因此 Spring 使用 AOP 将事务逻辑统一织入目标方法,避免每个方法重复编写 begin、commit、rollback 等代码。
2. 为什么自己调用this.save()事务失效?
因为 Spring 的事务是通过代理对象实现的。外部调用的是代理对象,所以事务能够生效;而 this.save() 是对象内部调用,没有经过代理对象,而是直接调用目标对象的方法,因此事务拦截器不会执行。
3. 为什么private 方法@Transactional失效?
JDK 动态代理是代理接口,接口里没有private方法。CGLIB代理的是子类,子类不能重写private方法。
private 方法不能被代理对象重写,因此事务增强无法织入。
4. REQUIRED和REQUIRES_NEW区别什么时候用REQUIRES_NEW?
REQUIRED是如果有事务就加入事务。没有就创建一个事务。
REQUIRES_NEW 会挂起当前事务,然后创建一个新事物。
当需要两个不同事物的情况下使用REQUIRES_NEW,比如业务代码和保存日志需要是两个事务,业务代码异常回滚,日志应该要保存下来。
流程:
1 | A事务 |
5. 为什么Spring 默认只回滚 RuntimeException?为什么throw new IOException();事务默认不回滚?
非RuntimeException需要手动处理,spring就认为开发者已经知道这个异常,应该主动处理。RuntimeException可能是程序出现bug应该回滚。
Spring 默认认为 Checked Exception(受检异常)是开发者可以预期并处理的业务异常,因此默认不回滚;而 RuntimeException 通常表示程序发生了不可预期的错误,因此默认回滚事务。
6. 为什么下面代码事务最后提交了?
1 |
|
因为方法最终没有抛出RuntimeException。
Day13:Spring Boot
1. Spring Boot 和 Spring 有什么区别?
springboot基于spring,由于spring需要大量繁琐的配置,springboot简化了这一流程。他的约定大于配置。会根据依赖自动加载需要的bean。
Spring Boot 是 Spring 的快速开发框架。
2. @SpringBootApplication 为什么只要一个注解?它里面最重要的三个注解是什么?各自负责什么?
@SpringbootConfigurarion
标注配置启动类
@EnableAutoConfiguration
开启自动配置,根据依赖自动配置。
@ComponentScan
开启注解扫描。例如@Configuration @Componet @Service
3. 为什么导入:spring-boot-starter-data-redis RedisTemplate 就自动有了?
因为这个依赖里面有自动装配RedisTemplate的配置类。
4. 为什么Spring Boot不会把所有AutoConfiguration全部加载?
他会根据条件,比如@ConditionalOnClass、@ConditionalOnMissingBean等。
Spring Boot 会根据各种条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean 等)决定是否加载自动配置,而不是无条件全部加载。
5. 为什么spring-boot-starter-web叫Starter为什么不是spring-web?
Starter就是依赖管理。自动引入需要的全部依赖,本质就是依赖聚合。









