Java 线程安全、线程池与并发工具详解

一、Java 线程安全性

线程安全性是指多线程环境下,程序执行结果始终与单线程执行结果一致,且无数据竞争、死锁等问题。核心本质是“保证共享资源的原子性、可见性、有序性”。

1. 线程不安全的根源:共享资源竞争

当多个线程同时操作共享可变资源(如静态变量、堆内存中的对象属性)时,若缺乏同步控制,会导致以下问题:

  • 原子性问题:一个操作(如 i++,实际包含“读取-修改-写入”三步)被拆分执行,中间被其他线程打断,导致结果错误。
    // 线程不安全示例:多线程执行 count++,结果可能小于预期
    public class UnsafeCounter {
        private static int count = 0;
    
        public static void increment() {
            count++; // 非原子操作
        }
    
        public static void main(String[] args) throws InterruptedException {
            Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) increment(); });
            Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) increment(); });
            t1.start(); t2.start();
            t1.join(); t2.join();
            System.out.println(count); // 可能输出 1500(而非 2000)
        }
    }
    
  • 可见性问题:线程对共享变量的修改,其他线程无法及时感知(因 CPU 缓存、指令重排序导致)。
  • 有序性问题:JVM 为优化性能对指令重排序,导致代码执行顺序与预期不一致(如“双重检查锁单例”的指令重排序问题)。

2. 保证线程安全的 5 种核心方案

(1)使用同步锁:synchronized

synchronized 是 Java 内置的隐式锁,可修饰方法或代码块,保证同一时间只有一个线程进入同步区域,实现原子性、可见性、有序性。

// 修复上述计数器:用 synchronized 修饰方法
public class SafeCounterWithSynchronized {
    private static int count = 0;

    // 同步静态方法(锁为类对象)
    public static synchronized void increment() {
        count++;
    }

    // 或同步代码块(锁为指定对象)
    public static void incrementBlock() {
        synchronized (SafeCounterWithSynchronized.class) {
            count++;
        }
    }
}
(2)使用显式锁:Lock

java.util.concurrent.locks.Lock 接口(如 ReentrantLock)是显式锁,需手动调用 lock() 获取锁、unlock() 释放锁,功能比 synchronized 更灵活(如可中断锁、超时锁、公平锁)。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SafeCounterWithLock {
    private static int count = 0;
    private static final Lock lock = new ReentrantLock(); // 可重入锁

    public static void increment() {
        lock.lock(); // 获取锁
        try {
            count++; // 临界区操作
        } finally {
            lock.unlock(); // 释放锁(必须在 finally 中,避免锁泄漏)
        }
    }
}
(3)使用原子类:java.util.concurrent.atomic

原子类基于“CAS(Compare and Swap,比较并交换)”机制实现无锁原子操作,性能优于锁(避免线程上下文切换),常用类包括 AtomicIntegerAtomicLongAtomicReference

import java.util.concurrent.atomic.AtomicInteger;

public class SafeCounterWithAtomic {
    // 原子整数,count++ 操作原子化
    private static final AtomicInteger count = new AtomicInteger(0);

    public static void increment() {
        count.incrementAndGet(); // 原子操作:自增并返回新值
    }

    public static void main(String[] args) throws InterruptedException {
        // 多线程执行后,结果必为 2000
        Thread t1 = new Thread(() -> { for (int i = 0; i < 1000; i++) increment(); });
        Thread t2 = new Thread(() -> { for (int i = 0; i < 1000; i++) increment(); });
        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println(count.get()); // 输出 2000
    }
}
(4)使用 volatile 关键字

volatile 是轻量级同步机制,无法保证原子性,但可保证可见性(线程修改后立即刷新到主内存,其他线程读取时从主内存获取)和有序性(禁止指令重排序),适用于“单线程写、多线程读”的场景(如状态标记)。

// volatile 示例:线程安全的状态标记
public class VolatileDemo {
    // 用 volatile 修饰状态变量,保证可见性
    private static volatile boolean isStop = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!isStop) {
                // 执行任务
            }
            System.out.println("线程停止");
        }).start();

        // 主线程修改状态,子线程可立即感知
        Thread.sleep(1000);
        isStop = true;
    }
}
(5)无共享资源:ThreadLocal

ThreadLocal 为每个线程提供独立的变量副本,避免线程间共享资源,从根本上消除竞争。适用于“每个线程需要独立实例”的场景(如数据库连接、Session 管理)。

import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadLocalDemo {
    // 每个线程拥有独立的 SimpleDateFormat 实例(避免线程不安全)
    private static final ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(() -> 
        new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );

    public static String formatDate(Date date) {
        // 每个线程调用 get() 时获取自己的副本
        return sdf.get().format(date);
    }

    public static void main(String[] args) {
        new Thread(() -> System.out.println(formatDate(new Date()))).start();
        new Thread(() -> System.out.println(formatDate(new Date()))).start();
    }
}

二、Java 线程池:原理与使用

线程池是“管理线程的容器”,通过复用线程减少线程创建/销毁的开销,控制并发数,避免资源耗尽,是生产环境中管理线程的首选方案。

1. 线程池核心参数(ThreadPoolExecutor)

Java 线程池的核心实现类是 java.util.concurrent.ThreadPoolExecutor,其构造方法包含 7 个核心参数,决定线程池的行为:

// ThreadPoolExecutor 构造方法
public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数:线程池长期维持的线程数(即使空闲也不销毁)
    int maximumPoolSize,     // 最大线程数:线程池可创建的最大线程数
    long keepAliveTime,      // 非核心线程空闲存活时间:超过该时间则销毁
    TimeUnit unit,           // keepAliveTime 的时间单位(如 TimeUnit.SECONDS)
    BlockingQueue<Runnable> workQueue, // 任务阻塞队列:核心线程满时,任务暂存的队列
    ThreadFactory threadFactory,       // 线程工厂:创建线程的方式(如自定义线程名)
    RejectedExecutionHandler handler   // 拒绝策略:任务队列满且线程数达最大值时的处理方式
) { ... }

2. 线程池工作流程

  1. 提交任务时,若核心线程数未达 corePoolSize,创建核心线程执行任务;
  2. 若核心线程满,将任务加入 workQueue 队列;
  3. 若队列已满,且当前线程数 < maximumPoolSize,创建非核心线程执行任务;
  4. 若队列已满且线程数达 maximumPoolSize,触发拒绝策略处理任务。

3. 常用拒绝策略

拒绝策略类 作用
AbortPolicy(默认) 直接抛出 RejectedExecutionException 异常,阻止系统正常运行
CallerRunsPolicy 让提交任务的线程(如主线程)自己执行任务,减缓任务提交速度
DiscardPolicy 直接丢弃新任务,不抛出异常
DiscardOldestPolicy 丢弃队列中最旧的任务,尝试提交新任务

4. 线程池的创建方式

(1)通过 Executors 工具类(简单但不推荐生产环境)

Executors 提供了快捷创建线程池的方法,但可能因参数固定导致资源问题(如 newCachedThreadPool 可能创建无限线程):

// 1. 固定大小线程池(corePoolSize = maximumPoolSize)
ExecutorService fixedPool = Executors.newFixedThreadPool(5);

// 2. 单线程池(仅1个线程,任务串行执行)
ExecutorService singlePool = Executors.newSingleThreadExecutor();

// 3. 可缓存线程池(核心线程0,最大线程无限,适合短期任务)
ExecutorService cachedPool = Executors.newCachedThreadPool();

// 4. 定时任务线程池(支持延迟/周期性任务)
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
(2)直接创建 ThreadPoolExecutor(推荐,可控性强)

根据业务需求自定义参数,避免资源耗尽风险:

// 示例:创建适合 IO 密集型任务的线程池(线程数可略多)
ExecutorService customPool = new ThreadPoolExecutor(
    5,                  // 核心线程数:5
    10,                 // 最大线程数:10
    60,                 // 非核心线程空闲60秒后销毁
    TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100), // 任务队列容量100
    Executors.defaultThreadFactory(), // 默认线程工厂
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:提交者执行
);

5. 线程池的关闭

  • shutdown():拒绝新任务,等待已提交任务完成后关闭。
  • shutdownNow():立即尝试停止所有任务(可能导致任务中断),返回未执行的任务列表。
// 推荐关闭方式
customPool.shutdown();
if (!customPool.awaitTermination(60, TimeUnit.SECONDS)) {
    // 超时后强制关闭
    customPool.shutdownNow();
}

三、Java 并发集合容器

普通集合(如 ArrayListHashMap)在多线程环境下不安全(如 ConcurrentModificationException),Java 提供了专门的并发集合,通过锁或无锁机制保证线程安全,常用容器如下:

并发集合类 对应普通集合 线程安全实现方式 适用场景
ConcurrentHashMap HashMap 分段锁(JDK7)/ CAS + synchronized(JDK8+) 高并发下的键值对存储,支持高效读写
CopyOnWriteArrayList ArrayList 写时复制(修改时复制新数组,读不加锁) 读多写少场景(如配置列表、事件监听器)
CopyOnWriteArraySet HashSet 基于 CopyOnWriteArrayList 实现 读多写少的无序集合
ConcurrentLinkedQueue LinkedList(队列) 无锁 CAS 操作 高并发下的队列(FIFO),如任务队列
BlockingQueue 实现类 - 内置锁或 Condition 机制 生产者-消费者模型(如线程池任务队列)

典型示例:ConcurrentHashMap

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentMapDemo {
    public static void main(String[] args) {
        Map<String, Integer> map = new ConcurrentHashMap<>();

        // 多线程并发写入
        for (int i = 0; i < 10; i++) {
            int num = i;
            new Thread(() -> map.put("key" + num, num)).start();
        }

        // 多线程并发读取
        new Thread(() -> {
            for (Map.Entry<String, Integer> entry : map.entrySet()) {
                System.out.println(entry.getKey() + ":" + entry.getValue());
            }
        }).start();
    }
}

四、原子操作与 CAS 机制

原子操作是“不可中断的操作”,要么完全执行,要么完全不执行。Java 中原子操作的实现依赖CAS(Compare and Swap,比较并交换) 机制,无需锁即可保证线程安全。

1. CAS 原理

CAS 包含 3 个操作数:

  • 内存地址 V:存储变量的内存地址;
  • 预期值 A:线程读取到的变量当前值;
  • 新值 B:线程要写入的新值。

执行逻辑:若内存地址 V 中的值等于预期值 A,则将其更新为 B;否则不执行更新,线程可重试或放弃(无锁自旋)。

2. 原子类的使用(见“线程安全”部分)

AtomicInteger 外,Java 还提供:

  • 引用类型原子类:AtomicReference(原子更新对象引用);
  • 数组类型原子类:AtomicIntegerArray(原子更新数组元素);
  • 字段更新器:AtomicIntegerFieldUpdater(原子更新对象的某个字段)。

3. CAS 的优缺点

  • 优点:无锁操作,避免线程上下文切换和锁竞争的开销,性能高;
  • 缺点
    • ABA 问题:变量从 A 变为 B 再变回 A,CAS 会误认为未修改(可通过 AtomicStampedReference 加版本号解决);
    • 自旋开销:高并发下 CAS 失败重试,可能导致 CPU 占用过高;
    • 只能保证单个变量的原子操作:多个变量的原子性需结合锁实现。

总结

Java 线程模型是并发编程的核心,理解进程与线程的区别、掌握线程安全保障机制(锁、原子类、ThreadLocal)、合理使用线程池和并发集合,是编写高效、安全的多线程程序的基础。实际开发中需根据业务场景选择合适的工具(如 IO 密集型任务用较大线程池,CPU 密集型任务用较小线程池),并警惕死锁、内存可见性等问题。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐