使用lazy{}引发类型转换异常及HashMap值丢失问题排查
核心问题
你的UseCaseProvider出现的null类型转换异常和HashMap值丢失,本质是**lazy的线程安全局限性**+HashMap非线程安全组合导致的并发竞态条件问题。
具体原因拆解
lazy的同步不覆盖map操作的竞态
Kotlin默认的lazy(LazyThreadSafetyMode.SYNCHRONIZED)仅保证UseCase实例只会被初始化一次,但无法保证get()方法中「检查map是否存在key → 初始化lazy实例 → 写入map」这一整套流程的原子性。
当多个线程同时调用get<ClearFiltersUseCase>()时,可能出现:- 线程A通过
containsKey检查(map中无对应key),进入分支准备初始化lazy实例; - 线程B同时通过
containsKey检查(此时线程A还没完成map写入),也进入同一分支; - 虽然lazy保证了实例唯一,但两个线程同时执行
register()(即HashMap.put()),而HashMap的put操作是非线程安全的,并发写入会导致内部哈希表结构损坏,后续读取时可能出现key存在但返回null的情况,最终触发强转异常。
- 线程A通过
HashMap本身的非线程安全特性HashMap在并发场景下的扩容、写入操作会出现数据丢失、结构错乱等问题,即使是同一key的重复写入,也可能因为并发修改导致后续读取失败,返回null。对比无崩溃的实现逻辑
另一模块在Provider构造时直接实例化所有UseCase并写入map,这一过程是在DI注入的单线程流程中完成的,没有并发操作map的场景,因此不会出现结构损坏或值丢失问题。
修复方案
方案一:改用线程安全Map+原子化初始化(推荐)
替换HashMap为ConcurrentHashMap,并利用其computeIfAbsent方法实现原子化的检查-初始化-写入逻辑,彻底消除竞态条件:
class UseCaseProvider(repository: Repository) { // 替换为线程安全的ConcurrentHashMap private val useCaseMap = ConcurrentHashMap<Class<*>, UseCase<*, *>>() internal inline <reified T: UseCase<*, *>> get(): T { return useCaseMap.computeIfAbsent(T::class.java) { when(T::class) { ClearFiltersUseCase::class -> ClearFiltersUseCase(repository) // 其他UseCase的实例化逻辑 else -> throw IllegalStateException("UseCase not registered") } } as T } }
computeIfAbsent是原子操作,保证同一key只会被初始化一次,且ConcurrentHashMap的所有操作都是线程安全的,彻底避免并发问题。
方案二:保留lazy但添加全局锁
如果必须保留lazy初始化,可以在get()方法中添加同步锁,保证检查-注册流程的原子性,同时替换map为线程安全实现:
class UseCaseProvider(repository: Repository) { private val useCaseMap = ConcurrentHashMap<Class<*>, UseCase<*, *>>() private val clearFiltersUseCase by lazy { ClearFiltersUseCase(repository) } // 其他lazy声明... // 全局锁对象 private val lock = Any() internal inline <reified T: UseCase<*, *>> get(): T { synchronized(lock) { return useCaseMap.computeIfAbsent(T::class.java) { when(T::class) { ClearFiltersUseCase::class -> clearFiltersUseCase // 其他lazy属性 else -> throw IllegalStateException("UseCase not registered") } } as T } } }
内容的提问来源于stack exchange,提问作者JustSightseeing

