Java多线程常见误区与最佳实践总结
1. 没有做线程管理
问题现象:相信很多人第一次接触多线程时,都是这样学的:
创建一个 Thread
对象,然后调用 start()
就可以了,于是写出这样一段经典的代码:
Thread t = new Thread(() -> {System.out.println("启动一个线程...");
});
t.start();
这段代码表面上没问题,但在复杂系统中,这种做法会埋下隐患。
深层原因:不要误以为线程和函数类似,调用它们就会并行执行。使用线程也就意味着你需要自己管理以下几个方面:
- 线程的生命周期
- 内存可见性与一致性
- 异常处理与任务调度
- 线程间协作与同步
如果管理不当,就可能引发:死锁、竞态条件、跨平台行为不一致等问题。
改进方案:使用线程池。在Java中,最常见的改进是使用 ExecutorService
,通过线程池,不需要手动管理线程,而是让框架完成调度以及线程资源复用。
// 线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(() -> {System.out.println("这是一个通过线程池启动的线程。");
});
executor.shutdown();// 更好的方案是自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(4, // corePoolSize8, // maximumPoolSize60L, // keepAliveTimeTimeUnit.SECONDS,new LinkedBlockingQueue<>(100), // 有界队列new ThreadFactoryBuilder().setNameFormat("biz-worker-%d").build(),new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
- 禁止使用 Executors.newFixedThreadPool 等工厂方法,因为默认是无界队列,高并发时容易 OOM。
- 生产场景一定要设置线程名,方便排查问题。
2. 不了解 Java 内存模型(JMM)
问题现象:假设有两个线程调用了下面这个简单程序,我们期望 reader()
总能读到最新的flag值,实际有时读不到。
class MyClass {boolean flag = false;public void writer() {flag = true;}public void reader() {if (flag) {System.out.println("Flag is true!");}}
}
原因分析:问题出在Java 内存模型(每个线程有工作内存,类似CPU缓存;变量写入主内存后,其他线程不一定立刻可见;指令可能被重排序优化)上。在没有显式同步的情况下,一个线程对变量的修改,可能对另一个线程不可见,reader()
可能一直读到旧值。
解决方案:使用 volatile
。如果需要保证变量更新的可见性,可以使用 volatile
关键字。volatile 保证可见性,写入结果对其他线程立刻可见。还禁止指令重排序,确保写入操作在读操作之前完成。但不保证原子性。
class MyClass {volatile boolean flag = false;public void writer() {flag = true;}public void reader() {if (flag) {System.out.println("Flag is true!");}}
}
3. 混淆并发(Concurrency)与并行(Parallelism)
用了多线程,程序并一定会更快。
- 并发:指同时处理多个任务,在单核 CPU上通过切换实现。
- 并行:指多个任务真正同时执行,需要多核 CPU支持。
如果程序要完成的任务主要是 I/O 密集型(如读写数据库、网络请求),并发可以显著提升性能。
如果是 CPU 密集型(如图像处理、大量计算),必须依赖真正的并行,才能加速。
// 示例:I/O 密集型
CompletableFuture.supplyAsync(this::queryDB).thenAccept(this::sendResponse);// CPU 密集型, 并行计算
ForkJoinPool pool = new ForkJoinPool();
pool.submit(() -> imageFiles.parallelStream().forEach(this::processImage));
4. 滥用 synchronized
问题现象: 在不理解锁的情况下随意加 synchronized
,虽然看似是安全的做法,但会使得应用运行变得缓慢。
public synchronized void doWork() {// 业务逻辑
}
原因分析:过度使用 synchronized
会导致:
- 线程竞争:所有线程抢同一把锁。
- 性能下降:尤其在高并发场景。
- 死锁风险:多线程互相等待锁释放。
解决方案
- 使用JDK 提供的高性能 并发集合,如
ConcurrentHashMap
- 使用 读写锁
ReadWriteLock
,区分读写场景 - 使用 原子类
AtomicInteger
/AtomicLong
处理简单计数,性能更高,也更安全
// 示例:用原子类代替 synchronized
AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet();
}// 示例:对于读多写少场景,使用 ReadWriteLock
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {// safe read
} finally {lock.readLock().unlock();
}
5. 没有用“多线程思维”设计程序
问题现象:刚开始写多线程程序时,还是会沿用一步一步执行,假设操作有序的线性编程思维。但多线程意味着多个步骤可能同时进行,如果不考虑线程安全,会导致出现bug。一些典型错误如:
- 无锁更新共享变量
- 假设操作顺序是固定的
- 不考虑竞态条件
建议:线程安全优先。编写多线程程序时要考虑:
- 有哪些共享状态?
- 谁会读?谁会写?
- 如果两个线程同时修改,会怎样?
/*
示例:银行账户提现
看似安全,但如果两个线程同时通过了 balance >= amount 判断,可能超提。
*/
class BankAccount {private int balance = 1000;public synchronized void withdraw(int amount) {if (balance >= amount) {balance -= amount;}}
}// 解决办法:使用乐观锁或数据库事务来保证一致性。
AtomicInteger balance = new AtomicInteger(1000);
public void withdraw(int amount) {int old;do {old = balance.get();if (old < amount) throw new RuntimeException("余额不足");} while (!balance.compareAndSet(old, old - amount));
}
6. 没有线程异常处理
问题现象:线程中的异常,默认会被悄悄吞掉:
// 主线程不会感知,异常就消失了
new Thread(() -> {throw new RuntimeException("线程发生异常!");
}).start();
解决方案一:统一异常捕获。
// 使用自定义 ThreadFactory 设置 UncaughtExceptionHandler
ThreadFactory factory = r -> {Thread t = new Thread(r);t.setUncaughtExceptionHandler((th, ex) -> log.error("线程异常", ex));return t;
};
解决方案二:使用 Future
Future<?> future = executor.submit(() -> {throw new RuntimeException("线程发生异常");
});
try {future.get();
} catch (ExecutionException e) {e.printStackTrace();
}
7. 没有正确测试多线程代码
问题现象:多线程代码测试起来的难度比较大,普通单元测试并不可靠。多线程bug常为低概率偶现,单次测试不足以发现问题。
解决方案
- 使用 Awaitility 等工具,等待条件满足
- 在生产环境引入随机延迟,模拟真实压力
// 示例:使用 Awaitility,相比Thread.sleep,这种写法更健壮。
Awaitility.await().atMost(5, TimeUnit.SECONDS).until(() -> sharedList.size() == 10);
8. 没有很好利用Java的新型并发工具
Java 并发编程已经大幅演进,有了更多更好用的工具类。
- CompletableFuture:简化异步编程
- ForkJoinPool:递归并行任务
- 结构化并发(Structured Concurrency):JDK21 引入的新特性
- 虚拟线程(Virtual Threads, Project Loom):降低并发成本
// 示例:使用 CompletableFuture,无需直接操纵线程池,异步任务写法更直观。
CompletableFuture.supplyAsync(() -> "从线程返回的结果").thenAccept(System.out::println);