Kotlin协程并发场景下,本地图片存储的线程安全问题咨询
问题解答
核心疑问解答
是否需要处理并发读写问题?
是的,你当前的代码确实存在并发读写风险。因为async块默认会继承父协程的调度器,而Repository中的下载操作大概率会切换到IO线程池执行(Firebase Storage的挂起函数通常在IO线程运行)。多个async任务可能在IO线程池的不同线程中同时调用savePicture,而MutableList本身并非线程安全容器,并发添加元素可能导致列表结构损坏、数据丢失或异常。使用ReentrantReadWriteLock是否合理?
是可行方案,但并非最适配Kotlin协程的选择。ReentrantReadWriteLock是线程级锁,会阻塞整个线程;而协程的核心优势是轻量调度,用线程锁会浪费协程的资源优势,更推荐使用协程友好的Mutex来处理并发。
代码优化方案
1. 替换线程锁为协程Mutex(本地存储层)
将ReentrantReadWriteLock替换为Kotlin协程的Mutex,它会挂起协程而非阻塞线程,更符合协程的设计理念:
class LocalPlantPictureStoreImpl : LocalPlantPictureStore { private val mutex = Mutex() private val plantPictureList: MutableList<Pair<String, ByteArray>> = mutableListOf() override suspend fun savePicture(plantPicture: Pair<String, ByteArray>) { mutex.withLock { plantPictureList.add(plantPicture) } } override suspend fun getPicture(plantId: String): Pair<String, ByteArray>? { return mutex.withLock { plantPictureList.firstOrNull { it.first == plantId } } } }
注意:需要将接口方法改为suspend类型,因为Mutex.withLock是挂起函数。
2. 优化Repository逻辑(避免重复下载)
在Repository中先检查本地存储,存在则直接返回,无需重复下载:
suspend fun getProfilePicture(plantId: String): Pair<String, ByteArray> { // 优先从本地缓存获取 localPicture.getPicture(plantId)?.let { return it } // 本地无缓存时才发起下载 val profilePicture = remotePicture.plantProfilePicture( plantPicturePath = profilePicturePath(currentUser, plantId), bufferSize = currentDownloadSize ) val result = Pair(plantId, profilePicture) localPicture.savePicture(result) return result }
3. 修正ViewModel逻辑(实现单任务完成即展示)
你当前的代码会等待所有下载完成后才更新RecyclerView,不符合“每个异步任务完成后展示图片”的需求。修改为每个任务完成后立即更新列表:
// 建议将LiveData替换为StateFlow,更适配协程且线程安全 private val _profilePictureList = MutableStateFlow<List<Pair<String, ByteArray>>>(emptyList()) val profilePictureList: StateFlow<List<Pair<String, ByteArray>>> = _profilePictureList init { viewModelScope.launch { plantList.forEach { plant -> launch { val picture = plantScreenRepository.getProfilePicture(plant.plantId) // 原子性更新列表,避免并发修改问题 _profilePictureList.update { currentList -> currentList + picture } } } } }
使用StateFlow的update方法可以保证列表更新的原子性,无需额外处理并发。
额外说明
- 内存缓存仅在ViewModel生命周期内有效,若进程被杀会丢失数据。如果需要持久化缓存,建议将图片保存到本地文件系统,而非内存列表。
- 屏幕旋转时,ViewModel会被保留,重新创建的Fragment/Activity可以直接从ViewModel的
profilePictureList获取已下载的图片,无需重新发起请求。
内容的提问来源于stack exchange,提问作者No_Name
相关产品推荐
相关产品推荐

