JAVA面试题经典十问,面试必问,附带详细答案解析
在面试中,常常遇到一些经典且易出错的问题,尤其是高并发、缓存、框架应用等模块。今天,我总结了十个必问的JAVA面试题,覆盖高并发超买超卖、索引、SpringBoot接口注解、Mybatis二级缓存、Redis和RabbitMQ应用场景、本地缓存框架Caffeine等核心内容。每个问题都附带详细解析、案例代码和应用场景,协助大家在面试中游刃有余,提升实战能力。文章内容充实,条理清晰,提议收藏反复阅读!
问题一:高并发场景下,如何防止商品超买和超卖?
问题描述:在电商系统中,多个用户同时抢购商品时,可能导致库存扣减错误,出现超买(库存扣成负数)或超卖(实际库存不足但订单生成)。请解释如何解决这一问题。
为什么易出错:许多开发者只思考单机锁(如synchronized),忽略分布式环境下的并发问题,或者对乐观锁、悲观锁的理解不够深入,导致在实际高并发场景中性能低下或数据不一致。
详细答案解析:
超买和超卖的本质是并发条件下对共享资源(库存)的竞争。解决方案包括:
- 悲观锁:假设并发冲突会发生,提前加锁,如数据库行锁(SELECT … FOR UPDATE)或Java中的synchronized、ReentrantLock。适用于写操作多的场景,但可能引发性能瓶颈。
- 乐观锁:假设冲突较少,通过版本号或时间戳实现,如数据库的CAS(Compare And Swap)操作。适用于读多写少的场景,性能更高。
- 分布式锁:在分布式系统中,使用Redis或ZooKeeper实现锁,确保跨服务节点的互斥访问。
案例代码:
以下是一个使用数据库乐观锁的Java示例(基于SpringBoot和MyBatis)。假设商品表有id、stock和version字段。
// 商品实体类
public class Product {
private Long id;
private Integer stock;
private Integer version;
// getter和setter
}
// Service层方法:扣减库存
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
public boolean deductStock(Long productId) {
// 循环重试,模拟乐观锁机制
for (int i = 0; i < 3; i++) { // 最多重试3次
Product product = productMapper.selectById(productId);
if (product.getStock() <= 0) {
return false; // 库存不足
}
int updated = productMapper.updateStockWithVersion(productId, product.getVersion());
if (updated > 0) {
return true; // 更新成功
}
// 版本冲突,重试
}
return false; // 更新失败
}
}
// MyBatis Mapper接口
@Mapper
public interface ProductMapper {
Product selectById(Long id);
// 乐观锁更新:版本号匹配时才更新
int updateStockWithVersion(@Param("id") Long id, @Param("version") Integer version);
}
对应的MyBatis XML映射文件:
<update id="updateStockWithVersion">
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = #{id} AND version = #{version}
</update>
应用场景描述:
在电商秒杀活动中,例如“双11”抢购,每秒可能有数万请求。使用乐观锁可以避免数据库死锁,提升并发性能。如果冲突频繁,可以结合Redis分布式锁(如Redisson)或消息队列(如RabbitMQ)进行流量削峰,确保库存准确性。
问题二:数据库索引的原理和优化策略是什么?
问题描述:索引是数据库性能优化的关键,请解释索引的底层原理(如B+树),并说明如何设计高效索引以避免全表扫描。
为什么易出错:开发者常滥用索引,导致写性能下降,或忽略复合索引的最左前缀原则,造成索引失效。
详细答案解析:
- 索引原理:大多数数据库(如MySQL)使用B+树索引,其特点包括有序存储、叶子节点链表连接(利于范围查询)、非叶子节点只存键值(减少磁盘I/O)。B+树相比B树,更适合磁盘存储,由于所有数据都在叶子节点,查询稳定。
- 优化策略:
- 选择合适的列:为高频查询条件(WHERE、JOIN、ORDER BY)创建索引。
- 避免索引失效:注意最左前缀原则(复合索引中,必须从最左列开始使用),避免在索引列上使用函数或类型转换。
- 索引类型:主键索引、唯一索引、普通索引。覆盖索引(索引包含所有查询字段)可以减少回表操作。
- 监控与调整:使用EXPLAIN分析查询计划,避免Using filesort或Using temporary。
案例代码:
假设有一个用户表user,字段包括id(主键)、name、age、city。
创建复合索引优化查询:
-- 创建复合索引(name, age)
CREATE INDEX idx_name_age ON user(name, age);
-- 有效查询:使用索引
EXPLAIN SELECT * FROM user WHERE name = '张三' AND age = 25;
-- 可能失效的查询:未使用最左前缀
EXPLAIN SELECT * FROM user WHERE age = 25; -- 索引失效,全表扫描
应用场景描述:
在社交App中,用户常常根据姓名和年龄搜索好友。复合索引(name, age)可以加速这类查询。如果查询条件只包含age,索引失效,需思考单独为age建索引或调整查询顺序。
问题三:SpringBoot中常用接口注解有哪些?它们的作用和区别是什么?
问题描述:SpringBoot简化了Web开发,请列举常用接口注解(如@RestController、@RequestMapping),并解释它们的应用场景。
为什么易出错:初学者容易混淆@Controller和@RestController,或错误使用@RequestParam和@PathVariable,导致接口调用失败。
详细答案解析:
- @Controller vs @RestController:
- @Controller:用于MVC模式,返回视图名称,需配合@ResponseBody返回JSON。
- @RestController:组合了@Controller和@ResponseBody,直接返回JSON数据,适用于RESTful API。
- @RequestMapping:定义请求映射,可指定HTTP方法(GET、POST等)、路径、参数等。
- @GetMapping/@PostMapping:简化版@RequestMapping,专用于特定HTTP方法。
- @RequestParam vs @PathVariable:
- @RequestParam:用于查询参数,如/user?id=1。
- @PathVariable:用于路径变量,如/user/1。
- @RequestBody:接收JSON请求体,自动反序列化为对象。
案例代码:
一个简单的用户管理API示例:
@RestController
@RequestMapping("/api/user")
public class UserController {
// GET请求:路径变量
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id);
}
// POST请求:请求体参数
@PostMapping("/create")
public String createUser(@RequestBody User user) {
userService.save(user);
return "用户创建成功";
}
// GET请求:查询参数
@GetMapping("/list")
public List<User> listUsers(@RequestParam String city) {
return userService.findByCity(city);
}
}
应用场景描述:
在微服务架构中,前端通过RESTful API与后端交互。例如,用户管理模块使用@RestController提供JSON接口,@PathVariable用于资源标识(如用户ID),@RequestParam用于过滤条件(如城市查询)。正确使用注解能提高代码可读性和维护性。
问题四:Mybatis二级缓存是如何工作的?使用时有哪些注意事项?
问题描述:Mybatis提供一级缓存(SqlSession级别)和二级缓存(Mapper级别),请解释二级缓存的机制,并说明如何配置和避免脏读。
为什么易出错:二级缓存默认关闭,配置不当可能导致数据不一致(如多表关联更新时缓存未清除),或引发序列化问题。
详细答案解析:
- 工作原理:二级缓存是Mapper级别的缓存,多个SqlSession共享。当查询执行时,结果会被缓存;更新操作(INSERT、UPDATE、DELETE)会清除缓存,确保数据一致性。
- 配置步骤:
- 在Mybatis配置文件中开启二级缓存:<setting value=”true”/>。
- 在Mapper XML中添加<cache/>标签,可自定义缓存策略(如LRU)。
- 注意事项:
- 实体类必须实现Serializable接口,由于缓存数据可能序列化到磁盘。
- 避免在多表关联场景使用,由于更新关联表时,缓存可能不会自动清除。
- 可以通过<cache-ref/>共享缓存,但需谨慎使用。
案例代码:
配置Mybatis二级缓存的示例:
在mybatis-config.xml中:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
在Mapper XML文件(如UserMapper.xml)中:
<!-- 开启二级缓存,使用LRU策略 -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
<select id="findById" resultType="User" useCache="true">
SELECT * FROM user WHERE id = #{id}
</select>
<update id="updateUser" parameterType="User">
UPDATE user SET name = #{name} WHERE id = #{id}
</update>
应用场景描述:
在查询频繁、数据变化少的系统中,如新闻App的文章详情页,二级缓存可以显著减少数据库压力。但如果有频繁更新(如用户评论),需谨慎使用,或通过手动清除缓存(flushCache=”true”)避免脏读。
问题五:Redis在项目中的常见应用场景有哪些?
问题描述:Redis作为内存数据库,高性能且支持多种数据结构,请列举其典型应用场景,并说明优势。
为什么易出错:开发者可能过度依赖Redis,忽略数据持久化问题,或错误选择数据结构(如用String存储列表),导致性能不佳。
详细答案解析:
Redis支持String、List、Set、Hash、ZSet等数据结构,常见应用场景包括:
- 缓存:缓存热点数据(如用户信息、商品详情),减少数据库访问,提升响应速度。
- 会话存储(Session Storage):分布式系统中存储用户会话,实现多服务器共享。
- 分布式锁:通过SETNX命令实现互斥访问,解决高并发问题。
- 消息队列:使用List或Pub/Sub实现异步任务处理。
- 计数器/排行榜:利用ZSet实现实时排名,如电商销量榜。
案例代码:
使用RedisTemplate在SpringBoot中实现缓存和分布式锁:
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 缓存用户信息
public User getUserById(Long id) {
String key = "user:" + id;
User user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
user = userService.findById(id); // 从数据库查询
redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30)); // 缓存30分钟
}
return user;
}
// 分布式锁示例
public boolean tryLock(String key, String value, long expireTime) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(expireTime));
return Boolean.TRUE.equals(result);
}
public void unlock(String key) {
redisTemplate.delete(key);
}
}
应用场景描述:
在在线教育平台中,Redis可用于缓存课程信息(减少数据库压力)、存储用户学习进度(Session存储)、实现秒杀系统的分布式锁(防止超买)。例如,使用ZSet维护课程热度排行榜,实时更新访问量。
问题六:RabbitMQ如何保证消息的可靠传输?应用在哪些业务场景?
问题描述:消息队列是解耦系统的利器,请解释RabbitMQ如何防止消息丢失,并举例说明其应用场景。
为什么易出错:开发者可能忽略消息确认机制(ACK)或持久化设置,导致消息在传输过程中丢失。
详细答案解析:
RabbitMQ通过以下机制保证可靠性:
- 生产者确认(Publisher Confirm):生产者发送消息后,等待Broker确认,确保消息到达交换机。
- 消息持久化:将消息和队列设置为持久化(durable=true),防止服务器重启后消息丢失。
- 消费者确认(Consumer ACK):消费者处理消息后手动发送ACK,Broker才删除消息;如果处理失败,可重试或进入死信队列。
- 事务机制:但性能较差,一般推荐使用Confirm模式。
应用场景:
- 异步处理:如用户注册后发送邮件,避免阻塞主流程。
- 流量削峰:在高并发场景(如秒杀)中,将请求缓冲到队列,逐步处理。
- 系统解耦:微服务间通过消息通信,降低依赖。
案例代码:
SpringBoot整合RabbitMQ,实现可靠消息发送和消费:
// 配置类,定义队列和交换机
@Configuration
public class RabbitConfig {
@Bean
public Queue orderQueue() {
return new Queue("order.queue", true); // 持久化队列
}
}
// 生产者
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void createOrder(Order order) {
// 发送消息,设置Confirm回调
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) {
// 消息发送失败,记录日志或重试
System.out.println("消息发送失败: " + cause);
}
});
rabbitTemplate.convertAndSend("order.queue", order);
}
}
// 消费者
@Component
public class OrderConsumer {
@RabbitListener(queues = "order.queue")
public void processOrder(Order order, Channel channel, Message message) throws IOException {
try {
// 处理订单逻辑
orderService.process(order);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); // 手动ACK
} catch (Exception e) {
// 处理失败,重试或进入死信队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
应用场景描述:
在电商订单系统中,用户下单后,通过RabbitMQ异步通知库存服务和物流服务。即使某个服务暂时不可用,消息也不会丢失,确保最终一致性。例如,秒杀场景中,订单请求先进入队列,再逐步处理,避免系统崩溃。
问题七:本地缓存框架Caffeine的使用方法和优势是什么?
问题描述:Caffeine是高性能的Java本地缓存库,请说明其基本用法,并对比Guava Cache的优势。
为什么易出错:开发者可能忽略缓存过期策略或内存管理,导致内存泄漏或缓存雪崩。
详细答案解析:
Caffeine基于Window TinyLFU算法,提供高命中率和低内存占用。优势包括:
- 高性能:读写操作接近O(1),适合高并发场景。
- 灵活配置:支持基于大小、时间、引用等的缓存淘汰策略。
- 异步刷新:自动刷新缓存值,避免过期数据。
常用API:Cache接口用于手动管理,LoadingCache自动加载数据。
案例代码:
在SpringBoot项目中集成Caffeine:
第一添加依赖(Maven):
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
Java配置和使用:
@Configuration
public class CacheConfig {
@Bean
public Cache<String, User> userCache() {
return Caffeine.newBuilder()
.maximumSize(1000) // 最大缓存条目
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
.build();
}
}
@Service
public class UserService {
@Autowired
private Cache<String, User> userCache;
public User getUser(String id) {
return userCache.get(id, key -> {
// 缓存未命中时,从数据库加载
return userRepository.findById(id);
});
}
}
应用场景描述:
在网关或API服务中,Caffeine可用于缓存频繁访问的配置数据或用户令牌。例如,在微服务架构中,每个实例本地缓存路由信息,减少对配置中心的请求,提升响应速度。相比Guava Cache,Caffeine在高并发下性能更优,且内存效率更高。
问题八:Java中synchronized和ReentrantLock的区别是什么?如何选择?
问题描述:synchronized和ReentrantLock都是Java中的锁机制,请对比它们的特性和适用场景。
为什么易出错:开发者可能误用锁导致死锁,或忽略ReentrantLock的高级功能(如公平锁、条件变量)。
详细答案解析:
- synchronized:
- 关键字,JVM内置锁,自动获取和释放。
- 支持可重入,但不支持中断和超时。
- 简单易用,但功能有限。
- ReentrantLock:
- 类,需手动加锁和解锁,灵活性高。
- 支持公平锁、非公平锁、可中断、超时尝试等。
- 需在finally块中解锁,避免死锁。
选择原则:简单场景用synchronized,复杂并发控制(如需要条件变量)用ReentrantLock。
案例代码:
使用ReentrantLock实现线程安全的计数器:
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock(); // 可重入锁
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 确保解锁
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
对比synchronized版本:
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
应用场景描述:
在银行转账系统中,synchronized适合简单的账户余额操作,而ReentrantLock可用于复杂交易,如需要超时回滚(tryLock)或条件等待(Condition)。例如,转账时如果目标账户被锁定,ReentrantLock可以设置超时,避免无限等待。
问题九:SpringBoot中如何通过注解实现接口权限控制?
问题描述:在Web应用中,接口权限控制是常见需求,请说明如何使用Spring Security或自定义注解实现。
为什么易出错:开发者可能混淆角色和权限,或忽略注解的继承性,导致权限验证漏洞。
详细答案解析:
Spring Security提供注解如@PreAuthorize、@PostAuthorize进行方法级权限控制。
- @PreAuthorize:在方法执行前验证权限,常用SpEL表达式。
- @Secured:基于角色验证,但功能较弱。
- 自定义注解:结合AOP实现灵活控制,例如@RequiresPermission。
案例代码:
使用Spring Security注解的示例:
第一添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置SecurityConfig:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启注解
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 配置用户和角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}123").roles("ADMIN")
.and()
.withUser("user").password("{noop}123").roles("USER");
}
}
在Controller中使用注解:
@RestController
public class AdminController {
// 只有ADMIN角色可访问
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin/data")
public String getAdminData() {
return "管理员数据";
}
// 自定义权限表达式
@PreAuthorize("hasAuthority('READ')")
@GetMapping("/user/data")
public String getUserData() {
return "用户数据";
}
}
自定义注解示例:
// 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasPermission(#id, 'READ')")
public @interface RequiresReadPermission {
}
// 在Controller中使用
@RestController
public class CustomController {
@RequiresReadPermission
@GetMapping("/resource/{id}")
public String getResource(@PathVariable Long id) {
return "资源数据";
}
}
应用场景描述:
在企业OA系统中,不同角色(如管理员、普通员工)访问不同接口。使用@PreAuthorize可以精细控制权限,例如只有管理员才能删除用户。自定义注解简化了重复配置,提升代码可读性。
问题十:Mybatis中一级缓存和二级缓存的详细对比?
问题描述:Mybatis的缓存机制是性能优化重点,请详细比较一级缓存和二级缓存的Scope、生命周期和适用场景。
为什么易出错:开发者可能混淆两者范围,导致缓存数据不一致,或错误配置二级缓存引发脏读。
详细答案解析:
- 一级缓存:
- Scope:SqlSession级别,同一个SqlSession内共享。
- 生命周期:随SqlSession创建而创建,关闭而清除。
- 默认开启,无法关闭。
- 适用场景:短时操作,如单个请求中的多次一样查询。
- 二级缓存:
- Scope:Mapper级别,多个SqlSession共享。
- 生命周期:应用级别,需手动配置。
- 通过<cache/>标签开启,可自定义策略。
- 适用场景:跨请求的共享数据,但需注意数据一致性。
对比总结:一级缓存轻量但范围小,二级缓存范围大但需管理。
案例代码:
演示一级缓存行为:
// 在同一个SqlSession中,查询一样数据只执行一次SQL
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.findById(1L); // 执行SQL
User user2 = mapper.findById(1L); // 从一级缓存获取,不执行SQL
sqlSession.close(); // 缓存清除
二级缓存配置见问题四。
应用场景描述:
在Web应用中,用户在一次会话中多次查看个人资料,一级缓存可减少数据库访问。而文章详情等公共数据,使用二级缓存跨会话共享,提升整体性能。但更新操作需及时清除缓存,避免脏读。
结语
通过以上十个问题的解析,我们覆盖了JAVA面试中的高并发、数据库、框架集成和缓存等核心模块。每个问题都结合案例代码和应用场景,协助大家深入理解。面试时,不仅要记住答案,更要理解背后的原理和适用条件。提议多动手实践,积累项目经验。如果觉得有用,请收藏分享,关注我获取更多技术干货!如有疑问,欢迎在评论区讨论。

💗感谢分享
向你学习👍
大神💪
加油👏