Java多线程基础与实战:同步、通信与线程池

前言

​ 在开发个人项目的过程中,随着业务逻辑的复杂化,多线程处理成为了必须面对的问题。无论是保证共享数据的安全性,还是提高系统的并发处理能力,掌握Java多线程的基础知识都至关重要。

​ 本文旨在梳理我目前对Java多线程的理解,重点记录核心机制的原理、代码实现以及在项目中遇到的实际问题。

一、线程同步机制

1.1 synchronized:内置锁的应用

synchronized是Java中最基础的同步关键字,基于对象监视器(Monitor)实现。它具有可重入性,且由JVM自动管理锁的获取与释放,不易出现死锁(相对于手动锁而言)。

代码实现

​ 在项目中处理库存扣减等涉及共享资源的操作时,我通常采用同步代码块的方式,以减小锁的粒度:

public class StockService {
    private int stock = 100;
    // 定义专用的锁对象,避免锁this带来的潜在风险
    private final Object lock = new Object();

    public void decreaseStock(int amount) {
        // 仅对关键逻辑加锁
        synchronized (lock) {
            if (stock >= amount) {
                stock -= amount;
                System.out.println("扣减成功,剩余库存:" + stock);
            } else {
                System.out.println("库存不足");
            }
        }
        // 耗时操作(如发送通知)放在锁外,避免阻塞其他线程
    }
}
注意事项
  • 锁对象选择:尽量避免使用字符串常量或全局单例对象作为锁,防止不同业务模块意外竞争同一把锁。
  • 静态方法synchronized修饰静态方法时,锁的是当前类的Class对象,作用范围是整个JVM,需谨慎使用。

1.2 volatile:可见性与有序性

volatile关键字主要用于保证变量的可见性禁止指令重排序,但它不保证原子性

适用场景

在控制线程运行状态的场景中,volatile非常有效。例如,需要一个标志位来优雅地停止后台任务:

public class DataTask implements Runnable {
    // 必须使用volatile,确保主线程修改后,工作线程能立即感知
    private volatile boolean running = true;

    @Override
    public void run() {
        while (running) {
            // 执行任务逻辑
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break;
            }
        }
        System.out.println("任务已停止");
    }

    public void stop() {
        this.running = false;
    }
}
误区提醒

​ 切勿使用volatile修饰自增操作(如count++)。因为count++包含读、改、写三个步骤,volatile无法保证这三个步骤的原子性,仍会导致数据不一致。此类场景需配合synchronized或原子类(AtomicInteger)使用。

1.3 关于ReentrantLock

ReentrantLock提供了比synchronized更灵活的功能,如尝试获取锁(tryLock)、中断等待、公平锁等。

现状说明
由于ReentrantLock需要手动在finally块中释放锁,若处理不当极易引发死锁风险。目前我的项目中暂无特殊锁需求,synchronized已能满足大部分场景。因此,现阶段我优先保证代码的稳健性,后续将在更复杂的并发场景中逐步引入ReentrantLock

二、线程间通信

2.1 wait/notify机制

wait()notify()Object类提供的方法,用于线程间的等待与唤醒。使用时必须配合synchronized,且等待条件判断必须使用while循环,以防止虚假唤醒
在这里插入图片描述

示例:生产者 - 消费者模型
public class Buffer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int MAX_SIZE = 10;

    public void produce(int item) throws InterruptedException {
        synchronized (queue) {
            // 队列满时等待
            while (queue.size() == MAX_SIZE) {
                queue.wait();
            }
            queue.add(item);
            queue.notifyAll(); // 唤醒所有等待线程
        }
    }

    public int consume() throws InterruptedException {
        synchronized (queue) {
            // 队列空时等待
            while (queue.isEmpty()) {
                queue.wait();
            }
            int item = queue.poll();
            queue.notifyAll();
            return item;
        }
    }
}

三、线程池实战

​ 在生产环境中,严禁直接使用new Thread()创建线程。频繁创建和销毁线程会消耗大量系统资源,甚至导致OOM。使用ThreadPoolExecutor统一管理线程是标准做法。

3.1 线程池参数配置

在这里插入图片描述

在SpringBoot项目中,我通过配置类定义了全局线程池:

@Bean
public ThreadPoolExecutor taskExecutor() {
    return new ThreadPoolExecutor(
        5,                      // 核心线程数
        10,                     // 最大线程数
        60L,                    // 空闲线程存活时间
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(100), // 任务队列
        new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
    );
}
参数解读
  • 核心线程数:线程池中保持存活的线程数量。
  • 最大线程数:当队列满时,允许创建的最大线程数。
  • 拒绝策略:当队列和线程数都达到上限时的处理方式。
    • CallerRunsPolicy:由调用线程执行该任务。这种策略既能防止任务丢失,又能通过降低提交速度来缓解系统压力,适合对任务完整性要求较高的场景。

3.2 避坑指南

  • 避免使用Executors工厂方法Executors.newFixedThreadPool()等方法创建的线程池,其队列长度默认为Integer.MAX_VALUE,可能导致内存溢出。建议始终通过ThreadPoolExecutor构造函数显式创建。
  • 资源隔离:不同业务场景(如IO密集型、CPU密集型)建议使用不同的线程池,避免相互影响。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐