面试题答案
一键面试可能出现的一致性问题
- 并发修改问题:在多线程环境下,多个线程同时对集合进行添加、删除等修改操作,可能导致数据不一致。例如,一个线程正在遍历集合,另一个线程删除了集合中的元素,可能引发
ConcurrentModificationException
,并且遍历结果可能不准确。 - 数据丢失更新:当多个线程同时读取集合中的数据,然后基于读取的值进行修改并写回时,可能会发生数据丢失更新。例如,两个线程读取同一个计数器的值为10,各自加1后写回,最终结果为11而不是预期的12。
- 读脏数据:在某些情况下,一个线程对集合的修改尚未完全完成,另一个线程就开始读取该集合,可能读到部分修改的数据,即脏数据。
解决方案
- 使用
Collections.synchronizedXxx
方法:通过Collections.synchronizedList
、Collections.synchronizedSet
等方法将普通集合包装成线程安全集合。例如:
List<String> list = new ArrayList<>();
List<String> synchronizedList = Collections.synchronizedList(list);
- **优点**:实现简单,不需要对现有代码结构进行大幅度调整,在单 JVM 环境下有较好的效果。
- **缺点**:性能较低,因为它是基于全局锁的机制,所有的读写操作都需要竞争同一把锁,在高并发场景下,锁竞争会成为性能瓶颈。
2. 使用 ConcurrentHashMap
等线程安全集合类:ConcurrentHashMap
采用分段锁机制,允许多个线程同时访问不同的段,提高了并发性能。例如:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
- **优点**:在高并发环境下性能较好,读操作基本无锁,写操作通过分段锁机制,减少了锁竞争。
- **缺点**:与普通 `HashMap` 相比,实现较为复杂,内存占用可能稍高。并且,`ConcurrentHashMap` 的迭代器是弱一致性的,在迭代过程中集合结构的变化可能不会立刻反映到迭代器上。
3. 使用分布式锁:在分布式系统中,可以使用如 Redis 分布式锁或 ZooKeeper 分布式锁。例如,使用 Redis 实现分布式锁:
// 伪代码
Jedis jedis = new Jedis("localhost");
String lockKey = "collection_lock";
String requestId = UUID.randomUUID().toString();
while (!jedis.set(lockKey, requestId, "NX", "EX", 10).equals("OK")) {
Thread.sleep(100);
}
try {
// 对集合进行操作
} finally {
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}
}
- **优点**:能有效保证分布式环境下集合操作的一致性,适用于跨 JVM 的多线程场景。
- **缺点**:引入了额外的分布式组件,增加了系统的复杂性和维护成本。并且分布式锁可能存在死锁、锁超时等问题,需要仔细处理。
4. 使用事务机制:如果集合操作可以放在数据库事务中,利用数据库的事务特性来保证一致性。例如,将集合数据存储在数据库表中,使用 JDBC 的事务操作:
Connection conn = DriverManager.getConnection(url, username, password);
try {
conn.setAutoCommit(false);
// 执行数据库操作,对应集合的添加、删除等
conn.commit();
} catch (SQLException e) {
conn.rollback();
} finally {
conn.setAutoCommit(true);
conn.close();
}
- **优点**:能保证强一致性,利用了数据库成熟的事务管理机制。
- **缺点**:性能相对较低,尤其是在高并发场景下,数据库的事务处理能力可能成为瓶颈。并且,与数据库的交互增加了系统的延迟,不适用于对性能要求极高的场景。