面试集合

java基础的集合

image-20241122003106371

Collection接口的常用方法

1
2
3
4
5
增加:add(E e) addAll(Collection<? extends E> c)
删除:clear() remove(Object o)
修改:
查看:iterator() size()
判断:contains(Object o) equals(Object o) isEmpty()

总结一下:首先是接口不能创建对象,利用实现类创建对象,

集合有一个特点:只能存放引用数据类型的数据,不能是基本数据类型

基本数据类型放入到集合里面会自动装箱。

特别问题

String、StringBuilder、StringBuffer 区别和联系

1、String 类是不可变类、即一旦一个 String 对象被创建后,包含在这个对象中的字符序列是不可改变的,制止这个对象销毁。

2、StringBuffer 类则代表一个字符序列可变的字符串,可以通过 append、insert、reverse、setCharAt、setLength 等方法改变其内容。一旦生成了最终的字符串,调用 toString 方法将其转变为 String

3、JDK1.5 新增了一个 StringBuilder 类和 StringBuffer 相似,构造方法和方法基本相同。不同的是 StrtingBuffer 是线程安全的,而 StringBuilder 是线程不安全的,所以性能略高,通常情况下,创建一个内容可变的字符串,应该优先考虑使用 StringBuilder。

StringBuilder:JDK1.5 开始 效率高 线程不安全

StringBuffer:JDK1.0 开始 效率低 线程安全

并发问题

问题1:购票系统的读写一致性问题

假如你设计一个购票系统,如何设计车票,让车票的读和写是一致的,比如一共有15个车票,买了5张,还剩10张,如何保证准确性

数据库事务 + 悲观锁

通过数据库的事务和悲观锁(如 SELECT FOR UPDATE)来确保同一时间只有一个线程可以修改票数。

实现思路

  • 在购票时,使用事务锁定票数记录,确保其他线程无法同时修改。
  • 读操作可以直接读取数据库中的剩余票数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class TicketService {
private static final String URL = "jdbc:mysql://localhost:3306/ticket_db";
private static final String USER = "root";
private static final String PASSWORD = "password";

public boolean purchaseTicket(int ticketId, int userId) {
Connection conn = null;
try {
conn = DriverManager.getConnection(URL, USER, PASSWORD);
conn.setAutoCommit(false); // 开启事务

// 使用悲观锁锁定票数记录
//SELECT remaining FROM tickets WHERE id = ? FOR UPDATE 是一条 SQL 查询语句,结合了 SELECT 和 FOR UPDATE 子句。它的作用是查询某张票的剩余数量,并在查询时对这条记录加锁,以确保在事务结束前其他事务无法修改这条记录。
String selectSql = "SELECT remaining FROM tickets WHERE id = ? FOR UPDATE";
PreparedStatement selectStmt = conn.prepareStatement(selectSql);
selectStmt.setInt(1, ticketId);
ResultSet rs = selectStmt.executeQuery();

if (rs.next()) {
int remaining = rs.getInt("remaining");
if (remaining > 0) {
// 更新剩余票数
String updateSql = "UPDATE tickets SET remaining = remaining - 1 WHERE id = ?";
PreparedStatement updateStmt = conn.prepareStatement(updateSql);
updateStmt.setInt(1, ticketId);
updateStmt.executeUpdate();

// 插入购票记录
String insertSql = "INSERT INTO purchases (user_id, ticket_id) VALUES (?, ?)";
PreparedStatement insertStmt = conn.prepareStatement(insertSql);
insertStmt.setInt(1, userId);
insertStmt.setInt(2, ticketId);
insertStmt.executeUpdate();

conn.commit(); // 提交事务
return true;
}
}
conn.rollback(); // 回滚事务
return false;
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
return false;
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true);
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}

public int getRemainingTickets(int ticketId) {
try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
PreparedStatement stmt = conn.prepareStatement("SELECT remaining FROM tickets WHERE id = ?")) {
stmt.setInt(1, ticketId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getInt("remaining");
}
} catch (SQLException e) {
e.printStackTrace();
}
return -1; // 表示查询失败
}
}

优点

  • 简单直接,利用数据库的事务和锁机制保证一致性。
  • 适合中小型系统。

缺点

  • 高并发下,悲观锁可能导致性能瓶颈。

乐观锁

通过版本号或时间戳来检测冲突,避免直接加锁。

实现思路

  • 在票数表中增加一个 version 字段。
  • 每次更新票数时,检查版本号是否一致,如果一致则更新,否则重试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class TicketService {
private static final String URL = "jdbc:mysql://localhost:3306/ticket_db";
private static final String USER = "root";
private static final String PASSWORD = "password";

public boolean purchaseTicket(int ticketId, int userId) {
Connection conn = null;
try {
conn = DriverManager.getConnection(URL, USER, PASSWORD);
conn.setAutoCommit(false);

// 查询票数和版本号
String selectSql = "SELECT remaining, version FROM tickets WHERE id = ?";
PreparedStatement selectStmt = conn.prepareStatement(selectSql);
selectStmt.setInt(1, ticketId);
ResultSet rs = selectStmt.executeQuery();

if (rs.next()) {
int remaining = rs.getInt("remaining");
int version = rs.getInt("version");

if (remaining > 0) {
// 更新票数和版本号
String updateSql = "UPDATE tickets SET remaining = remaining - 1, version = version + 1 WHERE id = ? AND version = ?";
PreparedStatement updateStmt = conn.prepareStatement(updateSql);
updateStmt.setInt(1, ticketId);
updateStmt.setInt(2, version);
int rowsUpdated = updateStmt.executeUpdate();

if (rowsUpdated > 0) {
// 插入购票记录
String insertSql = "INSERT INTO purchases (user_id, ticket_id) VALUES (?, ?)";
PreparedStatement insertStmt = conn.prepareStatement(insertSql);
insertStmt.setInt(1, userId);
insertStmt.setInt(2, ticketId);
insertStmt.executeUpdate();

conn.commit();
return true;
}
}
}
conn.rollback();
return false;
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
}
e.printStackTrace();
return false;
} finally {
if (conn != null) {
try {
conn.setAutoCommit(true);
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}

优点

  • 减少锁竞争,提高并发性能。
  • 适合高并发场景。

缺点

  • 需要处理重试逻辑,实现复杂度较高。

分布式锁 + 缓存

在分布式系统中,使用分布式锁(如Redis的RedLock)来保证同一时间只有一个线程可以修改票数,同时使用缓存(如Redis)来加速读操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import redis.clients.jedis.Jedis;

public class TicketService {
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;

public boolean purchaseTicket(int ticketId, int userId) {
Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);
String lockKey = "ticket_lock_" + ticketId;
String lockValue = String.valueOf(System.currentTimeMillis());

try {
// 获取分布式锁
String result = jedis.set(lockKey, lockValue, "NX", "PX", 10000);
if ("OK".equals(result)) {
// 执行业务逻辑
boolean success = doPurchaseTicket(ticketId, userId);
if (success) {
// 更新缓存
jedis.decr("ticket_remaining_" + ticketId);
return true;
}
}
return false;
} finally {
// 释放锁
if (lockValue.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
jedis.close();
}
}

private boolean doPurchaseTicket(int ticketId, int userId) {
// 实际的购票逻辑
return true;
}

public int getRemainingTickets(int ticketId) {
try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT)) {
// 从缓存读取剩余票数
String remaining = jedis.get("ticket_remaining_" + ticketId);
if (remaining != null) {
return Integer.parseInt(remaining);
}
// 缓存未命中,从数据库读取
int remainingTickets = fetchRemainingTicketsFromDB(ticketId);
jedis.set("ticket_remaining_" + ticketId, String.valueOf(remainingTickets));
return remainingTickets;
}
}

private int fetchRemainingTicketsFromDB(int ticketId) {
// 从数据库查询剩余票数
return 10; // 示例值
}
}

总结

  • 悲观锁:适合中小型系统,实现简单,但性能较差。
  • 乐观锁:适合高并发场景,但需要处理重试逻辑。
  • 分布式锁 + 缓存:适合分布式系统,性能较好,但实现复杂度较高。

根据系统的规模和需求,可以选择合适的方案来保证车票的读和写操作的一致性。

问题2:如何看是否有命中,如何知道读取数据是最新的数据

通过查询结果判断

在执行 SELECT ... FOR UPDATE 后,可以通过检查查询结果集来判断是否命中。

缓存中的数据可能会过期或失效,因此需要确保读取的是最新的数据。以下是几种常见的方法:

缓存更新策略

  • 写穿透(Write-Through)
    • 在更新数据库的同时,同步更新缓存。
    • 确保缓存中的数据始终与数据库一致。
  • 写回(Write-Back)

    • 先更新缓存,然后异步更新数据库。
    • 适合写操作频繁的场景,但可能会存在数据不一致的风险。
  • 缓存失效(Cache Invalidation)

    • 在数据更新时,使缓存失效(删除缓存)。
    • 下次读取时,从数据库加载最新数据并重新写入缓存。
  • 缓存过期时间(TTL)

    • 为缓存设置过期时间(Time-To-Live, TTL),确保缓存数据在一定时间后自动失效,从而强制从数据库加载最新数据。
  • 版本控制

    • 为缓存数据添加版本号,每次更新数据时递增版本号,并将版本号存储在缓存中。读取数据时,检查版本号是否匹配。

spring原理

问题1:讲一下spring ioc的实现原理

Spring 的 IOC 实现原理

1. 什么是 IOC(控制反转)?

IOC(Inversion of Control)是一种设计原则,它将对象的创建、依赖注入和生命周期管理交给框架(如 Spring)来处理,而不是由程序员手动控制。IOC 的核心思想是 将控制权从应用程序代码转移到框架

  • 传统方式:程序员手动创建对象并管理依赖关系。
  • IOC 方式:Spring 容器负责创建对象并注入依赖。

2. IOC 的核心组件

Spring 的 IOC 容器主要由以下几个核心组件实现:

  1. BeanFactory:IOC 容器的基础接口,提供了最基本的依赖注入功能。
  2. ApplicationContextBeanFactory 的子接口,提供了更多高级功能(如事件发布、国际化支持等)。
  3. BeanDefinition:用于描述一个 Bean 的元数据(如类名、作用域、依赖关系等)。
  4. BeanPostProcessor:用于在 Bean 初始化前后执行自定义逻辑。
  5. BeanFactoryPostProcessor:用于在 BeanFactory 初始化后修改 Bean 的定义。

3. IOC 的实现原理

Spring 的 IOC 实现原理可以分为以下几个步骤:

3.1 加载配置文件或注解

Spring 容器会加载配置文件(如 applicationContext.xml)或扫描注解(如 @Component@Service 等),获取 Bean 的定义信息。

  • XML 配置

    1
    2
    3
    <bean id="userService" class="com.example.UserService">
    <property name="userDao" ref="userDao"/>
    </bean>
  • 注解配置

    1
    2
    3
    4
    5
    @Service
    public class UserService {
    @Autowired
    private UserDao userDao;
    }

3.2 解析 Bean 定义

Spring 容器会解析配置文件或注解,将每个 Bean 的定义信息封装为 BeanDefinition 对象,并存储在一个 BeanDefinitionRegistry 中。

3.3 实例化 Bean

Spring 容器根据 BeanDefinition 中的信息,通过反射机制实例化 Bean。

  • 单例 Bean:容器启动时就会创建并缓存单例 Bean。
  • 原型 Bean:每次请求时都会创建一个新的 Bean。

3.4 依赖注入

Spring 容器会根据 Bean 的依赖关系,自动将依赖的 Bean 注入到目标 Bean 中。

  • Setter 注入

    1
    2
    3
    4
    5
    6
    public class UserService {
    private UserDao userDao;
    public void setUserDao(UserDao userDao) {
    this.userDao = userDao;
    }
    }
  • 构造器注入

    1
    2
    3
    4
    5
    6
    public class UserService {
    private UserDao userDao;
    public UserService(UserDao userDao) {
    this.userDao = userDao;
    }
    }
  • 字段注入(通过注解)

    1
    2
    @Autowired
    private UserDao userDao;

3.5 初始化 Bean

在 Bean 初始化前后,Spring 容器会调用 BeanPostProcessorpostProcessBeforeInitializationpostProcessAfterInitialization 方法,执行自定义逻辑。

  • 初始化方法:可以通过 @PostConstruct 注解或 init-method 配置指定初始化方法。
  • 销毁方法:可以通过 @PreDestroy 注解或 destroy-method 配置指定销毁方法。

3.6 使用 Bean

应用程序可以通过 ApplicationContextBeanFactory 获取 Bean 并使用。

1
2
3
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean(UserService.class);
userService.doSomething();

4. IOC 的核心机制

4.1 反射机制

Spring 通过反射机制动态创建 Bean 实例并注入依赖。

4.2 工厂模式

Spring 使用工厂模式(BeanFactory)来管理 Bean 的创建和依赖注入。

4.3 依赖查找 vs 依赖注入

  • 依赖查找:应用程序主动从容器中查找依赖(如 context.getBean())。
  • 依赖注入:容器自动将依赖注入到目标 Bean 中。

4.4 生命周期管理

Spring 容器负责管理 Bean 的整个生命周期,包括实例化、初始化、使用和销毁。