导读:本期聚焦于桃子创作的《Java多线程更新共享数组如何保证线程安全?这几种方案你用对了吗》,敬请观看详情。多个线程同时读写同一个数组时,为什么偶尔会出现数据丢失或者数值异常?根源在于类似i++这种操作并非原子性,多个线程交错执行时会互相覆盖对方的修改结果。本文从问题复现入手,分析共享数组在并发场景下的竞态条件成因,然后对比四种主流解决方案:synchronized同步块、原子类AtomicIntegerArray、显式锁ReentrantLock以及不可变快照设计。文章给出每种方案的完整代码示例,分析各自的性能特点与适用场景,比如高并发计数适合原子类数组,读多写少可以考虑读写锁。最后总结了按场景选型的建议,帮助你写出既正确又高效的多线程代码。

共享数组是Java并发编程里最常踩坑的场景之一。多个线程同时对数组的同一个下标执行自增、累加操作,看似代码没问题,实际运行时结果总是比预期小。这是因为a[i]++这样的操作在字节码层面被拆分成读取、加一、写回三步,线程之间随时可能在中间任意一步被打断,最终导致修改被覆盖。本文围绕这一问题,详细分析成因并给出几种可靠的解决方案。

Java多线程更新共享数组如何保证线程安全?这几种方案你用对了吗

先复现问题:不安全的数组自增

我们先用一段最直观的代码复现这个经典问题。定义一个长度为10的int数组,启动8个线程,每个线程对数组的每个元素自增1000次。理论上最终每个元素都应该是8000,但实际运行几乎每次都小于这个值。

public class UnsafeArrayDemo {
    private static final int[] array = new int[10];

    public static void main(String[] args) throws InterruptedException {
        int threadCount = 8;
        Thread[] threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++) {
            threads[i] = new Thread(() -> {
                for (int k = 0; k < 1000; k++) {
                    for (int j = 0; j < 10; j++) {
                        array[j]++; // 非原子操作,存在竞态条件
                    }
                }
            });
            threads[i].start();
        }
        for (Thread t : threads) {
            t.join();
        }
        System.out.println(Arrays.toString(array));
        // 输出大概率不是 [8000, 8000, ...]
    }
}</code>

问题的核心在于竞态条件(Race Condition)。两个线程同时读到旧值,各自加一后先后写回,后写的会覆盖先写的,一次自增就凭空消失了。值得注意的是,即使给数组加上volatile也无法解决,因为volatile只保证可见性和有序性,不保证复合操作的原子性。这是一个非常常见的认知误区。

另外还有一个容易被忽视的细节:可见性问题。如果没有同步措施,一个线程的修改可能长时间对其他线程不可见(由于CPU缓存和工作内存的存在),这会让问题进一步复杂化。所以解决方案必须同时兼顾原子性与可见性。

方案一:synchronized同步块

最直接的办法是用synchronized把修改数组的代码块包起来。同一时刻只有一个线程能进入同步块,既保证了原子性,也顺带保证了可见性。

public class SyncArrayDemo {
    private static final int[] array = new int[10];
    private static final Object lock = new Object();

    public static void increment(int index) {
        synchronized (lock) {
            array[index]++;
        }
    }
}

这种写法的优点是简单直观,JDK内部对synchronized做了大量优化(偏向锁、轻量级锁等),低竞争场景下性能并不差。缺点是锁粒度问题:上面的例子锁住了整个数组,所有线程在更新任何下标时都要竞争同一把锁,高并发下会形成串行化瓶颈。

如果不同下标之间没有逻辑关联,可以细化锁粒度,比如按index % lockCount分段加锁,让不同段的操作并行执行。这种分段锁思想在ConcurrentHashMap的早期实现中也有体现,能显著提升吞吐量,但代码复杂度也随之上升,需要谨慎评估是否值得。

方案二:AtomicIntegerArray原子类数组

JDK的java.util.concurrent.atomic包提供了AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray,专门用于解决数组元素的原子更新问题。它底层基于CAS(Compare And Swap)操作,无锁化实现,高并发下性能优异。

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicArrayDemo {
    private static final AtomicIntegerArray array = new AtomicIntegerArray(10);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int k = 0; k < 10000; k++) {
                array.getAndIncrement(k % 10); // 原子自增
            }
        });
        Thread t2 = new Thread(() -> {
            for (int k = 0; k < 10000; k++) {
                array.addAndGet(k % 10, 2); // 原子增加指定值
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(array); // 结果总是确定的
    }
}

AtomicIntegerArray的优势在于无锁。CAS在硬件层面直接支持,冲突激烈时会自旋重试,不会让线程挂起。对于单纯的计数、累加场景,它几乎是首选方案。不过CAS也有短板:高竞争下自旋次数增多会浪费CPU;此外它只适合单个变量的简单更新,如果一次业务操作需要同时改多个下标且要求整体一致,CAS就无能为力了,这时还得回到锁方案。

还有一个细节值得注意:AtomicIntegerArray内部并不是把每个元素包装成AtomicInteger对象,而是用单个int数组配合偏移量直接操作内存,避免了额外的对象创建开销,这也是它性能好的原因之一。

方案三:显式锁与读写锁

当业务逻辑复杂到需要一段代码整体原子执行,或者需要尝试获取锁、设置超时等高级能力时,可以使用ReentrantLock。它的语义与synchronized类似,但功能更灵活。

import java.util.concurrent.locks.ReentrantLock;

public class LockArrayDemo {
    private static final int[] array = new int[10];
    private static final ReentrantLock lock = new ReentrantLock();

    public static void update(int index, int delta) {
        lock.lock();
        try {
            // 复合操作整体原子执行
            int old = array[index];
            array[index] = old + delta;
        } finally {
            lock.unlock(); // 必须在finally中释放
        }
    }
}

如果场景是读多写少,比如配置数组、路由表,大部分线程只读、偶尔有线程更新,那么ReentrantReadWriteLock更合适。读写锁允许多个读线程同时进入,写线程独占,读吞吐量能成倍提升。不过要注意写锁升级的问题:一个持有读锁的线程不能直接获取写锁,否则会死锁,需要先释放读锁再重新竞争写锁。

对于数组结构经常整体变化的情况,还可以考虑直接换成CopyOnWriteArrayList之类的容器,每次写都复制一份新数组,读操作完全无锁。它适合读远多于写且数据量不大的场景,写频繁则内存开销会难以承受。

如何选择合适的方案

没有万能的方案,选型要看具体场景。下面给出一个简单的对照参考:

场景特征推荐方案理由
简单计数、累加AtomicIntegerArray无锁CAS,高并发性能最好
复合逻辑需整体原子synchronized或ReentrantLock锁保证代码块整体互斥
读多写少读写锁、CopyOnWrite容器读并行化,提升吞吐
低并发、追求简单synchronized代码最简单,JIT优化后性能足够

除了选型,还有几条实践建议值得遵守。第一,尽量缩小同步范围,只锁住真正需要互斥的语句,锁内的IO操作、耗时计算都会放大竞争。第二,明确锁保护的对象,最好用一个专门的lock对象,而不是直接锁数组或this,避免外部代码意外锁同一对象造成死锁。第三,写完并发代码后一定要用多线程压测验证,比如使用jcstress工具或简单的循环断言,很多并发bug在低负载下根本不会暴露。

总结一下,共享数组的线程安全问题本质上是对原子性和可见性的理解。volatile解决不了复合操作的竞态条件;单变量更新优先考虑原子类数组;跨多个下标的一致性操作用锁;读多写少用读写锁或写时复制容器。理解了这些原理,再面对更复杂的并发数据结构时也就有了判断依据。

Java多线程线程安全并发编程修改时间:2026-09-09 09:12:54

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260909/53313.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。