Java面试突击案例

wen java案例 1

Java面试突击案例大全

我为你整理了一套高频面试题 + 实战案例,分为基础、进阶、框架、系统设计四个层次,每个案例都附有代码和解析。

Java面试突击案例


Java基础高频面试题

HashMap底层原理(必考)

// 面试官:说一下HashMap的put流程
// 完整回答思路:
public class HashMapDemo {
    public static void main(String[] args) {
        HashMap<String, Integer> map = new HashMap<>();
        map.put("key1", 1);
        // put流程:
        // 1. 计算hash:hash(key) = (h = key.hashCode()) ^ (h >>> 16)
        // 2. 如果table为null,调用resize()初始化,默认容量16
        // 3. 计算索引:(n - 1) & hash
        // 4. 如果该位置为null,直接插入
        // 5. 不为null,判断是链表还是红黑树
        //    - 链表长度 >= 8 且数组长度 >= 64 → 转红黑树
        //    - 链表长度 >= 8 但数组长度 < 64 → 先扩容
        // 6. 插入后size > threshold(容量*0.75),扩容为2倍
    }
}

并发编程核心(必考)

// 问题:如何实现线程安全的单例?为什么推荐枚举方式?
public class SingletonDemo {
    // 方式1:双重检查锁(DCL)- 需要volatile防止指令重排序
    private static volatile SingletonDemo instance;
    private SingletonDemo() {}
    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo(); // 不是原子操作!
                }
            }
        }
        return instance;
    }
    // 方式2:静态内部类 - 线程安全且懒加载
    private static class Holder {
        private static final SingletonDemo INSTANCE = new SingletonDemo();
    }
    public static SingletonDemo getInstance2() {
        return Holder.INSTANCE;
    }
    // 方式3:枚举(最推荐) - 天然防止反射和序列化破坏
    // 详见下方
}

内存模型与垃圾回收(必考)

// 面试官:Java对象在内存中如何分配?
// 回答要点:
public class JVMDemo {
    // 对象创建流程:
    // 1. 类加载检查 → 2. 分配内存(指针碰撞 / 空闲列表)
    // 3. 内存空间初始化(零值) → 4. 设置对象头
    // 5. 执行<init>方法
    // GC Roots有哪些?
    // ① 虚拟机栈中引用的对象(局部变量)
    // ② 方法区中静态属性引用的对象(static变量)
    // ③ 方法区中常量引用的对象(final常量)
    // ④ 本地方法栈中JNI引用的对象
    // 判断对象存活:可达性分析(从GC Roots出发,不可达即回收)
    // 不是引用计数法!(循环引用问题)
}

高效实战用例

场景1:千万级用户订单系统 - 并发处理

public class OrderService {
    // 问题:如何保证订单号不重复?
    // 方案1:数据库自增ID(分库分表时需使用雪花算法)
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    // 雪花算法生成唯一ID
    public long generateOrderId() {
        // Snowflake算法:1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号
        // 每秒可生成409.6万个ID
        return SnowflakeIdGenerator.getInstance().nextId();
    }
    // 防止超卖:分布式锁+乐观锁
    public boolean createOrder(Long userId, Long productId, int count) {
        // 方式1:Redis分布式锁(面试重点)
        String lockKey = "lock:product:" + productId;
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
        if (!locked) {
            return false; // 获取锁失败
        }
        try {
            // 检查库存(乐观锁:version字段)
            // update t_product set stock = stock - #{count}
            // where id = #{productId} and stock >= #{count}
            // 扣减库存
            // 创建订单
            return true;
        } finally {
            // 释放锁(必须用Lua脚本保证原子性)
            redisTemplate.delete(lockKey);
        }
    }
}

场景2:超时订单自动关闭 - 延迟队列

// 问题:如何高效处理超时未支付订单?
// 方案1:Redis过期监听(简单但不可靠)
// 方案2:消息队列延迟消息(推荐)
@Component
public class OrderTimeoutConsumer {
    // RabbitMQ延迟队列实现
    // 原理:消息先进入死信交换机,延迟时间后再投递到业务队列
    @RabbitListener(queues = "order.timeout.queue")
    public void handleTimeoutOrder(Order order) {
        // 1. 检查订单状态
        Order dbOrder = orderMapper.selectById(order.getId());
        if (dbOrder.getStatus() == OrderStatus.UNPAID) {
            // 2. 自动取消订单
            orderMapper.updateStatus(order.getId(), OrderStatus.CANCELLED);
            // 3. 恢复库存
            stockService.restoreStock(order.getProductId(), order.getCount());
        }
    }
}

场景3:缓存穿透、击穿、雪崩的解决方案

@Service
public class CacheService {
    // 1. 缓存穿透:查询不存在的数据
    // 解决方案:布隆过滤器
    public String getProductInfo(Long productId) {
        // 先查布隆过滤器
        if (!bloomFilter.mightContain("product:" + productId)) {
            return null; // 肯定不存在,直接返回
        }
        // 2. 缓存空值(额外方案)
        String value = redisTemplate.opsForValue().get("product:" + productId);
        if (value == null) {
            value = productMapper.selectById(productId);
            if (value == null) {
                // 缓存空值,设置较短过期时间(如5分钟)
                redisTemplate.opsForValue().set("product:" + productId, "null", 300, TimeUnit.SECONDS);
                return null;
            }
        }
        return value;
    }
    // 2. 缓存击穿:热点key过期瞬间大量请求
    // 解决方案:互斥锁
    @Transactional
    public String getHotProduct(Long productId) {
        String key = "product:" + productId;
        String value = redisTemplate.opsForValue().get(key);
        if (value != null) return value;
        // 加锁查询数据库(只让一个请求去查库)
        String lockKey = "lock:" + key;
        Boolean locked = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
        if (locked) {
            try {
                value = productMapper.selectById(productId);
                redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
                return value;
            } finally {
                redisTemplate.delete(lockKey);
            }
        } else {
            // 其他请求等等,然后重试
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            return getHotProduct(productId); // 递归重试
        }
    }
    // 3. 缓存雪崩
    // 解决方案:
    // - 设置不同的过期时间:baseTime + random
    // - 缓存数据永不过期,后台定时刷新
    public void setProductCache(Long productId, String value) {
        int timeout = 3600 + new Random().nextInt(600); // 随机60-70分钟
        redisTemplate.opsForValue().set("product:" + productId, value, timeout, TimeUnit.SECONDS);
    }
}

分布式系统设计经典案例

案例:设计一个短链接系统

// 需求:长链接转为短链接,支持跳转
// 核心设计:
public class ShortUrlService {
    // 1. 生成短码:使用发号器(Redis INCR)或雪花算法
    public String createShortUrl(String longUrl) {
        // 发号器生成唯一ID
        long id = redisTemplate.opsForValue().increment("short:url:id");
        // 将ID转为62进制(0-9a-zA-Z)
        String shortCode = base62Encode(id); // 10进制→62进制
        // 存储映射关系
        redisTemplate.opsForValue().set("short:" + shortCode, longUrl);
        return "https://t.cn/" + shortCode;
    }
    // 2. 跳转:redis找不到则查数据库
    public String getLongUrl(String shortCode) {
        String longUrl = redisTemplate.opsForValue().get("short:" + shortCode);
        if (longUrl == null) {
            // 查数据库,然后回填缓存
            UrlMap urlMap = urlMapper.selectByShortCode(shortCode);
            longUrl = urlMap.getLongUrl();
            redisTemplate.opsForValue().set("short:" + shortCode, longUrl, 3600, TimeUnit.SECONDS);
        }
        return longUrl;
    }
    // 3. 10进制转62进制
    private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private String base62Encode(long num) {
        StringBuilder sb = new StringBuilder();
        while (num > 0) {
            sb.append(CHARS.charAt((int)(num % 62)));
            num /= 62;
        }
        return sb.reverse().toString();
    }
}

线上故障排查实战

案例:CPU 100% 排查

# 1. 查看进程
top -c  # 找到PID
# 2. 查看该进程的线程
top -Hp PID
# 3. 将线程ID转为16进制
printf "%x\n" 线程ID
# 4. 查看线程堆栈
jstack PID | grep 线程ID十六进制 -A 50
# 5. 常见原因:
# - 死循环
# - 频繁GC
# - 线程阻塞(等待锁/IO)

代码示例 - 死循环排查:

// 一旦线上CPU飙升,很可能有类似代码
public class CPUTrouble {
    // 死循环示例
    public void deadLoop() {
        while (true) {
            // 业务逻辑
        }
    }
    // 正确的做法:加上超时控制
    public void correctLoop() {
        long start = System.currentTimeMillis();
        while (System.currentTimeMillis() - start < 5000) { // 最多执行5秒
            // 业务逻辑
        }
    }
}

最常问的第三问:Spring事务失效场景

@Service
public class TransactionDemo {
    // 场景1:同类内部调用 - 事务失效
    public void outerMethod() {
        this.innerMethod(); // 这是直接调用,不走代理
    }
    @Transactional
    public void innerMethod() {
        // 数据库操作
    }
    // 解决:注入自身代理对象
    @Autowired
    private TransactionDemo self;
    public void outerMethod2() {
        self.innerMethod(); // 通过代理调用
    }
    // 场景2:final方法(CGLIB代理无法重写)或private方法
    // 场景3:异常被捕获,未抛出
    @Transactional
    public void methodCatch() {
        try {
            int result = 1 / 0;
        } catch (Exception e) {
            // 异常被捕获,事务不会回滚
        }
    }
    // 解决方法:抛异常
    @Transactional
    public void methodThrow() {
        try {
            int result = 1 / 0;
        } catch (Exception e) {
            throw new RuntimeException("异常", e); // 抛出RuntimeException
        }
    }
    // 场景4:传播行为不当
    // Propagation.NOT_SUPPORTED 不会启动事务
    // 场景5:配置了错误的回滚条件
    // @Transactional(rollbackFor = Exception.class) // 必须指定,否则只回滚RuntimeException
    // 场景6:数据库引擎不支持事务(MyISAM)
}

线程池面试必问

public class ThreadPoolDemo {
    // 问题1:为什么不用Executors.newFixedThreadPool?
    // 1. 无界队列(LinkedBlockingQueue),OOM风险
    // 2. 线程数量固定,无法灵活伸缩
    // 正确方式:
    @Bean
    public ThreadPoolExecutor businessThreadPool() {
        return new ThreadPoolExecutor(
            10,     // 核心线程数
            20,     // 最大线程数
            60,     // 空闲时间
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100), // 有界队列,容量100
            new NamedThreadFactory("business-"),
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );
    }
    // 问题2:线程池执行流程
    // 提交任务 → 核心线程数不足?→ 创建核心线程执行
    //          → 核心线程满了?→ 放入队列
    //          → 队列满了?→ 创建非核心线程
    //          → 最大线程满了?→ 执行拒绝策略
    // 问题3:常见拒绝策略
    // ThreadPoolExecutor.AbortPolicy:直接抛异常(默认)
    // DiscardPolicy:丢弃任务不抛异常
    // DiscardOldestPolicy:丢弃最旧的任务(队列第一个)
    // CallerRunsPolicy:调用者线程执行(不会丢任务,但会阻塞调用者)
}

MySQL索引优化实战

-- 最左匹配原则示例
CREATE INDEX idx_user_name_age ON t_user(name, age);
-- 下面的SQL会用到索引
SELECT * FROM t_user WHERE name = '张三';       -- 用索引
SELECT * FROM t_user WHERE name = '张三' AND age = 18;  -- 用索引
-- 下面的SQL不会用到索引
SELECT * FROM t_user WHERE age = 18;  -- 跳过第一个字段,不走索引
-- 覆盖索引优化
-- 原SQL(回表查询慢)
SELECT * FROM t_order WHERE user_id = 100;  
-- 优化为覆盖索引
CREATE INDEX idx_order_user_status ON t_order(user_id, status);  
SELECT user_id, status FROM t_order WHERE user_id = 100;  -- 只查索引列,避免回表
-- 分页优化
-- 深度分页(慢)
SELECT * FROM t_order ORDER BY id LIMIT 100000, 20;
-- 优化:延迟关联
SELECT * FROM t_order 
WHERE id > (SELECT id FROM t_order ORDER BY id LIMIT 100000, 1) 
ORDER BY id LIMIT 20;

消息队列可靠性保障

@Component
public class MQProducter {
    // 问题:如何保证消息不丢失?
    // 发送端:
    @Autowired
    private RabbitTemplate rabbitTemplate;
    public void sendOrderMessage(Order order) {
        CorrelationData correlationData = new CorrelationData(order.getId());
        // 开启confirm模式:发送成功后回调确认
        rabbitTemplate.convertAndSend(
            "order.exchange", 
            "order.route", 
            order, 
            correlationData
        );
    }
    @PostConstruct
    public void init() {
        // Confirm确认回调
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (!ack) {
                // 消息发送失败,重试或入库
                System.err.println("消息发送失败: " + cause);
            }
        });
    }
    // 消费端:手动ACK
    @RabbitListener(queues = "order.queue")
    public void consume(Order order, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
        try {
            // 处理业务
            orderService.handleOrder(order);
            // 手动确认
            channel.basicAck(deliveryTag, false);
        } catch (Exception e) {
            // 处理失败,要求重试或进入死信队列
            channel.basicNack(deliveryTag, false, true); // 重新入队
        }
    }
}

系统设计真题

设计一个秒杀系统

// 架构层次:
// 1. 前端层:按钮置灰 + 答题 + 限流(滑动窗口)
// 2. 网关层:负载均衡 + 限流
// 3. 应用层:分布式锁 + 库存预减
// 4. 数据层:Redis库存预热 + 异步下单
@Service
public class SeckillService {
    // Redis预减库存
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    // 商品数量预先加载到Redis
    public void preLoadStock(Long productId, int count) {
        redisTemplate.opsForValue().set("seckill:stock:" + productId, String.valueOf(count));
    }
    // 秒杀入口
    public Result seckill(Long userId, Long productId) {
        // 1. 限流:redis计数器
        // 2. 预减库存(原子操作)
        Long stock = redisTemplate.opsForValue()
            .decrement("seckill:stock:" + productId);
        if (stock < 0) {
            // 库存不足,恢复数量
            redisTemplate.opsForValue().increment("seckill:stock:" + productId);
            return Result.error("已售罄");
        }
        // 3. 发送MQ异步处理订单
        SeckillMessage message = new SeckillMessage(userId, productId);
        rocketMQTemplate.convertAndSend("seckill-topic", message);
        return Result.success("排队中");
    }
    // MQ消费者:真正落库处理
    @RocketMQMessageListener(topic = "seckill-topic", consumerGroup = "seckill-group")
    public class SeckillConsumer implements RocketMQListener<SeckillMessage> {
        @Override
        public void onMessage(SeckillMessage message) {
            // 1. 创建订单(唯一索引防重复)
            // 2. 扣减数据库库存
            // UPDATE stock SET count = count - 1 WHERE product_id = ? AND count > 0
            // 3. 发送成功通知
        }
    }
}

消息队列必答 - Kafka/RabbitMQ对比

// 面试官:说一下Kafka和RabbitMQ的区别和选型
面试要点:
1. 吞吐量:Kafka(百万级/秒) > RabbitMQ(万级/秒)
2. 消息顺序:Kafka分区内有序;RabbitMQ需要特殊配置
3. 消息确认:Kafka offset机制;RabbitMQ手动ACK
4. 延迟:Kafka毫秒级;RabbitMQ微秒级
5. 功能特性:RabbitMQ功能丰富(延迟、死信、优先级);Kafka简单
6. 扩展性:Kafka水平扩展好(分区机制)
选型建议:
- 需要高吞吐、日志收集 → Kafka
- 业务复杂、需精确路由 → RabbitMQ
- 延迟敏感业务 → RabbitMQ

十一、JVM调优实战

// 线上OOM排查案例
public class OOMAnalyze {
    // 出现OOM后:
    // 命令添加参数:
    // -XX:+HeapDumpOnOutOfMemoryError
    // -XX:HeapDumpPath=/tmp/heapdump.hprof
    // 分析堆转储文件
    // jmap -dump:format=b,file=/tmp/heap.hprof PID
    // 使用MAT分析:
    // - 找大对象
    // - 找重复字符串
    // - 找未释放的集合
    // 常见OOM场景:
    // 1. 堆内存不足(对象过多)
    // 2. 元空间不足(动态生成类)
    // 3. 直接内存(NIO使用不当)
}

十二、Redis高级特性必答

// 面试官:Redis中热Key和大Key如何解决?
// 热Key问题:
// 1. 本地缓存:JVM层缓存热点数据
// 2. 多副本:多个Redis副本分担读压力
// 3. 读写分离:master写,slave读
// 大Key问题:
// 1. 拆分:将大对象拆分成多个key
// 2. 压缩:序列化时使用压缩算法(protobuf、snappy)
// 3. 分批:删除大集合时用scan而非del
// Sorted Set底层实现(树和跳跃表的选择原因)
// 综合对比:跳跃表实现简单、查找效率低,但范围查找效率高
// 因此Redis选择跳表 + 哈希表
// 一致性哈希在Redis中的应用:
// 一致性哈希解决分布式缓存中节点的动态增删问题
// 虚拟节点解决数据倾斜问题

十三、Spring Boot + MyBatis-Plus经典案例

// 实现多租户数据隔离
@Configuration
public class TenantConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(
            new TenantLineHandler() {
                @Override
                public Expression getTenantId() {
                    // 从上下文获取租户ID
                    return new LongValue(TenantContext.getTenantId());
                }
                @Override
                public String getTenantIdColumn() {
                    return "tenant_id";  // 自动拼接tenant_id条件
                }
            }
        ));
        return interceptor;
    }
}
// 分页插件
@Configuration
public class PaginationConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

十四、面试中要掌握的框架原理

Spring Boot自动配置原理

面试回答思路:
1. @SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan
2. @EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class)
3. 加载 META-INF/spring.factories 中所有自动配置类
4. 结合 @ConditionalOnXxx 条件判断哪些配置生效
5. 自动配置类中通过 @Bean 生成对应的 Bean

Spring循环依赖解决方案

// 面试句重点:
// 三级缓存:
// 一级缓存: singletonObjects  存储完整Bean
// 二级缓存: earlySingletonObjects  存储半成品Bean(未填充属性)
// 三级缓存: singletonFactories  存储ObjectFactory(lambda表达式)
@Lazy  // 最简单的解决方案:懒加载打破循环
public class A {
    @Autowired
    private B b;
}
// 循环依赖无法解决的情况(完全类比构造函数循环依赖)

MyBatis一级缓存和二级缓存

一级缓存:SqlSession级别,默认开启,同一个SqlSession查询会命中缓存
二级缓存:namespace级别,需要配置,且实体类需要序列化
注意:多表关联查询时缓存可能导致脏数据

十五、面试中的高并发实战案例

案例:热搜榜设计

// 问题:如何实现热搜排行榜?
// 方案:利用Redis的ZSet结构
public class HotSearchService {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    private static final String HOT_KEY = "hot:search";
    // 1. 搜索时增加热度
    public void addHot(String keyword) {
        redisTemplate.opsForZSet().incrementScore(HOT_KEY, keyword, 1.0);
    }
    // 2. 获取Top10热搜
    public List<String> getTop10() {
        Set<String> top = redisTemplate.opsForZSet()
            .reverseRange(HOT_KEY, 0, 9);  // 分数从高到低
        return new ArrayList<>(top);
    }
    // 3. 热度随时间衰减
    // 可以使用定时任务:每小时把所有词的分数乘以0.9
    // 4. 防止某个词在某个时间段被重复刷
    // 可加上单日搜索量限制
}

面试拿到Offer的关键

要素 重要性 怎么准备
基础功底 多线程、集合、JVM原理必须烂熟于心
框架原理 能讲出Spring/MyBatis核心原理
系统设计 面试前准备5-8个业务场景
实战经验 独立完成至少2-3个项目,深入理解每一行代码
思路清晰 多练习,用金字塔原理回答,先说结论再说原因

面试禁忌

  1. 不要背答案:结合自己的项目去阐述,哪怕是实习项目
  2. 不要说"我不会":尽量说"这块我了解的是…"
  3. 不要一直强调不会的:引导面试官问你擅长的领域
  4. 不要紧张:提前做几轮模拟面试

最后的考前面试清单

  • [ ] 把HashMap、ConcurrentHashMap源码看一遍
  • [ ] 能默写线程池的7个参数并解释执行流程
  • [ ] Spring IOC/AOP源码核心流程过一遍
  • [ ] MySQL索引结构(B+树)能画出来并讲清楚
  • [ ] Redis数据类型及典型应用场景
  • [ ] 准备2-3个系统设计案例(秒杀、分布式锁、限流)

抱歉,评论功能暂时关闭!