【面试问题】java字节八股部分

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的垃圾回收算法有哪些

    主要有四种:

    1. 标记-清除:先标记可回收对象,然后统一清除。缺点:产生内存碎片,效率不高。

    2. 标记-复制:把内存分成两块,每次只使用一块,回收时将存活对象复制到另一块,然后清理当前块。优点:无碎片,简单高效。缺点:浪费一半内存。常用于新生代。

    3. 标记-整理:标记存活对象,然后把它们往一端移动,清理边界外的内存。优点:无碎片,适合老年代。

    4. 分代收集:不是单独算法,而是结合上述算法,把堆分成新生代(复制算法)、老年代(标记-整理或标记-清除),根据不同代的特点用不同算法。

  • 刚才讲的GC机制是在哪些垃圾回收器中使用的

    不同的垃圾回收器采用不同算法组合:

    • Serial/ParNew:新生代用复制算法,老年代用标记-整理(Serial Old)。

    • Parallel Scavenge:新生代复制,老年代标记-整理(Parallel Old)。

    • CMS:老年代用标记-清除(并发),所以有碎片问题。

    • G1:整体看是标记-整理,但局部用复制,可以避免碎片。

    • ZGC:基于染色指针和读屏障,并发标记-整理,几乎不停顿。

4. Java并发

  • 线程池的核心参数有哪些

    线程池(ThreadPoolExecutor)有7个核心参数,但主要记前5个:

    1. corePoolSize:核心线程数,即使空闲也会保留的线程数。

    2. maximumPoolSize:最大线程数,当任务队列满了,可以创建的最大线程数。

    3. keepAliveTime:空闲线程存活时间,超过corePoolSize的线程空闲多久会被回收。

    4. unit:时间单位。

    5. workQueue:任务队列,用于存放等待执行的任务(比如ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue)。

    6. threadFactory:线程工厂,用于创建线程,可以自定义名字等。

    7. 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方法内部调用了一系列方法,比如:

    1. 获取并启动监听器(SpringApplicationRunListeners)

    2. 准备环境(Environment)

    3. 打印Banner

    4. 创建ApplicationContext

    5. 准备上下文(prepareContext),加载资源、设置环境等

    6. 刷新上下文(refreshContext),这是最核心的,里面就是Spring的refresh方法,包含了BeanFactory的初始化、BeanPostProcessor注册、事件发布等

    7. 刷新后的处理(afterRefresh),比如调用CommandLineRunner

    8. 发布应用已启动事件等等。

      具体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事务隔离级别有哪些?每种隔离级别会产生哪些问题

    四种隔离级别(从低到高):

    1. 读未提交:可能产生脏读、不可重复读、幻读。

    2. 读已提交:解决了脏读,但可能不可重复读、幻读。

    3. 可重复读(MySQL默认):解决了脏读、不可重复读,但可能幻读(InnoDB通过间隙锁解决了大部分幻读)。

    4. 串行化 :所有问题都解决,但并发最低。

      问题解释:

    • 脏读:读到别的事务未提交的数据。

    • 不可重复读:同一事务内,两次读同一数据,结果不一样(因为别的事务修改并提交了)。

    • 幻读 :同一事务内,两次查询记录数不一样(因为别的事务插入或删除了记录)。重点:MySQL可重复读级别通过MVCC避免了快照读的幻读,但当前读可能还有幻读,需加锁解决。

7. Redis

  • Redis key设置TTL后是如何实现过期删除的

    Redis采用两种策略结合:

    1. 定期删除:每隔100ms随机抽取一些设置了过期时间的key,检查是否过期,如果过期就删除。

    2. 惰性删除 :当客户端访问一个key时,先检查它是否过期,如果过期就删除,返回nil。

      如果有些key一直没被访问,定期删除也没抽到,就会积累大量过期key,导致内存不足。这时候Redis会触发内存淘汰策略(比如LRU、LFU等)来清理内存。

  • Redis为什么快

    主要有几个原因:

    1. 基于内存:数据全在内存,读写速度快。

    2. 单线程模型:避免了多线程上下文切换和竞争开销,而且基于非阻塞I/O多路复用(epoll),可以高效处理大量连接。

    3. 数据结构高效:底层数据结构如SDS、跳表、压缩列表等设计巧妙。

    4. 没有锁:单线程天然不需要锁,避免了锁竞争。

    5. I/O多路复用:一个线程可以监控多个socket,当有数据时再处理,高效利用CPU。

相关推荐
小王不爱笑1321 小时前
Java 对象拷贝(浅拷贝 / 深拷贝)
java·开发语言·python
架构师沉默1 小时前
程序员真的要失业了吗?
java·后端·架构
小王不爱笑1322 小时前
SpringBoot 自动装配深度解析:从底层原理到自定义 starter 实战(含源码断点调试)
java·spring boot·mybatis
森林里的程序猿猿2 小时前
Spring Aop底层源码实现(一)
java·后端·spring
xlp666hub2 小时前
【Linux驱动实战】:字符设备驱动之内核态与用户态数据交互
linux·面试
x_xbx2 小时前
LeetCode:2. 两数相加
算法·leetcode·职场和发展
weixin_456321642 小时前
Java架构设计:Redis持久化方案整合实战
java·开发语言·redis
_日拱一卒2 小时前
LeetCode:最长连续序列
算法·leetcode·职场和发展
攒了一袋星辰2 小时前
SequenceGenerator高并发有序顺序号生成中间件 - 架构设计文档
java·后端·spring·中间件·架构·kafka·maven