Spring Web服务器异步调用共享资源同步失效问题排查
咱们先梳理下你遇到的情况:你有个SpringBoot Web服务,里面有个操作单例状态的路由,为了保证多调用方访问时的线程安全,你给set()方法加了Kotlin的@Synchronized注解,同时之前还尝试过用ReentrantLock,相关代码还没移除。但现在日志里出现了多次连续的“锁定”日志,之后才批量出现“释放”日志,和你期望的“锁定-释放”交替出现的情况不符。
你的核心代码
@Synchronized public fun <T> set(t: T) { val incomingState = State.from(t) if(incomingState == innerState){ return } change(incomingState) } @Synchronized private fun change(newState: State){ this.stateAccessLock.lock() SystemLog.logResourceLocking("HomeState", isLocked = true) SystemLog.logHomeEvent("Changing HomeState from '${innerState.name}' to '${newState.name}'") try { Home.INSTANCE.EXTENSION.broadcastStateChange( innerState?.name, newState.name ) } finally { innerState = newState Home.save() stateAccessLock.unlock() SystemLog.logResourceLocking("HomeState", isLocked = false) } } // ReentrantLock相关代码 private var stateAccessLock = ReentrantLock() @Autowired fun HomeState(@Qualifier("stateAccessLock") stateAccessLock: ReentrantLock) { this.stateAccessLock = stateAccessLock }
日志异常情况
实际日志(不符合预期):
[2020-10-09T16:07:01.709] LOCK: Resource locked
[2020-10-09T16:07:01.710] LOCK: Resource locked
[2020-10-09T16:07:01.725] LOCK: Resource locked
[2020-10-09T16:07:01.727] LOCK: Resource locked
[2020-10-09T16:07:01.818] LOCK: Resource unlocked
[2020-10-09T16:07:01.828] LOCK: Resource unlocked
[2020-10-09T16:07:01.829] LOCK: Resource unlocked
[2020-10-09T16:07:01.839] LOCK: Resource unlocked
期望日志:
[2020-10-09T16:07:01.709] LOCK: Resource locked
[2020-10-09T16:07:01.710] LOCK: Resource unlocked
[2020-10-09T16:07:01.725] LOCK: Resource locked
[2020-10-09T16:07:01.727] LOCK: Resource unlocked
...
问题根源:同步机制混用 + 锁实例不唯一
你忽略了两个关键问题:
同时混用两种独立的同步机制
Kotlin的@Synchronized注解本质是给方法加了对象级别的内置锁(和Java的synchronized方法一致),而你又在change()里手动加了ReentrantLock的独占锁。这两种锁是完全独立的,互相不会感知对方的状态。- 如果
HomeState是多实例的,每个实例的@Synchronized锁是各自独立的,多个线程可以同时进入不同实例的set()方法,然后各自去抢自己的ReentrantLock(如果锁实例不唯一的话),导致多个线程同时成功锁定,日志连续输出“locked”。
- 如果
ReentrantLock实例可能不是单例
从你的注入代码来看,如果Spring容器中stateAccessLock这个bean没有被定义为单例(比如误设为原型scope),那么每个HomeState实例都会拿到一个独立的ReentrantLock对象。这种情况下,多个线程操作不同HomeState实例时,各自的锁互不干扰,自然能同时锁定,产生连续的锁定日志。
解决方案:二选一,统一同步机制
你只需要选择一种同步机制即可,不需要同时使用:
方案1:只用@Synchronized注解(移除ReentrantLock)
既然@Synchronized已经能保证同一时间只有一个线程进入set()和change()方法(只要HomeState是单例),就不需要额外的ReentrantLock了。修改后代码如下:
@Synchronized public fun <T> set(t: T) { val incomingState = State.from(t) if(incomingState == innerState){ return } change(incomingState) } @Synchronized private fun change(newState: State){ SystemLog.logResourceLocking("HomeState", isLocked = true) SystemLog.logHomeEvent("Changing HomeState from '${innerState.name}' to '${newState.name}'") try { Home.INSTANCE.EXTENSION.broadcastStateChange( innerState?.name, newState.name ) } finally { innerState = newState Home.save() SystemLog.logResourceLocking("HomeState", isLocked = false) } }
方案2:只用ReentrantLock(移除@Synchronized)
确保stateAccessLock是单例bean,然后在set()方法中统一加锁,避免重复加锁:
// 确保stateAccessLock是单例bean @Bean("stateAccessLock") fun stateAccessLock(): ReentrantLock { return ReentrantLock() } // HomeState类代码 private lateinit var stateAccessLock: ReentrantLock @Autowired fun HomeState(@Qualifier("stateAccessLock") stateAccessLock: ReentrantLock) { this.stateAccessLock = stateAccessLock } public fun <T> set(t: T) { stateAccessLock.lock() try { val incomingState = State.from(t) if(incomingState == innerState){ return } change(incomingState) } finally { stateAccessLock.unlock() } } private fun change(newState: State){ SystemLog.logResourceLocking("HomeState", isLocked = true) SystemLog.logHomeEvent("Changing HomeState from '${innerState.name}' to '${newState.name}'") try { Home.INSTANCE.EXTENSION.broadcastStateChange( innerState?.name, newState.name ) } finally { innerState = newState Home.save() SystemLog.logResourceLocking("HomeState", isLocked = false) } }
内容的提问来源于stack exchange,提问作者Blatzheim

