非synchronized方法能否与synchronized方法交错执行引发竞态?
问题结论
这种写法一定存在竞态条件隐患,vmlens测试未发现异常不代表代码是线程安全的,本质是对synchronized的锁机制理解有偏差,且并发问题的触发本身存在概率性。
synchronized的实际锁规则
很多开发者对synchronized的认知存在根本误区:它锁的不是方法内部操作的共享资源(也就是你持有的List),而是调用方法的对象实例本身(静态同步方法锁的是对应类的Class对象):
- 线程调用对象的
synchronized修饰的实例方法时,必须先抢到该对象的内置监视器锁(monitor),方法执行完成(无论正常返回还是抛出异常)才会释放锁 - 没有加
synchronized修饰的方法,执行时完全不需要争抢这把内置锁,不会被同步方法阻塞,随时可以并发执行
代码中的具体竞态点
你的实现里两个方法操作同一个共享List,但只有加元素的方法加了锁,删除方法完全在锁保护范围外,会出现两类并发问题:
- 容器层面的数据损坏:如果持有的是
ArrayList这类非线程安全容器,两个线程同时修改容器内部的存储数组、size计数等状态,会触发元素丢失、数组索引越界,JDK7及更早版本中甚至会触发ArrayList扩容死链,直接占满CPU - 业务逻辑的原子性破坏:就算你用
Vector、CopyOnWriteArrayList这类自带线程安全的容器,依然会出问题——"添加元素+处理元素"是一个不可拆分的复合逻辑,但删除操作可以在元素刚添加完、还没处理的时机插队执行,直接把待处理元素删掉,导致后续逻辑拿到空值、处理错数据,甚至抛出运行时异常
举个典型的错误时序:
- 线程A抢到对象锁,进入同步方法,刚完成List的add操作,还没开始执行元素的后续处理
- 线程B不需要等待任何锁,直接调用非同步的delete方法,把线程A刚添加的元素删除
- 线程A继续执行后续处理逻辑,发现目标元素不存在,触发逻辑错误或运行时异常
为什么测试工具没有检测到异常
并发问题的触发本身是概率事件,测试未复现不代表问题不存在:
- 并发冲突的触发和测试时的线程数、CPU核心数、JVM版本、操作系统线程调度策略、JIT指令重排时机强相关,如果测试用例的调用频率低、运行时长短,完全可能跑几十万次都碰不到一次冲突
- vmlens这类工具主要检测字节码层面的未同步内存访问,对于"复合操作被插队"这类业务逻辑层面的竞态,工具本身没有能力识别
注意:永远不要靠"测试没测出并发问题"判断线程安全性。只要代码没有遵循Java内存模型的同步规范,哪怕99.99%的场景运行正常,剩下0.01%的故障触发时基本都是难以复现、难以排查的线上偶发问题。
正确修复方案
要彻底消除竞态,核心原则是所有访问共享List的操作,都必须被同一把锁保护:
- 最简便的改法是给delete方法也加上
synchronized修饰,让add、delete两个方法争抢同一把对象实例锁,保证add+后续处理的整个流程执行完之前,delete操作无法插队 - 如果不想占用对象本身的内置锁,可以单独定义一个私有不可变的锁对象:
private final Object lock = new Object();,两个方法内部都用synchronized(lock)包裹所有操作List的逻辑 - 也可以使用
ReentrantLock等显式锁实现,逻辑和内置锁一致,要保证两个方法操作List前都先加锁,操作完成后在finally块中释放锁
内容的提问来源于stack exchange,提问作者Peter
相关产品推荐
相关产品推荐

