ArrayList线程不安全因其方法无同步控制,多线程并发add可能引发扩容竞态,导致ArrayIndexOutOfBoundsException、数据丢失或ConcurrentModificationException。

为什么多个线程访问同一个 ArrayList 会出错
因为 ArrayList 不是线程安全的:它的 add()、remove()、get() 等方法内部没有同步控制。当两个线程同时调用 add(),可能触发底层数组扩容,而扩容过程涉及新建数组、复制元素、更新引用——若此时另一个线程正在读或写旧数组,就会出现 ArrayIndexOutOfBoundsException、数据丢失或 ConcurrentModificationException。
常见现象包括:集合大小比预期小、遍历时抛 ConcurrentModificationException、偶发 NullPointerException(因元素未完全写入)。
- 不要在多线程环境中直接共享裸
ArrayList、HashMap、HashSet - 如果只读不写,且初始化完成后不再修改,可用
Collections.unmodifiableList()包装 - 若需读多写少,优先考虑
CopyOnWriteArrayList;但注意它每次写操作都复制整个数组,不适合高频写场景
如何正确使用 synchronized 保护临界区
加锁本身不难,关键在于锁对象是否一致、粒度是否合理。错误做法是给每个方法加 synchronized,却用不同锁(如 this vs getClass()),导致实际没互斥。
典型问题:对共享资源的读写必须使用同一把锁;锁范围应刚好覆盖所有依赖该资源的操作,过大会降低并发度,过小则失去保护。
立即学习“Java免费学习笔记(深入)”;
1.修正BUG站用资源问题,优化程序2.增加关键词搜索3.修改报价4.修正BUG 水印问题5.修改上传方式6.彻底整合论坛,实现一站通7.彻底解决群发垃圾信息问题。注册会员等发垃圾邮件7.彻底解决数据库安全9.修改交易方式.增加网站担保,和直接交易两中10.全站可选生成html.和单独新闻生成html(需要装组建)11. 网站有10中颜色选择适合不同的行业不同的颜色12.修改竞价格排名方式13.修
- 实例方法上加
synchronized,等价于用this作锁;静态方法加synchronized,等价于用MyClass.class作锁 - 若需细粒度控制(例如只锁某个字段),显式使用
synchronized(lockObj),且确保所有访问路径都用同一个lockObj - 避免在同步块内调用外部可重入的方法(如回调、I/O),以防死锁或锁持有时间过长
ReentrantLock 比 synchronized 多了什么
ReentrantLock 提供更灵活的锁控制能力,但不是“更高级就该默认用它”。它适合需要尝试获取锁、可中断等待、或按特定顺序释放多个锁的场景。
容易忽略的点:ReentrantLock 必须手动 lock() 和 unlock(),且 unlock() 必须放在 finally 块中;否则一旦异常跳出,锁永远无法释放。
- 用
lock.tryLock()可避免无限等待,适合超时控制或避免死锁检测 -
lock.lockInterruptibly()允许线程被interrupt()中断,而synchronized不响应中断 - 不要在
ReentrantLock的lock()和unlock()之间跨方法调用,除非你明确掌控整个调用链的异常路径
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 访问共享资源
} finally {
lock.unlock(); // 必须放在这里
}
原子变量(AtomicInteger 等)能替代锁吗
能,但仅限于单个变量的简单操作,比如计数器、状态标志、序列号生成。它们底层依赖 CPU 的 CAS(Compare-And-Swap)指令,无锁且高效。
不能替代锁的场景:多个变量需要保持一致性(如银行转账:从 A 扣款 + 给 B 加款),或操作逻辑复杂(如先查再改再存),这时 CAS 无法保证整体原子性。
-
AtomicInteger的incrementAndGet()是原子的;但i++(即get() + 1再set())不是,必须用原子方法 - 注意
lazySet()和set()的内存语义差异:lazySet()不保证后续读操作立即看到,适合写后极少读的场景(如统计上报) - 不要试图用多个原子变量拼出复合操作的安全性——这通常意味着你需要真正的锁或事务机制









