自定义同步缓冲保存类竞态条件成因咨询
竞态条件的成因及触发场景
代码复现
public abstract class SynchronizedBufferedSaver<E> { private final int MAX_BUFFER_SIZE; private Set<E> savingBuffer; public SynchronizedBufferedSaver(final int max_buffer_size) { MAX_BUFFER_SIZE = max_buffer_size; savingBuffer = ConcurrentHashMap.newKeySet((int) (max_buffer_size * 1.5)); } public void save(@NonNull final E entity) { copyToCache(entity); flushCache(false); } abstract protected void persist(@NonNull final Set<E> cache); public void flushCache(final boolean force) { synchronized (savingBuffer) { if (!savingBuffer.isEmpty() && (force || savingBuffer.size() >= MAX_BUFFER_SIZE)) { final Set<E> tmp = savingBuffer; savingBuffer = ConcurrentHashMap.newKeySet((int) (MAX_BUFFER_SIZE * 1.5)); persist(tmp); } } } private void copyToCache(@NonNull final E entity) { savingBuffer.add(entity); } }
核心成因
- 锁对象动态变更,丧失互斥性:
savingBuffer是非final字段,在flushCache的同步块内会被替换为新的集合实例。当线程A进入同步块并完成savingBuffer的替换后,后续线程进入同步块时,会以新的savingBuffer实例作为锁——这意味着不同线程可能持有不同的锁,完全失去了同步互斥的作用,多个线程可以同时操作不同的集合实例。 copyToCache无锁保护,且内存可见性缺失:copyToCache直接调用savingBuffer.add,既没有被锁同步,savingBuffer也没有用volatile修饰。这会导致两个问题:一是多个线程可以同时往同一个集合里加元素;二是线程之间对savingBuffer引用的更新不保证可见,部分线程可能一直持有旧的集合实例引用,持续往旧实例里加元素。
典型触发场景
假设MAX_BUFFER_SIZE=2,多线程并发执行save方法:
- 线程A、B先后执行
copyToCache,往初始集合S1中添加E1、E2,此时S1大小达到阈值。 - 线程B率先进入
flushCache的同步块,拿到S1的锁,将tmp赋值为S1,然后把savingBuffer替换为新集合S2,随后调用persist(S1)(开始处理E1、E2)。 - 线程C此时执行
copyToCache,由于内存可见性问题,它的savingBuffer引用仍指向S1,于是往S1中添加E3。 - 此时如果有线程D执行
flushCache(true)(强制刷入),它的savingBuffer引用可能也还指向S1,于是拿到S1的锁,发现S1不为空,就会调用persist(S1)——此时S1里包含E1、E2、E3,导致E1、E2被重复刷入持久化。
另一种场景:
- 线程A进入
flushCache,拿到S1的锁,替换savingBuffer为S2,但还没开始执行persist(S1)。 - 线程E执行
copyToCache,由于内存可见性问题,它的savingBuffer还是S1,往S1里添加E4。 - 线程A执行
persist(S1),此时S1包含初始元素+E4,而如果后续线程E再次触发flushCache,它可能依然以S1为锁进入同步块,再次将S1传入persist,造成重复。
内容的提问来源于stack exchange,提问作者Rafael Lima
相关产品推荐
相关产品推荐

