Java 多线程编程详解

Java 多线程编程是实现并发任务的核心技术,通过充分利用 CPU 多核资源,可显著提升程序的执行效率(如并行处理数据、异步响应请求等)。本文从线程基础、创建方式、同步机制、线程通信到高级工具类,全面解析 Java 多线程编程的核心知识点与实战技巧。

一、线程基础:什么是线程?为什么需要多线程?

在 Java 中,进程是程序的一次执行过程(如运行中的 JVM 实例),而线程是进程内的最小执行单元(一个进程可包含多个线程,共享进程资源)。多线程的核心价值在于:
 
  • 提高资源利用率:当一个线程因 IO 操作(如读写文件、网络请求)阻塞时,其他线程可继续执行,避免 CPU 空闲。
  • 提升响应速度:如 GUI 程序中,用后台线程处理数据,主线程保持界面响应(避免界面卡顿)。

线程与进程的核心区别

维度进程线程
资源占用 独立内存空间、文件描述符 共享进程内存空间、资源
切换开销 大(需切换内存映射等) 小(仅切换寄存器、栈等)
通信方式 复杂(如管道、Socket) 简单(直接读写共享变量)

线程的生命周期

Java 线程有 5 种状态,状态转换是多线程调试的核心依据:
 
  1. 新建(New):线程对象创建后(如 new Thread()),未调用 start() 时的状态。
  2. 就绪(Runnable):调用 start() 后,线程进入 “可运行” 状态,等待 CPU 调度(可能处于运行中或等待调度)。
  3. 运行(Running):CPU 调度线程执行 run() 方法,此时处于运行状态。
  4. 阻塞(Blocked/Waiting/Timed Waiting):线程暂停执行,如等待锁(Blocked)、调用 wait()Waiting)、sleep(1000)Timed Waiting)。
  5. 死亡(Terminated)run() 方法执行完毕,或因异常终止。

二、线程创建:3 种核心方式与对比

Java 提供 3 种创建线程的方式,各有适用场景,需根据是否需要返回值、是否继承类等需求选择。

1. 继承 Thread 类(无返回值)

Thread 类是线程的核心类,继承它并重写 run() 方法(线程执行体),调用 start() 启动线程(不可直接调用 run(),否则会作为普通方法执行)。
 
// 1. 继承 Thread 类
class MyThread extends Thread {
    @Override
    public void run() {  // 线程执行体
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

// 2. 使用线程
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.setName("线程1");  // 设置线程名
        t1.start();  // 启动线程(进入就绪状态,等待CPU调度)

        // 主线程执行
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
 
 
输出(顺序不确定,因 CPU 调度)
 
main:0
线程1:0
main:1
线程1:1
...
 
 
缺点:Java 是单继承,继承 Thread 后无法再继承其他类,灵活性低。

2. 实现 Runnable 接口(无返回值,推荐)

Runnable 是函数式接口(仅 run() 方法),实现它后,将实例传入 Thread 构造器,通过 Thread 启动线程。解决了单继承限制,是最常用的方式。
 
// 1. 实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {  // 线程执行体
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}

// 2. 使用线程
public class RunnableDemo {
    public static void main(String[] args) {
        // 创建 Runnable 实例
        MyRunnable task = new MyRunnable();
        // 传入 Thread 构造器,启动线程
        Thread t2 = new Thread(task, "线程2");  // 直接指定线程名
        t2.start();

        // 主线程执行
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
 
 
优势
 
  • 可多线程共享一个 Runnable 实例(适合多线程处理同一份资源,如卖票系统)。
  • 避免单继承限制,更灵活。

3. 实现 Callable 接口(有返回值,支持异常)

Callable 接口(Java 5+)与 Runnable 类似,但:
 
  • 有返回值(泛型 V);
  • 可抛出受检异常;
  • 需配合 Future 或 FutureTask 获取结果。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 1. 实现 Callable 接口(泛型指定返回值类型)
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {  // 线程执行体,有返回值
        int sum = 0;
        for (int i = 1; i <= 10; i++) {
            sum += i;
        }
        return sum;  // 返回 1+2+...+10 的结果
    }
}

// 2. 使用线程
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建 Callable 实例
        MyCallable task = new MyCallable();
        // 包装为 FutureTask(实现了 Future 和 Runnable 接口)
        FutureTask<Integer> futureTask = new FutureTask<>(task);
        // 传入 Thread 启动
        new Thread(futureTask, "计算线程").start();

        // 主线程获取结果(get() 会阻塞,直到子线程执行完毕)
        System.out.println("1到10的和:" + futureTask.get());  // 输出:55
    }
}
 
 
适用场景:需要线程执行结果的场景(如并行计算汇总结果)。

三、线程同步:解决并发安全问题

多线程共享资源时,若多个线程同时读写共享变量,可能导致数据不一致(如卖票系统中多线程同时减库存,出现超卖)。线程同步的核心是 “保证同一时间只有一个线程访问共享资源”。

1. 共享资源问题示例(未同步)

// 共享资源:票池
class Ticket {
    private int count = 10;  // 10张票

    // 卖票方法(未同步)
    public void sell() {
        if (count > 0) {
            // 模拟网络延迟(放大并发问题)
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--count));
        }
    }
}

// 多线程卖票
public class UnsafeDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();  // 共享一个票池
        // 3个线程同时卖票
        new Thread(() -> { for (int i = 0; i < 5; i++) ticket.sell(); }, "窗口1").start();
        new Thread(() -> { for (int i = 0; i < 5; i++) ticket.sell(); }, "窗口2").start();
        new Thread(() -> { for (int i = 0; i < 5; i++) ticket.sell(); }, "窗口3").start();
    }
}
 
 
可能的错误输出(出现负数,超卖):
窗口1卖出1张,剩余:9
窗口2卖出1张,剩余:8
窗口3卖出1张,剩余:7
...
窗口1卖出1张,剩余:-1  // 错误!
 

2. synchronized 关键字(隐式锁)

synchronized 是 Java 内置的同步机制,通过 “锁” 保证代码块 / 方法的原子性(同一时间只有一个线程执行)。有两种用法:
(1)同步方法(锁是当前对象 this
在方法声明上添加 synchronized,锁对象为当前实例(this)。
class Ticket {
    private int count = 10;

    // 同步方法(锁是 this)
    public synchronized void sell() {
        if (count > 0) {
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--count));
        }
    }
}
 
(2)同步代码块(锁是指定对象)
更灵活,可指定锁对象(如 this、类对象、自定义对象),只同步关键代码(减少锁竞争)。
 
class Ticket {
    private int count = 10;
    private Object lock = new Object();  // 自定义锁对象

    public void sell() {
        // 同步代码块(锁是 lock 对象)
        synchronized (lock) {
            if (count > 0) {
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--count));
            }
        }
    }
}
 
 
注意
 
  • 锁对象必须是同一个(多线程竞争同一把锁才有效)。
  • 同步会降低效率(因线程阻塞等待锁),需最小化同步范围(只锁必要代码)。

3. Lock 接口(显式锁,Java 5+)

java.util.concurrent.locks.Lock 是更灵活的同步机制,需手动获取和释放锁(lock() 和 unlock()),推荐配合 try-finally 使用(确保锁释放)。
 
常用实现类:ReentrantLock(可重入锁,支持公平锁 / 非公平锁)。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Ticket {
    private int count = 10;
    // 创建锁对象(true 表示公平锁:按线程等待顺序获取锁,默认非公平锁)
    private Lock lock = new ReentrantLock(true);

    public void sell() {
        lock.lock();  // 获取锁
        try {
            if (count > 0) {
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                System.out.println(Thread.currentThread().getName() + "卖出1张,剩余:" + (--count));
            }
        } finally {
            lock.unlock();  // 释放锁(必须在 finally 中,避免异常导致锁未释放)
        }
    }
}
 
 
Lock vs synchronized
 
特性synchronizedLock
锁获取 / 释放 隐式(自动获取 / 释放) 显式(lock()/unlock()
公平锁 不支持 支持(ReentrantLock(true)
中断响应 不支持(不可中断等待锁的线程) 支持(lockInterruptibly()
超时机制 不支持 支持(tryLock(long, TimeUnit)

4. volatile 关键字(保证可见性)

volatile 用于修饰共享变量,保证多线程间的可见性(一个线程修改后,其他线程立即看到最新值),但不保证原子性(不能替代 synchronized 或 Lock)。
 
适用场景:单线程写、多线程读的变量(如状态标记)。
 
class Flag {
    private volatile boolean isStop = false;  // volatile 保证可见性

    public void setStop(boolean stop) {
        isStop = stop;
    }

    public void run() {
        while (!isStop) {  // 多线程读取时,能立即看到 isStop 的修改
            System.out.println("运行中...");
        }
        System.out.println("已停止");
    }
}

public class VolatileDemo {
    public static void main(String[] args) throws InterruptedException {
        Flag flag = new Flag();
        Thread t = new Thread(flag::run);
        t.start();

        // 主线程 2 秒后修改状态
        Thread.sleep(2000);
        flag.setStop(true);
    }
}
 

四、线程通信:协作完成任务

多线程常需协作(如生产者生产数据后通知消费者消费),Java 提供两种核心通信方式:

1. wait()/notify()/notifyAll()(基于 synchronized

这三个方法是 Object 类的 native 方法,必须在同步代码块 / 方法中使用(否则抛 IllegalMonitorStateException),作用是:
 
  • wait():释放当前锁,进入等待状态,直到被 notify() 唤醒。
  • notify():随机唤醒一个等待该锁的线程。
  • notifyAll():唤醒所有等待该锁的线程。
 
生产者 - 消费者模型示例
// 共享缓冲区
class Buffer {
    private int data;
    private boolean hasData = false;  // 是否有数据

    // 生产者放入数据
    public synchronized void put(int num) throws InterruptedException {
        while (hasData) {  // 若有数据,等待消费者取走
            wait();  // 释放锁,进入等待
        }
        data = num;
        hasData = true;
        System.out.println("生产者放入:" + num);
        notify();  // 唤醒消费者
    }

    // 消费者取出数据
    public synchronized int take() throws InterruptedException {
        while (!hasData) {  // 若无数据,等待生产者放入
            wait();  // 释放锁,进入等待
        }
        hasData = false;
        System.out.println("消费者取出:" + data);
        notify();  // 唤醒生产者
        return data;
    }
}

// 生产者线程
class Producer implements Runnable {
    private Buffer buffer;
    // 构造器传入缓冲区
    public Producer(Buffer buffer) { this.buffer = buffer; }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            try {
                buffer.put(i);
                Thread.sleep(500);  // 模拟生产耗时
            } catch (InterruptedException e) {}
        }
    }
}

// 消费者线程
class Consumer implements Runnable {
    private Buffer buffer;
    public Consumer(Buffer buffer) { this.buffer = buffer; }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            try {
                buffer.take();
                Thread.sleep(1000);  // 模拟消费耗时
            } catch (InterruptedException e) {}
        }
    }
}

// 测试
public class ProducerConsumer {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();
        new Thread(new Producer(buffer)).start();
        new Thread(new Consumer(buffer)).start();
    }
}
 
 
输出
 
生产者放入:1
消费者取出:1
生产者放入:2
消费者取出:2
...
 

2. Condition 接口(基于 Lock,更灵活)

Condition 是 Lock 的配套接口,通过 Lock.newCondition() 创建,替代 wait()/notify(),支持多条件等待(一个锁可关联多个 Condition,实现更精细的线程唤醒)。
 
核心方法:await()(替代 wait())、signal()(替代 notify())、signalAll()(替代 notifyAll())。

五、线程池:高效管理线程资源

频繁创建 / 销毁线程会消耗大量资源(线程创建需分配栈空间等),线程池通过复用线程减少开销,是生产环境的推荐方式(Java 5+ 提供 Executor 框架)。

1. 线程池核心参数(ThreadPoolExecutor

public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数(常驻线程,即使空闲也不销毁)
    int maximumPoolSize,     // 最大线程数(核心线程+临时线程的上限)
    long keepAliveTime,      // 临时线程空闲超时时间(超时后销毁)
    TimeUnit unit,           // keepAliveTime 的单位(如 SECONDS)
    BlockingQueue<Runnable> workQueue,  // 任务队列(核心线程满时,新任务入队)
    ThreadFactory threadFactory,        // 线程工厂(自定义线程创建,如命名)
    RejectedExecutionHandler handler    // 拒绝策略(任务队列满+线程达最大时的处理方式)
)
 

2. 常见线程池(Executors 工具类创建)

Executors 提供了 4 种预定义线程池,覆盖多数场景:
 
线程池类型特点适用场景
FixedThreadPool 核心线程数 = 最大线程数,无临时线程,队列无界(LinkedBlockingQueue 任务数量固定,需长期运行的任务
CachedThreadPool 核心线程数 = 0,最大线程数 = Integer.MAX_VALUE,队列是同步队列 短期、轻量任务(如临时请求)
SingleThreadExecutor 核心线程数 = 1,最大线程数 = 1,队列无界 需顺序执行的任务(如日志记录)
ScheduledThreadPool 支持定时 / 周期性执行任务 定时任务(如心跳检测)
 
示例:FixedThreadPool 使用
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建固定 3 个线程的线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);

        // 提交 5 个任务
        for (int i = 0; i < 5; i++) {
            int taskId = i;
            pool.submit(() -> {  // 提交 Runnable 任务
                System.out.println(Thread.currentThread().getName() + "执行任务:" + taskId);
                try { Thread.sleep(1000); } catch (InterruptedException e) {}
            });
        }

        pool.shutdown();  // 关闭线程池(已提交任务执行完毕后关闭)
    }
}
 
 
输出(3 个线程复用执行 5 个任务):
 
pool-1-thread-1执行任务:0
pool-1-thread-2执行任务:1
pool-1-thread-3执行任务:2
pool-1-thread-1执行任务:3  // 线程1复用
pool-1-thread-2执行任务:4  // 线程2复用
 

3. 拒绝策略(任务无法处理时)

当任务队列满且线程数达最大值时,线程池会触发拒绝策略,ThreadPoolExecutor 提供 4 种默认策略:
 
拒绝策略行为
AbortPolicy(默认) 抛 RejectedExecutionException 异常(推荐,及时发现问题)
CallerRunsPolicy 由提交任务的线程(如主线程)执行任务(降低新任务提交速度)
DiscardPolicy 直接丢弃新任务(无提示,不推荐)
DiscardOldestPolicy 丢弃队列中最旧的任务,再尝试提交新任务

六、高级并发工具类(java.util.concurrent

JDK 提供多种工具类简化复杂并发场景,以下是最常用的 3 种:

1. CountDownLatch(倒计时器)

让一个线程等待其他多个线程完成后再执行(如主线程等待所有子线程处理完数据后汇总)。
 
import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        int threadCount = 3;
        CountDownLatch latch = new CountDownLatch(threadCount);  // 计数 3

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "完成任务");
                latch.countDown();  // 计数减1
            }, "子线程" + i).start();
        }

        latch.await();  // 主线程等待,直到计数为0
        System.out.println("所有子线程完成,主线程开始汇总");
    }
}
 

2. CyclicBarrier(循环屏障)

让多个线程到达屏障点后再一起继续执行(如多个线程先各自加载数据,全部加载完后再一起处理)。
 
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
    public static void main(String[] args) {
        int threadCount = 3;
        // 屏障点:3个线程到达后,执行 Runnable(可选)
        CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
            System.out.println("所有线程已到达,开始处理数据");
        });

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + "加载数据中...");
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + "到达屏障点");
                    barrier.await();  // 等待其他线程
                    System.out.println(Thread.currentThread().getName() + "继续执行");
                } catch (Exception e) {}
            }, "线程" + i).start();
        }
    }
}
 

3. Semaphore(信号量)

控制同时访问某个资源的线程数量(如限制并发连接数)。
 
import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    public static void main(String[] args) {
        int permits = 2;  // 允许 2 个线程同时访问
        Semaphore semaphore = new Semaphore(permits);

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    semaphore.acquire();  // 获取许可(若满则等待)
                    System.out.println(Thread.currentThread().getName() + "获取资源,开始操作");
                    Thread.sleep(1000);  // 模拟操作耗时
                } catch (InterruptedException e) {
                } finally {
                    semaphore.release();  // 释放许可
                    System.out.println(Thread.currentThread().getName() + "释放资源");
                }
            }, "用户" + i).start();
        }
    }
}
 

七、线程安全与最佳实践

  1. 避免死锁:死锁是多线程因互相等待对方释放锁而卡死的状态,预防措施:
    • 按固定顺序获取锁(如所有线程先锁 A 再锁 B);
    • 使用 tryLock(timeout) 设置超时,避免无限等待。
  2. 减少锁竞争
    • 最小化同步范围(只锁必要代码);
    • 用 ConcurrentHashMap 等并发集合替代同步容器(如 Hashtable)。
  3. 优先使用线程池:避免手动创建线程,通过线程池管理资源,控制并发量。
  4. 慎用 ThreadLocalThreadLocal 为每个线程提供独立变量副本(如存储用户会话),但需注意:
    • 线程池环境下,线程复用可能导致 ThreadLocal 变量残留,需手动 remove()
    • 避免存储大对象,防止内存泄漏。

总结

Java 多线程编程的核心是并发协作与资源安全,需掌握:
 
  • 线程创建的 3 种方式(Thread/Runnable/Callable);
  • 同步机制(synchronized/Lock/volatile)解决线程安全问题;
  • 线程通信(wait()/notify()/Condition)实现任务协作;
  • 线程池(ThreadPoolExecutor)高效管理线程资源;
  • 高级工具类(CountDownLatch/CyclicBarrier/Semaphore)简化复杂场景。
 
实际开发中,需结合业务场景选择合适的技术,同时重视线程安全与性能平衡,避免过度同步或资源浪费。

posted on 2025-10-21 13:43  coding博客  阅读(3)  评论(0)    收藏  举报