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