Java 并发的 Bug

编写正确的并发程序不是一件容易的事情

Java

Java 并发的 Bug

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

CPU 缓存导致的问题

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

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

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

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

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

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

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,因为它简洁且性能较好。如果需要更多的同步控制(如在方法中进行复杂的操作),则选择 synchronized 或 ReentrantLock。