Java并发——多线程

在计算机领域,多线程是提升程序性能、充分利用多核处理器的重要手段。Java从诞生之初就内置了对多线程的支持,提供了丰富的API和并发工具。然而,多线程编程也伴随着线程安全、死锁、性能损耗等挑战。本文将系统性地介绍Java多线程的核心概念、同步机制、高级并发工具以及最佳实践,帮助你构建可靠的高并发应用。

一、线程的基本概念

1.1 进程与线程

  • 进程:操作系统资源分配的基本单位,拥有独立的内存空间。

  • 线程:CPU调度的基本单位,是进程内的一个执行流,共享进程的内存空间。

Java中的线程通过 java.lang.Thread 类表示。每个线程都有自己的程序计数器、栈和局部变量,但堆内存和方法区是共享的。

1.2 线程的生命周期

Java线程在运行过程中会经历以下状态(定义在 Thread.State 枚举中):

  • NEW :线程对象已创建,但尚未调用 start()

  • RUNNABLE:可运行状态,可能正在执行或等待CPU调度。

  • BLOCKED:等待获取监视器锁(synchronized)而被阻塞。

  • WAITING :无限期等待另一个线程执行特定操作(如 wait()join() 无超时)。

  • TIMED_WAITING :有限时等待(如 sleep(long)wait(long)join(long))。

  • TERMINATED:线程执行完毕。

状态转换关系如下:

java 复制代码
NEW -> RUNNABLE -> TERMINATED
RUNNABLE <-> BLOCKED / WAITING / TIMED_WAITING

二、创建线程的三种方式

2.1 继承Thread类

重写 run() 方法,然后创建子类对象并调用 start()

java 复制代码
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running: " + Thread.currentThread().getName());
    }
}

// 使用
MyThread t = new MyThread();
t.start();

缺点:Java单继承,无法再继承其他类。

2.2 实现Runnable接口

实现 Runnable 接口,将其作为参数传递给 Thread 对象。

java 复制代码
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable running");
    }
}

// 使用
Thread t = new Thread(new MyRunnable());
t.start();

优点:解耦任务与线程,更灵活,推荐使用。

2.3 实现Callable接口与Future

Callable 可以有返回值,并抛出异常。通过 FutureTask 包装后提交给线程执行,或配合线程池使用。

java 复制代码
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 42;
    }
}

// 使用
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
Thread t = new Thread(futureTask);
t.start();
Integer result = futureTask.get(); // 阻塞获取结果

三、线程同步机制

多线程访问共享资源时,需要保证原子性、可见性和有序性。Java提供了多种同步机制。

3.1 synchronized关键字

  • 同步方法:锁是当前实例对象(实例方法)或Class对象(静态方法)。

  • 同步代码块:可以指定任意对象作为锁。

java 复制代码
public class Counter {
    private int count = 0;

    public synchronized void increment() { // 实例锁
        count++;
    }

    public void decrement() {
        synchronized (this) { // 等价于上面的方法锁
            count--;
        }
    }
}

synchronized 保证了原子性和可见性,但在高竞争时可能引入性能问题。

3.2 volatile关键字

volatile 保证变量的可见性,禁止指令重排序,但不保证原子性。适用于单个变量的读写操作,如标志位。

java 复制代码
volatile boolean flag = true;

3.3 Lock接口与ReentrantLock

java.util.concurrent.locks.Lock 提供了比 synchronized 更灵活的锁操作,如尝试锁、可中断锁、公平锁等。

java 复制代码
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

ReentrantLock 支持公平锁(通过构造函数指定),并且可以配合 Condition 实现更灵活的等待/通知。

3.4 原子类

java.util.concurrent.atomic 包提供了原子操作类,如 AtomicIntegerAtomicReference 等,利用CAS(Compare-And-Swap)实现无锁并发。

java 复制代码
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); // 原子自增

四、线程间通信

4.1 wait/notify/notifyAll

这些方法必须在 synchronized 块内调用,配合对象监视器使用。

java 复制代码
synchronized (lock) {
    while (condition) {
        lock.wait(); // 释放锁,进入等待
    }
    // 条件满足,执行操作
    lock.notifyAll(); // 唤醒等待线程
}

注意 :始终在循环中使用 wait(),防止虚假唤醒。

4.2 Condition

ConditionLock 的等待/通知机制,可以创建多个等待队列,比 wait/notify 更精细。

java 复制代码
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();

lock.lock();
try {
    while (conditionFlag) {
        condition.await();
    }
    condition.signalAll();
} finally {
    lock.unlock();
}

4.3 管道流(PipedInputStream/PipedOutputStream)

用于线程间的字节流或字符流通信,但实际使用较少。

五、高级并发工具(JUC)

java.util.concurrent 包提供了大量现成的并发组件,极大地简化了并发编程。

5.1 CountDownLatch

允许一个或多个线程等待其他线程完成操作。计数器递减到零时,等待线程被唤醒。

java 复制代码
CountDownLatch latch = new CountDownLatch(3);

// 工作线程
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 执行任务
        latch.countDown();
    }).start();
}

latch.await(); // 主线程等待所有工作线程完成

5.2 CyclicBarrier

让一组线程到达一个屏障点时被阻塞,直到最后一个线程到达,所有线程才继续执行。可重用。

java 复制代码
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程都到了,开始下一阶段");
});

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 阶段1工作
        barrier.await();
        // 阶段2工作
    }).start();
}

5.3 Semaphore

控制同时访问特定资源的线程数量,相当于许可证。

java 复制代码
Semaphore semaphore = new Semaphore(3); // 最多3个线程同时访问

semaphore.acquire(); // 获取许可,若不足则阻塞
try {
    // 访问共享资源
} finally {
    semaphore.release();
}

5.4 Exchanger

用于两个线程之间交换数据。

java 复制代码
Exchanger<String> exchanger = new Exchanger<>();

// 线程A
String data = "A数据";
String result = exchanger.exchange(data);

// 线程B
String data = "B数据";
String result = exchanger.exchange(data);

5.5 CompletableFuture

Java 8引入的异步编程工具,支持函数式组合,可以轻松编排异步任务。

java 复制代码
CompletableFuture.supplyAsync(() -> "Hello")
        .thenApply(s -> s + " World")
        .thenAccept(System.out::println);

六、线程池(Executor框架)

频繁创建销毁线程开销巨大,线程池通过复用线程提升性能。ExecutorService 是线程池的主要接口。

6.1 创建线程池

可以通过 Executors 工厂方法创建预定义线程池,但更推荐直接使用 ThreadPoolExecutor 以精确控制参数。

java 复制代码
// 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);

// 缓存线程池(无限大,空闲60秒回收)
ExecutorService cachedPool = Executors.newCachedThreadPool();

// 单线程池
ExecutorService singlePool = Executors.newSingleThreadExecutor();

// 自定义线程池
ThreadPoolExecutor customPool = new ThreadPoolExecutor(
    2, 10, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(100),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

6.2 提交任务

  • execute(Runnable):无返回值。

  • submit(Runnable/Callable):返回 Future,可获取结果或异常。

java 复制代码
Future<Integer> future = pool.submit(() -> {
    Thread.sleep(1000);
    return 100;
});
Integer result = future.get(); // 阻塞

6.3 关闭线程池

  • shutdown():不再接受新任务,已提交任务继续执行。

  • shutdownNow():尝试中断正在执行的任务,并返回未开始的任务列表。

java 复制代码
pool.shutdown();
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
    pool.shutdownNow();
}

七、并发集合

JUC提供了线程安全的集合,避免手动同步:

  • ConcurrentHashMap:分段锁或CAS实现的高并发Map。

  • CopyOnWriteArrayList:写时复制,适合读多写少场景。

  • BlockingQueue :阻塞队列,用于生产者-消费者模式(如 ArrayBlockingQueueLinkedBlockingQueue)。

  • ConcurrentLinkedQueue:非阻塞队列,基于CAS。

  • ConcurrentSkipListMap/Set:基于跳表的有序集合。

八、死锁与避免

8.1 死锁产生的四个必要条件

  1. 互斥条件:资源一次只能被一个线程占用。

  2. 持有并等待:线程持有至少一个资源,并等待其他资源。

  3. 非抢占:资源只能由持有者主动释放。

  4. 循环等待:线程间形成循环等待链。

8.2 避免死锁的策略

  • 打破循环等待:统一资源获取顺序。

  • 使用 tryLock 超时:获取锁失败时释放已持有的锁。

  • 使用 open call 原则:调用外部方法时不持有锁。

  • 使用更高层次的并发工具 (如 ConcurrentHashMapCountDownLatch)替代手动锁。

九、线程安全与最佳实践

  1. 优先使用不可变对象:对象一旦创建,状态不可变,天然线程安全。

  2. 缩小同步范围:仅在必要的地方加锁,使用同步代码块代替同步方法。

  3. 使用并发工具而非 wait/notify:JUC工具更可靠、易用。

  4. 避免线程泄露:线程池使用后必须关闭。

  5. 善用 ThreadLocal:维护线程本地变量,避免共享。

  6. 充分测试并发代码:使用工具(如JMH、JCStress)进行压力测试和正确性验证。

十、总结

Java多线程编程从基础的 Thread 到高级的 CompletableFuture,提供了丰富的抽象和工具。掌握线程的生命周期、同步机制以及JUC中的并发组件,是构建高性能、高可用系统的基石。在实际开发中,应根据场景选择合适的并发策略,避免过度优化或忽视线程安全。并发编程虽复杂,但通过系统学习和实践,完全可以驾驭。

相关推荐
AMoon丶2 小时前
Golang--内存管理
开发语言·后端·算法·缓存·golang·os
yunyun321232 小时前
自动化与脚本
jvm·数据库·python
阿里嘎多学长2 小时前
2026-03-22 GitHub 热点项目精选
开发语言·程序员·github·代码托管
暮冬-  Gentle°2 小时前
使用PyTorch构建你的第一个神经网络
jvm·数据库·python
setmoon2142 小时前
构建一个基于命令行的待办事项应用
jvm·数据库·python
23.2 小时前
【Java】字符串底层与常量池演变全解析
java·开发语言·jvm
美式请加冰2 小时前
异常的介绍和使用
开发语言·c++
极客先躯2 小时前
高级java每日一道面试题-2025年9月09日-数据处理篇[LangChain4j]-金融行业使用 LLM 有哪些合规要求?
java·金融·高级面试题·权限与访问控制·数据脱敏与隐私计算·模型可解释性工具·审计日志与监控
jing-ya2 小时前
day 59 图论part10
java·开发语言·数据结构·算法·图论