Java基础(六:线程、线程同步,线程池)

Java教程 2025-11-14

1. 多线程基础概念

1.1 线程与进程

  • 进程:操作系统中正在运行的程序实例,拥有独立的内存空间,可以看做是一个正在运行的程序实例,进程之间是相互独立的。
  • 线程:进程中的执行单元,一个进程中可以包含多个线程,共享进程的内存空间。线程使用的资源开销更小,而且线程通信因为共享内存所以更轻松。

1.2 线程的应用

1. 并发处理

  • Web服务器: 处理多个客户端请求,每个请求由独立 Thread 处理
  • 数据库连接池: 多个 Thread 共享数据库连接资源
  • 文件I/O操作: 同时读写多个文件

2. 异步任务执行

  • 后台任务: 日志记录、数据备份等不需要用户等待的操作
  • 定时任务: 使用 ScheduledExecutorService 执行周期性任务
  • 消息队列消费者: 并行处理消息队列中的任务

3. UI应用程序

  • 事件处理: GUI应用中使用 Thread 分离用户界面响应和后台计算
  • 动画效果: 在单独 Thread 中处理界面动画,避免阻塞主线程

4. 计算密集型任务

  • 并行计算: 将大型计算任务分解为多个子任务并行执行
  • 数据处理: 多 Thread 处理大量数据集
  • 图像处理: 并行处理图像的不同区域

5. 网络编程

  • Socket通信: 服务器端使用多 Thread 处理多个客户端连接
  • HTTP客户端: 并发发送多个HTTP请求
  • RPC调用: 并行执行远程过程调用

1.3 线程状态

Java线程有以下几种状态:

  • NEW:新建状态,线程被创建但未启动
  • RUNNABLE:可运行状态,包括运行中和就绪状态
  • BLOCKED:阻塞状态,等待监视器锁
  • WAITING:等待状态,无限期等待其他线程执行特定操作
  • TIMED_WAITING:超时等待状态,有限时间等待
  • TERMINATED:终止状态,线程执行完毕

image.png

这张图展示了Java线程的生命周期和状态转换。一个线程从创建开始,经过就绪、运行、阻塞、等待等状态,最终结束。简单来说:线程刚创建时是"新建"状态 调用start()后进入"就绪"状态,等着CPU来执行一旦拿到CPU时间就开始"运行"如果遇到需要等待的情况(比如等待锁、休眠),就会进入相应的等待状态当等待条件满足或时间到,又会回到就绪状态继续竞争CPU最终线程执行完任务就"死亡"了。

1.4 线程的创建方式

方式一:继承 Thread 类

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
        System.out.println("线程执行中...");
    }
}

// 使用方式
MyThread thread = new MyThread();
thread.start(); // 启动线程

方式二:实现 Runnable 接口

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
        System.out.println("线程执行中...");
    }
}

// 使用方式
Thread thread = new Thread(new MyRunnable());
thread.start();

方式三:实现 Callable 接口

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable {
    @Override
    public String call() throws Exception {
        return "执行结果";
    }
}

// 使用方式
FutureTask futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
String result = futureTask.get(); // 获取执行结果

通过实现Runnable接口可以避免单继承的限制,提供更好的灵活性,因此最常用的创建方式是Runnable接口

1.5 线程控制方法

Java线程控制方法主要包括以下几种:

  • start(): 启动线程,使线程进入就绪状态
  • run(): 线程执行的具体任务逻辑
  • join(): 等待线程执行完毕
  • sleep(long millis): 使当前线程休眠指定时间
  • yield(): 暂停当前线程,让出CPU时间片
  • interrupt(): 中断线程
  • isAlive(): 判断线程是否存活
  • currentThread(): 获取当前线程实例

2. 线程安全与线程同步

2.1 概念

线程安全‌是一个核心概念,它描述了一个类、方法或对象在多线程环境下‌行为正确的特性。当多个线程同时访问同一个共享资源(如一个变量、一个对象或一个静态字段)时,如果系统始终能产生‌正确的行为和结果,那么这个资源就是线程安全的。线程不安全源于多个线程在没有适当保护的情况下,对共享数据进行“‌读-改-写‌”操作。这会导致数据竞争和脏读,最终得到不可预知、错误的结果。线程安全的目标是保证数据的‌一致性‌和‌可见性‌。一个线程修改了数据,其他线程能立即看到修改后的、一致的数据。

一个经典的线程不安全例子(银行取款):

假设一个银行账户初始有1000元,夫妻二人同时用两张卡从两个ATM机取款1000元。

  1. 线程A(丈夫)读取余额:1000元。
  2. 线程B(妻子)读取余额:1000元。
  3. 线程A计算:1000 - 1000 = 0,并将0写回余额。
  4. 线程B计算:1000 - 1000 = 0,并将0写回余额。

最终账户余额为0元,但实际只应扣款一次,正确的余额应为0元(或根据逻辑处理为负数不允许操作),但这个过程银行损失了1000元。这就是线程不安全导致的数据错误。

线程同步作为实现线程安全的核心技术体系,其本质在于通过协调多个线程的执行顺序和控制共享资源访问权限,建立并发的秩序。具体而言,这一机制通过两种关键方式保障系统可靠性:一是通过互斥锁(如synchronized或Lock)将可能被多线程同时访问的“临界区”代码转化为串行化执行,确保任一时刻仅有一个线程能操作共享资源;二是通过线程间通信机制(如wait/notify)使不同线程能够有序协作,将原本杂乱的并发操作转化为可控的有序流程。这种双重保障既防范了数据竞争导致的计算错误,又确保了资源状态的一致性,最终使得多线程程序在高效运行的同时保持行为的可预测性与正确性。

2.2 synchronized 关键字

synchronized 是Java中最基本的线程同步机制,可以保证同一时刻只有一个线程执行被它修饰的代码块。

修饰实例方法

public class Counter {
    private int count = 0;
    
    public synchronized void increment() {
        count++; // 同一时刻只有一个线程能执行此方法
    }
    
    public synchronized int getCount() {
        return count;
    }
}

修饰静态方法

public class StaticCounter {
    private static int count = 0;
    
    public static synchronized void increment() {
        count++; // 对类的所有实例同步
    }
}

修饰代码块

public class BlockSynchronization {
    private Object lock = new Object();
    private int count = 0;
    
    public void increment() {
        synchronized(lock) {
            count++; // 只对lock对象加锁
        }
    }
    
    public void decrement() {
        synchronized(this) {
            count--; // 对当前实例加锁
        }
    }
}

2.3 volatile 关键字

‌volatile通过强制主内存读写和禁止指令重排序,解决了共享变量的可见性和有序性问题,是线程安全的必要组成部分,但不保证原子性。

public class VolatileExample {‌**volatile通过强制主内存读写和禁止指令重排序,解决了共享变量的可见性和有序性问题,是线程安全的必要组成部分
    private volatile boolean flag = false; // 保证可见性
    private volatile int counter = 0; // 保证可见性
    
    public void setFlag(boolean flag) {
        this.flag = flag; // 修改立即对其他线程可见
    }
    
    public boolean getFlag() {
        return flag; // 读取最新的值
    }
}

注意volatile 不能替代 synchronized,因为它不保证复合操作的原子性:

private volatile int count = 0;

public void increment() {
    count++; // 这不是原子操作,仍可能出现线程安全问题
}

3. 线程池入门 Executors

3.1 什么是线程池

线程池是一种基于池化思想管理线程的工具,通过预先创建一定数量的线程来处理任务,避免频繁创建和销毁线程带来的开销。 举个例子,想象一家汉堡店(线程池):

  1. 核心员工(核心线程) ‌:常驻3位厨师(corePoolSize=3),日常订单由他们处理。
  2. 临时工(非核心线程) ‌:高峰期新增2位兼职(maximumPoolSize=5),应对客流激增。
  3. 候餐区(任务队列) ‌:10个座位(ArrayBlockingQueue(10)),新顾客在此排队。
  4. 拒绝策略(饱和策略) ‌:
  • 抛异常‌:座位满时新顾客直接被告知"无法接待"(AbortPolicy
  • 调用者做‌:经理亲自做汉堡(CallerRunsPolicy
  • 丢弃队列最旧‌:请走排队最久的顾客(DiscardOldestPolicy
  • 直接拒绝‌:新顾客直接被拒(DiscardPolicy

运作场景

  • 低峰期‌:3位厨师处理订单,候餐区空置。

  • 高峰期‌:

    • 订单涌入 → 候餐区坐满10人
    • 继续来客 → 激活2位临时工
    • 超负荷‌:新顾客触发拒绝策略(如经理道歉劝离)
  • 回归平静‌:临时工闲置超过keepAliveTime(如30分钟)后被解雇。

关键配置类比

new ThreadPoolExecutor(
  3,    // 核心厨师3人  
  5,    // 最大厨师5人  
  30,   // 临时工空闲30分钟下班  
  TimeUnit.MINUTES,  
  new ArrayBlockingQueue<>(10) // 10个候餐位  
);

为什么用线程池?

  • 避免重复开销‌:不重复雇佣/解雇厨师(线程创建/销毁耗资源)
  • 流量控制‌:队列缓冲突发订单,拒绝策略防系统崩溃
  • 资源复用‌:厨师持续接单提升效率

3.2 常见的线程池类型

FixedThreadPool 固定大小线程池

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// 创建固定大小为5的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);

// 提交任务
for (int i = 0; i < 10; i++) {
    final int taskId = i;
    fixedThreadPool.submit(() -> {
        System.out.println("任务 " + taskId + " 正在执行,线程:" + Thread.currentThread().getName());
    });
}

fixedThreadPool.shutdown(); // 关闭线程池

CachedThreadPool 缓存线程池

// 创建缓存线程池,可根据需要创建新线程,空闲线程会被回收
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

for (int i = 0; i < 10; i++) {
    final int taskId = i;
    cachedThreadPool.submit(() -> {
        System.out.println("任务 " + taskId + " 正在执行");
    });
}

cachedThreadPool.shutdown();

SingleThreadExecutor 单线程池

// 创建单线程池,保证所有任务按顺序执行
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

for (int i = 0; i < 5; i++) {
    final int taskId = i;
    singleThreadExecutor.submit(() -> {
        System.out.println("任务 " + taskId + " 正在执行");
    });
}

singleThreadExecutor.shutdown();

ScheduledThreadPool 定时任务线程池

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

// 创建定时任务线程池
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);

// 延迟执行
scheduledThreadPool.schedule(() -> {
    System.out.println("延迟3秒执行的任务");
}, 3, TimeUnit.SECONDS);

// 周期性执行
scheduledThreadPool.scheduleAtFixedRate(() -> {
    System.out.println("每2秒执行一次的任务");
}, 0, 2, TimeUnit.SECONDS);

// scheduledThreadPool.shutdown(); // 注意:定时任务通常不立即关闭

4. ThreadPoolExecutor 使用

4.1 ThreadPoolExecutor 构造参数详解

import java.util.concurrent.*;

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5,                          // corePoolSize: 核心线程数
    10,                         // maximumPoolSize: 最大线程数
    60L,                        // keepAliveTime: 空闲线程存活时间
    TimeUnit.SECONDS,           // unit: 时间单位
    new LinkedBlockingQueue<>(100), // workQueue: 工作队列
    Executors.defaultThreadFactory(), // threadFactory: 线程工厂
    new ThreadPoolExecutor.AbortPolicy() // handler: 拒绝策略
);

4.2 核心参数说明

参数说明
corePoolSize核心线程数,即使空闲也会保留的线程数量
maximumPoolSize最大线程数,线程池允许创建的最大线程数量
keepAliveTime非核心线程的空闲存活时间
workQueue用于存放待执行任务的阻塞队列
threadFactory创建新线程的工厂
handler拒绝策略,当线程池和队列都满时的处理策略

4.3 工作流程

  1. 如果当前线程数小于 corePoolSize,即使线程池中有空闲线程,也会创建新的核心线程来执行任务
  2. 如果当前线程数等于或大于 corePoolSize,任务将被放入 workQueue
  3. 如果 workQueue 已满,且当前线程数小于 maximumPoolSize,会创建新的非核心线程来执行任务
  4. 如果当前线程数已经达到 maximumPoolSize,且 workQueue 已满,则根据拒绝策略处理新任务

4.4 拒绝策略

// AbortPolicy: 直接抛出异常(默认策略)
new ThreadPoolExecutor.AbortPolicy()

// CallerRunsPolicy: 由调用线程执行任务
new ThreadPoolExecutor.CallerRunsPolicy()

// DiscardPolicy: 直接丢弃任务
new ThreadPoolExecutor.DiscardPolicy()

// DiscardOldestPolicy: 丢弃队列最前面的任务,然后重新尝试执行
new ThreadPoolExecutor.DiscardOldestPolicy()

4.5 自定义 ThreadPoolExecutor 示例

import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 自定义线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
            3,                                      // 核心线程数
            6,                                      // 最大线程数
            30L,                                    // 空闲线程存活时间
            TimeUnit.SECONDS,                       // 时间单位
            new ArrayBlockingQueue<>(10),          // 有界阻塞队列
            new ThreadFactory() {                  // 自定义线程工厂
                private int counter = 0;
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r, "CustomThread-" + counter++);
                    thread.setDaemon(false);
                    return thread;
                }
            },
            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        );
        
        // 提交任务
        for (int i = 0; i < 20; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("任务 " + taskId + " 由 " + 
                    Thread.currentThread().getName() + " 执行");
                try {
                    Thread.sleep(2000); // 模拟任务执行
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
        
        // 关闭线程池
        executor.shutdown();
        
        try {
            // 等待所有任务完成
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow(); // 强制关闭
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
        }
    }
}

4.6 线程池状态管理

// 常用的线程池管理方法
executor.shutdown();           // 平滑关闭,不再接受新任务,等待已有任务完成
executor.shutdownNow();        // 立即关闭,尝试中断所有正在执行的任务
executor.isShutdown();         // 判断是否已关闭
executor.isTerminated();       // 判断是否所有任务都已完成
executor.getPoolSize();        // 获取当前线程数
executor.getActiveCount();     // 获取活跃线程数
executor.getCompletedTaskCount(); // 获取已完成任务数

5. 最佳实践建议

  1. 优先使用 ThreadPoolExecutor 而不是 Executors 工厂方法,可以更精确地控制线程池行为
  2. 合理设置线程池大小:CPU密集型任务设置为CPU核心数+1,IO密集型任务可以设置更大
  3. 选择合适的队列类型ArrayBlockingQueue 适合有界队列,LinkedBlockingQueue 适合无界队列
  4. 正确处理异常:在线程任务中捕获并处理异常,避免线程意外终止
  5. 及时关闭线程池:应用结束前调用 shutdown()shutdownNow() 方法