混合使用不同锁是否可行?多Map数据同步锁机制问询
锁机制混合使用问题解答
问题背景
我要实现一个简易数据库,包含3个Map:一个存储核心数据,另外两个分别作为“按用户”和“按设备”的索引,用来提升两种维度的过滤效率。由于几乎所有方法都会操作2个或全部Map,ConcurrentMap无法满足多Map间的一致性要求,因此需要手动做同步控制。
核心疑问
- 能否混合使用
synchronized(this)与synchronized(userIdIndices)这类不同锁对象?还是必须全程使用this作为唯一锁对象? - 当线程进入
addDataPoint()方法的synchronized(this)代码块时,会不会顺带锁住getUserIdIndicesSize()中synchronized(userIdIndices)的锁(类似层级锁)?还是这两个锁完全独立,允许并发访问?
期望目标
实现如下同步规则:
- 线程在执行增删改数据时,其他增删改或过滤操作必须等待,不能并行执行;
- 但执行
filterByDeviceId()这类过滤操作时,允许并发执行getUserIdIndicesSize()这类单一索引的查询操作,因此希望通过混合锁实现这种细粒度控制。
代码示例
@Component class DatapointStore { private val dataPoints = HashMap<DatapointKey, Double>() private val userIdIndices = HashMap<Long, TreeSet<DatapointKey>>() private val deviceIdIndices = HashMap<Long, TreeSet<DatapointKey>>() fun getDataPointsSize() = synchronized(dataPoints) { dataPoints.size } fun getUserIdIndicesSize() = synchronized(userIdIndices) { userIdIndices.size } fun getDeviceIdIndicesSize() = synchronized(deviceIdIndices) { deviceIdIndices.size } fun addDataPoint(dataPoint: DatapointRequest) { synchronized(this) { ... } } fun filterByUserId(userId: Long): List<DatapointRequest> { synchronized(this) { ... } } fun filterByDeviceId(deviceId: Long): List<DatapointRequest> { synchronized(this) { ... } } fun deleteByUserId(userId: Long) { synchronized(this) { ... } } fun deleteByDeviceId(deviceId: Long) { synchronized(this) { ... } } }
解答
锁的独立性说明
synchronized(this)和synchronized(userIdIndices)是完全独立的锁,彼此没有任何关联:
- 持有
this锁的线程,不会自动获得userIdIndices或其他Map对象的锁;反过来,持有某个Map锁的线程,也不会影响this锁的获取。 - 你当前的代码存在一致性隐患:比如
addDataPoint()持有this锁修改userIdIndices时,getUserIdIndicesSize()可以同时获取userIdIndices锁读取数据,此时读到的是未完成修改的中间状态,会破坏数据一致性。
混合锁的可行性与正确实现
你完全可以混合使用不同锁,但必须严格遵循操作涉及的所有Map,都要持有对应锁的原则,同时注意锁的获取顺序避免死锁,结合你的需求,调整方案如下:
- 增删改操作(add/delete系列):这类操作会修改全部3个Map,必须持有全局锁(比如
this),确保同一时间只有一个线程能执行这类操作,避免多Map间的数据不一致。 - 过滤操作(filter系列):这类操作会读取核心数据Map和对应的索引Map,同样需要持有全局锁,保证读取过程中没有增删改操作修改数据,确保返回结果的一致性。
- 单一Map的只读查询(getXXXSize系列):这类操作仅访问单个Map,只需持有该Map的锁即可。这样当
filterByDeviceId()持有全局锁时,只要它不操作userIdIndices,getUserIdIndicesSize()就能获取userIdIndices锁并发执行,正好满足你的期望。
关键注意事项
- 固定锁获取顺序:如果某个方法需要同时持有多个锁(比如同时操作
dataPoints和userIdIndices),必须按固定顺序获取(比如先获取dataPoints锁,再获取userIdIndices锁),否则可能引发死锁。 - 避免锁内耗时操作:在
synchronized代码块中不要执行IO、复杂计算等耗时操作,否则会严重降低并发性能。 - 可选替代方案:如果原生
synchronized的灵活性不够,可考虑使用ReentrantLock,它支持锁的公平性设置、可中断获取等特性,但锁的独立性原则和上述方案一致。
内容的提问来源于stack exchange,提问作者Peeve
相关产品推荐
相关产品推荐

