
在多核 CPU 普及的今天,多线程早已不是高级开发的 “选修课”—— 它是提升程序吞吐量、优化资源利用率的核心手段。但多线程带来的并发安全、死锁、性能损耗等问题,却让很多开发者 “谈线程色变”:明明逻辑没问题,一跑多线程就出现数据错乱;好不容易解决了安全问题,程序又变得比单线程还慢。
今天,我们就梳理一套多线程使用的最佳实践,从线程创建到通信、从同步控制到问题排查,帮你写出既安全又高效的并发代码。
多线程的核心痛点,本质是 “无序性” 和 “共享资源竞争”。最佳实践的第一步,就是用标准化工具替代 “随手写线程” 的野蛮方式,从根源减少混乱。
每次用 new Thread() 创建线程,会带来两大问题:
避免使用 Executors 提供的默认方法(如 FixedThreadPool 可能因任务堆积导致 OOM),手动指定线程池参数,按需管控线程生命周期。
示例代码:
// 核心参数:核心线程数、最大线程数、空闲线程存活时间、队列、拒绝策略ThreadPoolExecutor threadPool = new ThreadPoolExecutor( 2, // 核心线程数(CPU密集型设为 CPU核数,IO密集型设为 2*CPU核数) 4, // 最大线程数(控制并发上限) 60L, // 空闲线程存活时间 TimeUnit.SECONDS, new ArrayBlockingQueue<>(10), // 有界队列(避免无界队列OOM) new ThreadPoolExecutor.AbortPolicy() // 拒绝策略(任务满时直接抛异常,便于感知问题));// 提交任务threadPool.submit(() -> { try { // 业务逻辑:如IO请求、数据处理 Thread.sleep(100); System.out.println("任务执行完成"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态,避免中断信号丢失 }});// 程序结束前关闭线程池(优雅关闭:先拒绝新任务,再等待已提交任务完成)threadPool.shutdown();if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) { threadPool.shutdownNow(); // 超时未完成则强制关闭}关键配置建议:
线程安全的核心是 “控制共享资源的访问顺序”,但错误的同步方式会导致性能暴跌或死锁。
反例(过度加锁):
// 错误:锁整个方法,即使只有一行共享资源操作public synchronized void updateData() { // 非共享资源操作:如日志打印、局部变量处理(无需加锁) log.info("开始更新数据"); // 共享资源操作:仅这一行需要同步 sharedCount++;}正例(缩小锁粒度):
private final Object lock = new Object(); // 独立锁对象,避免锁this或Class对象public void updateData() { log.info("开始更新数据"); // 无锁操作 // 仅对共享资源操作加锁,缩小锁范围 synchronized (lock) { sharedCount++; }}Lock 示例(支持超时):
private final Lock lock = new ReentrantLock();public void tryUpdate() throws InterruptedException { // 尝试加锁,5秒超时则放弃(避免死锁) if (lock.tryLock(5, TimeUnit.SECONDS)) { try { sharedCount++; } finally { lock.unlock(); // 必须在finally中释放锁,避免异常导致锁泄漏 } } else { log.warn("加锁超时,放弃更新"); }}死锁的本质是满足 4 个条件:互斥、持有并等待、不可剥夺、循环等待。只要破坏其中一个,就能避免死锁。
常见死锁场景:
// 线程1:先锁A,再锁Bsynchronized (lockA) { synchronized (lockB) { // 操作 }}// 线程2:先锁B,再锁Asynchronized (lockB) { synchronized (lockA) { // 操作 }}避免死锁的 3 个实用方法:
线程间通信(如通知、数据传递)是多线程的高频场景,原生的 wait()/notify() 容易出错,推荐用更安全的工具类。
BlockingQueue 是线程安全的队列,自带 “阻塞等待” 功能(队列空时消费者阻塞,队列满时生产者阻塞),无需手动处理 wait()/notify() 的细节。
示例代码:
// 1. 创建有界阻塞队列private final BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);// 2. 生产者线程:向队列存数据class Producer implements Runnable { @Override public void run() { try { String data = "数据-" + System.currentTimeMillis(); queue.put(data); // 队列满时自动阻塞 System.out.println("生产者存入:" + data); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }}// 3. 消费者线程:从队列取数据class Consumer implements Runnable { @Override public void run() { try { String data = queue.take(); // 队列空时自动阻塞 System.out.println("消费者取出:" + data); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }}// 4. 启动线程threadPool.submit(new Producer());threadPool.submit(new Consumer());CountDownLatch 示例:
// 主线程等待5个任务线程完成CountDownLatch latch = new CountDownLatch(5);for (int i = 0; i < 5; i++) { threadPool.submit(() -> { try { // 业务逻辑 Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); // 任务完成,计数器减1 } });}latch.await(); // 主线程阻塞,直到计数器为0System.out.println("所有任务执行完成");即使掌握了最佳实践,仍可能因细节疏忽踩坑,以下是高频误区:
volatile 仅能保证可见性(线程修改后其他线程能立即看到)和有序性(禁止指令重排序),但不能保证原子性(如 i++ 是 “读 - 改 - 写” 三步操作,会被打断)。
反例:
private volatile int count = 0;// 多线程调用该方法,count最终结果会小于预期(原子性问题)public void increment() { count++; }正例:
用原子类(AtomicInteger)替代 volatile,原子类基于 CAS(Compare and Swap)实现无锁原子操作:
private final AtomicInteger count = new AtomicInteger(0);public void increment() { count.incrementAndGet(); // 原子操作}ThreadLocal 用于存储线程私有变量(如 Web 项目中存储当前用户信息),但如果不手动清理,会导致内存泄漏:
最佳实践:用完后调用 remove(),尤其是在 Web 场景(如拦截器、过滤器):
// 1. 定义ThreadLocalprivate static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();// 2. 存储数据userThreadLocal.set(currentUser);try { // 业务逻辑} finally { // 3. 必须清理!避免内存泄漏 userThreadLocal.remove();}Collections.synchronizedMap() 等同步包装器,本质是对所有方法加锁(相当于 “全局锁”),性能极差;推荐用专门的并发容器,如 ConcurrentHashMap、CopyOnWriteArrayList。
对比:
场景 | 不推荐 | 推荐 | 优势 |
|---|---|---|---|
线程安全 Map | Collections.synchronizedMap() | ConcurrentHashMap | 分段锁 / CAS,支持高并发读写 |
线程安全 List | Collections.synchronizedList() | CopyOnWriteArrayList | 读无锁,写时复制,适合读多写少 |
多线程的核心不是 “炫技”,而是 “平衡安全与性能”。记住这三个字,能帮你少走 90% 的弯路:
最后,多线程问题往往隐藏在 “极端场景” 中,建议结合工具排查:用 `
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。