1. 线程安全集合
线程安全和非线程安全的集合举例 :
非线程安全的:平时用的ArrayList、LinkedList、HashSet、HashMap这些都是非线程安全的,单线程没问题,多线程一起读写就可能出问题,比如数据覆盖、ConcurrentModificationException。
线程安全的:有老一代的Vector、Hashtable,它们是方法上加synchronized,性能不好。现在常用的是java.util.concurrent包下的,比如ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue系列。另外还可以用Collections.synchronizedXXX包装,但性能也不如JUC的。
ConcurrentHashMap的实现原理 :
JDK1.7是用分段锁 (Segment数组,继承ReentrantLock),把数据分成一段一段的,每段独立加锁,这样不同线程可以同时操作不同段,提高并发。JDK1.8以后放弃了分段锁,改用CAS + synchronized + 红黑树。具体是:数组+链表+红黑树,put操作时,如果对应bucket为空,就用CAS无锁插入;如果有冲突,就用synchronized锁住链表头节点或树根节点,然后操作。读操作基本无锁(volatile),所以并发性能很高。重点:1.8的ConcurrentHashMap锁粒度更细,只锁一个bucket,而不是一段。
2. 泛型
什么是类型擦除 ?
泛型是Java 1.5引入的,但虚拟机并不认识泛型,编译的时候会把泛型类型擦除掉,替换成上限类型(如果没有指定就是Object)。比如
List<String>在编译后变成List,插入和读取的时候自动插入强制类型转换。这就是类型擦除。所以运行时你是拿不到泛型的具体类型的(除非有特殊手段,比如通过反射获取父类的泛型)。重点:泛型只是编译时的语法糖,运行时不存在。? extends T 和 ? super T 的区别 ?
这是泛型通配符,用来限制类型范围。
? extends T:表示类型是T的某个子类(包括T本身)。用于读操作,比如从集合里取元素,可以当成T类型来用,但不能往里放(除了null),因为不知道具体是哪个子类,放任何东西都不安全。
? super T:表示类型是T的某个父类(包括T本身)。用于写 操作,比如往集合里放元素,可以放T类型或T的子类型,但读取的时候只能当成Object,因为不知道具体是哪个父类。口诀:PECS(Producer Extends, Consumer Super)。如果集合是生产者(向外提供数据),用extends;如果是消费者(往里存数据),用super。
3. JVM
JVM的垃圾回收算法有哪些 ?
主要有四种:
标记-清除:先标记可回收对象,然后统一清除。缺点:产生内存碎片,效率不高。
标记-复制:把内存分成两块,每次只使用一块,回收时将存活对象复制到另一块,然后清理当前块。优点:无碎片,简单高效。缺点:浪费一半内存。常用于新生代。
标记-整理:标记存活对象,然后把它们往一端移动,清理边界外的内存。优点:无碎片,适合老年代。
分代收集:不是单独算法,而是结合上述算法,把堆分成新生代(复制算法)、老年代(标记-整理或标记-清除),根据不同代的特点用不同算法。
刚才讲的GC机制是在哪些垃圾回收器中使用的 ?
不同的垃圾回收器采用不同算法组合:
Serial/ParNew:新生代用复制算法,老年代用标记-整理(Serial Old)。
Parallel Scavenge:新生代复制,老年代标记-整理(Parallel Old)。
CMS:老年代用标记-清除(并发),所以有碎片问题。
G1:整体看是标记-整理,但局部用复制,可以避免碎片。
ZGC:基于染色指针和读屏障,并发标记-整理,几乎不停顿。
4. Java并发
-
线程池的核心参数有哪些 ?
线程池(ThreadPoolExecutor)有7个核心参数,但主要记前5个:
-
corePoolSize:核心线程数,即使空闲也会保留的线程数。
-
maximumPoolSize:最大线程数,当任务队列满了,可以创建的最大线程数。
-
keepAliveTime:空闲线程存活时间,超过corePoolSize的线程空闲多久会被回收。
-
unit:时间单位。
-
workQueue:任务队列,用于存放等待执行的任务(比如ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue)。
-
threadFactory:线程工厂,用于创建线程,可以自定义名字等。
-
handler:拒绝策略,当线程池和队列都满了,怎么处理新任务(AbortPolicy抛异常、CallerRunsPolicy调用者执行、DiscardPolicy丢弃、DiscardOldestPolicy丢弃最旧任务)。
-
5. Spring / Spring Boot
Spring Boot main函数启动过程了解吗 ?
main函数里就一句
SpringApplication.run(SpringBootApp.class, args);。启动过程大致是:创建SpringApplication实例,然后调用run方法。重点:先初始化监听器、环境,然后创建ApplicationContext,刷新容器(加载所有Bean),最后启动web服务器(如果有)。Spring Boot启动的13个步骤(方法)了解吗 ?
这个有点偏细节,但可以概括一下:SpringApplication的run方法内部调用了一系列方法,比如:
获取并启动监听器(SpringApplicationRunListeners)
准备环境(Environment)
打印Banner
创建ApplicationContext
准备上下文(prepareContext),加载资源、设置环境等
刷新上下文(refreshContext),这是最核心的,里面就是Spring的refresh方法,包含了BeanFactory的初始化、BeanPostProcessor注册、事件发布等
刷新后的处理(afterRefresh),比如调用CommandLineRunner
发布应用已启动事件等等。
具体13个方法可能指SpringApplicationRunListener的回调点,面试不需要全背,但要知道整体流程。
Spring的@Repository和@Reference的区别 ?
@Repository是Spring的注解,用于标记数据访问层(DAO)组件,它会被Spring扫描并注册为Bean,而且它还有翻译持久层异常的功能(将SQLException转换成DataAccessException)。
@Reference是Dubbo的注解,用于引用远程服务(RPC客户端),表示这个字段需要注入一个远程服务的代理。两者完全是两回事,一个用于本地Bean,一个用于远程服务调用。重点:不要混淆了,一个Spring一个Dubbo。
6. MySQL
MySQL有哪些锁 ?
按粒度分:表级锁、行级锁(InnoDB支持)、页级锁(BDB)。
按模式分:共享锁(S锁,读锁)、排他锁(X锁,写锁)。
按算法分:记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock)。
还可以有:意向锁(Intention Lock,表级锁,表示事务打算对行加什么锁)。
重点:InnoDB默认用行锁,结合MVCC实现高并发。MySQL事务隔离级别有哪些?每种隔离级别会产生哪些问题 ?
四种隔离级别(从低到高):
读未提交:可能产生脏读、不可重复读、幻读。
读已提交:解决了脏读,但可能不可重复读、幻读。
可重复读(MySQL默认):解决了脏读、不可重复读,但可能幻读(InnoDB通过间隙锁解决了大部分幻读)。
串行化 :所有问题都解决,但并发最低。
问题解释:
脏读:读到别的事务未提交的数据。
不可重复读:同一事务内,两次读同一数据,结果不一样(因为别的事务修改并提交了)。
幻读 :同一事务内,两次查询记录数不一样(因为别的事务插入或删除了记录)。重点:MySQL可重复读级别通过MVCC避免了快照读的幻读,但当前读可能还有幻读,需加锁解决。
7. Redis
Redis key设置TTL后是如何实现过期删除的 ?
Redis采用两种策略结合:
定期删除:每隔100ms随机抽取一些设置了过期时间的key,检查是否过期,如果过期就删除。
惰性删除 :当客户端访问一个key时,先检查它是否过期,如果过期就删除,返回nil。
如果有些key一直没被访问,定期删除也没抽到,就会积累大量过期key,导致内存不足。这时候Redis会触发内存淘汰策略(比如LRU、LFU等)来清理内存。
Redis为什么快 ?
主要有几个原因:
基于内存:数据全在内存,读写速度快。
单线程模型:避免了多线程上下文切换和竞争开销,而且基于非阻塞I/O多路复用(epoll),可以高效处理大量连接。
数据结构高效:底层数据结构如SDS、跳表、压缩列表等设计巧妙。
没有锁:单线程天然不需要锁,避免了锁竞争。
I/O多路复用:一个线程可以监控多个socket,当有数据时再处理,高效利用CPU。