Skip to content

Java 并发的 Bug

编写正确的并发程序是一件极困难的事情,并发程序的 Bug 往往会诡异地出现,然后又诡异地消失,很难重现,也很难追踪。

CPU 缓存导致的问题

单核时代,所有的线程都是在一颗 CPU 上执行,所有线程都是操作同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说是可见的。

多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。

下面的代码,每执行一次 summationCount() 方法,都会循环 10000 次 count+=1 操作。在 calc() 方法中我们创建了两个线程,每个线程调用一次 summationCount() 方法,直觉告诉我们执行 calc() 方法得到的结果应该是20000,因为在单线程里调用两次 summationCount() 方法,count 的值就是 20000,但实际上 calc() 的执行结果是个 10000 到 20000 之间的随机数。

Java
package org.foundation;

public class FoundationApplication {
    public static void main(String[] args) throws InterruptedException {
        long result = Test.calc();
        System.out.println("最终 count 的值是: " + result);
    }
}

class Test {
    private static long count = 0;
    private void summationCount() {
        int idx = 0;
        while(idx++ < 10000) {
            count += 1;
        }
    }
    public static long calc() throws InterruptedException {
        final Test test = new Test();
        // 创建两个线程,执行summationCount()操作
        Thread th1 = new Thread(test::summationCount);
        Thread th2 = new Thread(test::summationCount);
        // 启动两个线程
        th1.start();
        th2.start();
        // 等待两个线程执行结束
        th1.join();
        th2.join();
        return count;
    }
}

假设线程 A 和线程 B 同时开始执行,那么第一次都会将 count=0 读到各自的 CPU 缓存里,执行完 count+=1 之后,各自 CPU 缓存里的值都是 1,同时写入内存后,我们会发现内存中是 1,而不是我们期望的 2。之后由于各自的 CPU 缓存里都有了 count 的值,两个线程都是基于 CPU 缓存里的 count 值来计算,所以导致最终 count 的值都是小于 20000 的。

解决方法

使用 synchronized

Java
package org.foundation;

public class FoundationApplication {
    public static void main(String[] args) throws InterruptedException {
        long result = SynchronizedTest.calc();
        System.out.println("最终 count 的值是: " + result);
        // 这里输出 
        // 最终 count 的值是: 20000
    }
}

class SynchronizedTest {
    private static long count = 0;

    private synchronized void summationCount() {
        int idx = 0;
        while (idx++ < 10000) {
            count += 1;
        }
    }

    public static long calc() throws InterruptedException {
        final SynchronizedTest test = new SynchronizedTest();
        Thread th1 = new Thread(test::summationCount);
        Thread th2 = new Thread(test::summationCount);

        th1.start();
        th2.start();

        th1.join();
        th2.join();

        return count;
    }
}

优点:简单直观,确保 summationCount 的整个方法是线程安全的。
缺点:粒度较大,可能影响性能。


使用 AtomicLong

Java
package org.foundation;

import java.util.concurrent.atomic.AtomicLong;

public class FoundationApplication {
    public static void main(String[] args) throws InterruptedException {
        long result = AtomicLongTest.calc();
        System.out.println("最终 count 的值是: " + result);
        // 这里输出 
        // 最终 count 的值是: 20000
    }
}

class AtomicLongTest {
    private static final AtomicLong count = new AtomicLong(0);

    private void summationCount() {
        int idx = 0;
        while (idx++ < 10000) {
            count.incrementAndGet();
        }
    }

    public static long calc() throws InterruptedException {
        final AtomicLongTest test = new AtomicLongTest();
        Thread th1 = new Thread(test::summationCount);
        Thread th2 = new Thread(test::summationCount);

        th1.start();
        th2.start();

        th1.join();
        th2.join();

        return count.get();
    }
}

优点:高效,AtomicLong 提供原子操作,性能优于 synchronized
缺点:适用于简单的计数场景,不适合复杂逻辑。


使用 ReentrantLock

Java
package org.foundation;

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;

public class FoundationApplication {
    public static void main(String[] args) throws InterruptedException {
        long result = ReentrantLockTest.calc();
        System.out.println("最终 count 的值是: " + result);
        // 这里输出 
        // 最终 count 的值是: 20000
    }
}

class ReentrantLockTest {
    private static long count = 0;
    private static final ReentrantLock lock = new ReentrantLock();

    private void summationCount() {
        int idx = 0;
        while (idx++ < 10000) {
            lock.lock();
            try {
                count += 1;
            } finally {
                lock.unlock();
            }
        }
    }

    public static long calc() throws InterruptedException {
        final ReentrantLockTest test = new ReentrantLockTest();
        Thread th1 = new Thread(test::summationCount);
        Thread th2 = new Thread(test::summationCount);

        th1.start();
        th2.start();

        th1.join();
        th2.join();

        return count;
    }
}

优点:灵活,适合复杂的同步需求。
缺点:代码稍显冗长,锁的开销较 AtomicLong 大。


推荐方案

如果只是简单的计数,推荐使用 AtomicLong,因为它简洁且性能较好。如果需要更多的同步控制(如在方法中进行复杂的操作),则选择 synchronizedReentrantLock

最后更新: