多线程篇(实现方式)

目录

多线程篇

一、核心概念

二、多线程的关键特性

三、多线程的实现方式

1、继承 Thread 类(基础方式)

2、实现 Runnable 接口(推荐方式)

3、实现 Callable 接口(带返回值的线程)

4、使用线程池(ExecutorService 框架)

(一)线程池的四种类型

*1. FixedThreadPool(固定大小线程池)**

2. CachedThreadPool(可缓存线程池)

3. SingleThreadExecutor(单线程池)

4. ScheduledThreadPool(定时任务线程池)

(二)推荐使用的线程池:手动创建 ThreadPoolExecutor

为什么不推荐 Executors?

推荐方式:手动构造 ThreadPoolExecutor

(三)线程池七大核心参数详解

1. corePoolSize(核心线程数)

2. maximumPoolSize(最大线程数)

3. keepAliveTime(存活时间)

4. unit(时间单位)

5. workQueue(任务队列)

6. threadFactory(线程工厂)

7. handler(拒绝策略)

(四)线程池参数配置最佳实践

(五)总结

四、多线程的优缺点

五、适用场景

六、注意事项


多线程是指在一个程序(进程)中同时运行多个独立的执行路径(线程),每个线程可以并行或并发地处理不同任务,从而提高程序的执行效率和资源利用率。以下是关于多线程的详细解析:

一、核心概念

  1. 进程 vs 线程

    • 进程:是操作系统分配资源的基本单位(如内存、文件句柄等),每个进程至少包含一个主线程。

    • 线程:是进程内的最小执行单元,共享进程的资源(如内存空间、全局变量),但有独立的执行栈和寄存器状态。

    • 关系:一个进程可包含多个线程,线程在进程的资源中运行,切换成本低于进程。

  2. 多线程的目标

    • 提高效率:利用多核 CPU 并行处理任务(如同时下载文件、播放音乐)。

    • 改善用户体验:避免单线程阻塞导致程序无响应(如 GUI 程序中,主线程处理界面,子线程处理数据加载)。

    • 资源共享:线程间共享进程内存,无需复杂的 IPC(进程间通信)机制。

二、多线程的关键特性

  1. 并行与并发

    • 并行:多个线程在多核 CPU 上同时执行(真正的 “同时运行”)。

    • 并发:多个线程在单核 CPU 上通过时间片轮转交替执行(宏观上 “同时运行”,微观上串行)。

  2. 共享资源与竞争条件

    • 共享资源:线程间共享进程的内存空间,如全局变量、对象实例等。

    • 竞争条件(Race Condition):多个线程同时访问或修改共享资源时,可能导致结果不可预测(如计数器自增操作)。

    • 解决方案:

      • 同步机制:使用锁(Lock)、信号量(Semaphore)、原子变量等保证同一时刻只有一个线程访问资源。

      • 线程安全类:使用标准库中线程安全的容器(如 Java 的ConcurrentHashMap)。

  3. 线程生命周期

    • 新建(New):创建线程对象但未启动。

    • 就绪(Runnable):调用start()方法后,等待 CPU 调度。

    • 运行(Running):获取 CPU 资源,执行run()方法。

    • 阻塞(Blocked):因等待锁、IO 操作等暂停执行。

    • 终止(Terminated):线程执行完毕或异常终止。

三、多线程的实现方式

不同编程语言和框架提供了多种创建线程的方式,以下是常见示例:

  • 1、继承 Thread 类(基础方式)

    核心思想:创建一个类继承 Thread 类,重写 run() 方法定义线程任务,通过 start() 方法启动线程。 代码示例

    public class MyThread extends Thread {
        // 重写 run() 方法,定义线程执行逻辑
        @Override
        public void run() {
            System.out.println("线程 " + getName() + " 运行中");
        }
    ​
        public static void main(String[] args) {
            MyThread thread = new MyThread();
            thread.setName("自定义线程"); // 设置线程名称
            thread.start(); // 启动线程(而非直接调用 run())
        }
    }

    特点

    • 优点:简单直观,直接操作线程对象。

    • 缺点:Java 不支持类的多继承,限制了子类的扩展性;代码与线程耦合度高,不利于资源共享。 适用场景:简单场景下的独立线程任务,无需共享状态。

    2、实现 Runnable 接口(推荐方式)

    核心思想:创建一个类实现 Runnable 接口,重写 run() 方法定义任务,将该类实例作为参数传入 Thread 构造器中启动线程。 代码示例

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("线程 " + Thread.currentThread().getName() + " 运行中");
        }
    ​
        public static void main(String[] args) {
            Runnable task = new MyRunnable(); // 创建任务对象
            Thread thread = new Thread(task); // 将任务绑定到线程
            thread.setName("Runnable 线程");
            thread.start();
        }
    }

    特点

    • 优点:避免单继承限制,支持资源共享(多个线程可共享同一个 Runnable 实例);分离 “线程” 与 “任务”,代码解耦性更强。

    • 缺点:需显式与 Thread 类结合,代码稍复杂。 适用场景:多线程共享同一资源(如计数器、缓存)的场景,或需要继承其他类的情况。

    3、实现 Callable 接口(带返回值的线程)

    核心思想:通过 Callable 接口和 Future 机制实现有返回值的线程任务,需搭配 ExecutorService 线程池使用。 代码示例

    import java.util.concurrent.*;
    ​
    public class MyCallable implements Callable<String> { // 泛型指定返回值类型
        @Override
        public String call() throws Exception { // 可抛出异常,支持返回值
            Thread.sleep(1000); // 模拟耗时任务
            return "任务完成,当前时间:" + System.currentTimeMillis();
        }
    ​
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ExecutorService executor = Executors.newSingleThreadExecutor(); // 创建线程池
            Callable<String> task = new MyCallable();
            Future<String> future = executor.submit(task); // 提交任务并获取 Future 对象
    ​
            // 获取线程执行结果(可阻塞等待或设置超时)
            String result = future.get(); // 阻塞直到任务完成
            System.out.println("线程返回结果:" + result);
    ​
            executor.shutdown(); // 关闭线程池
        }
    }

    特点

    • 优点:支持线程执行结果返回和异常处理;通过线程池管理线程,避免资源浪费。

    • 缺点:需结合线程池使用,代码复杂度较高。 适用场景:需要获取线程执行结果或处理异常的场景(如异步计算、任务分治)。

    4、使用线程池(ExecutorService 框架)

    核心思想:通过 Executor 框架管理线程池,复用线程对象,避免频繁创建 / 销毁线程的开销。

    不推荐直接使用(原因见下文 “推荐使用的线程池” 部分)。以下是四种常见类型:

    (一)线程池的四种类型
    *1. FixedThreadPool(固定大小线程池)**
    • 创建方式:

      ExecutorService fixedPool = Executors.newFixedThreadPool(int nThreads);
    • 核心特性:

      • 核心线程数 = 最大线程数 = nThreads,线程数固定,不会销毁。

      • 任务队列是 无界队列 LinkedBlockingQueue,任务积压时可能导致 OOM(内存溢出)。

    • 适用场景: 长期运行的固定并发任务(如数据库连接池线程),但需注意队列长度限制。

    2. CachedThreadPool(可缓存线程池)
    • 创建方式:

      ExecutorService cachedPool = Executors.newCachedThreadPool();
    • 核心特性:

      • 核心线程数为 0,最大线程数为 Integer.MAX_VALUE(理论无上限)。

      • 任务队列是 同步队列 SynchronousQueue,无任务时线程存活时间为 60s,空闲线程会被销毁。

    • 风险: 短时间提交大量任务时可能创建海量线程,导致 CPU 耗尽或 OOM。

    • 适用场景: 少量短期异步任务(如临时计算),不适合高并发场景。

    3. SingleThreadExecutor(单线程池)
    • 创建方式:

      ExecutorService singlePool = Executors.newSingleThreadExecutor();
    • 核心特性:

      • 仅包含 1 个核心线程,保证任务按顺序串行执行。

      • 任务队列是 无界队列 LinkedBlockingQueue,同样存在 OOM 风险。

    • 适用场景: 需要保证顺序执行的任务(如单例资源操作、数据库串行写入)。

    4. ScheduledThreadPool(定时任务线程池)
    • 创建方式:

      ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(int corePoolSize);
    • 核心特性:

      • 核心线程数由 corePoolSize 指定,最大线程数为 Integer.MAX_VALUE

      • 支持定时任务(scheduleAtFixedRate)和周期性任务(scheduleWithFixedDelay)。

    • 适用场景: 定时任务(如日志刷新、缓存过期清理)、周期性任务(如心跳检测)。

    (二)推荐使用的线程池:手动创建 ThreadPoolExecutor
    为什么不推荐 Executors

    Executors 创建的线程池使用 无界队列(如 LinkedBlockingQueue)或 过大的最大线程数,可能导致:

    1. 内存溢出:大量任务积压在无界队列中,耗尽内存。

    2. 资源耗尽CachedThreadPool 可能创建过多线程,导致 CPU 饱和或线程竞争加剧。

    推荐方式:手动构造 ThreadPoolExecutor

    通过 ThreadPoolExecutor 构造方法自定义七大参数,精准控制线程池行为:

    ThreadPoolExecutor executor = new ThreadPoolExecutor(
        corePoolSize,       // 核心线程数
        maximumPoolSize,    // 最大线程数
        keepAliveTime,      // 非核心线程存活时间
        unit,               // 时间单位
        workQueue,          // 任务队列
        threadFactory,      // 线程工厂
        handler             // 拒绝策略
    );
    (三)线程池七大核心参数详解
    1. corePoolSize(核心线程数)
    • 定义:线程池中常驻的核心线程数量,即使无任务也不会销毁(除非 allowCoreThreadTimeOut 设为 true)。

    • 作用:

      • 任务提交时,优先创建核心线程执行任务,直到达到 corePoolSize

      • 建议根据任务类型设置:

        • CPU 密集型:设为 CPU核心数 + 1(充分利用 CPU,避免上下文切换)。

        • IO 密集型:设为 2 * CPU核心数(线程可能因 IO 阻塞,需更多线程处理任务)。

    2. maximumPoolSize(最大线程数)
    • 定义:线程池允许创建的最大线程数量(核心线程 + 非核心线程)。

    • 触发条件: 当任务队列已满且当前运行线程数 < maximumPoolSize 时,创建新线程(非核心线程)处理任务。

    • 注意:

      • 非核心线程在空闲时间超过 keepAliveTime 时会被销毁。

      • 若任务队列是 无界队列(如 LinkedBlockingQueue),maximumPoolSize 形同虚设(队列不会满,不会创建非核心线程)。

    3. keepAliveTime(存活时间)
    • 定义:非核心线程在空闲状态下的存活时间,超时则销毁。

    • 作用范围:

      • 线程数 > corePoolSize 时,非核心线程空闲时间超过 keepAliveTime 会被回收。

      • 可通过 setKeepAliveTime() 动态调整。

    4. unit(时间单位)
    • 可选值TimeUnit 枚举类(NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS 等)。

    • 示例keepAliveTime = 30, unit = TimeUnit.SECONDS 表示非核心线程最多存活 30 秒。

    5. workQueue(任务队列)
    • 定义:用于存放待执行任务的队列,类型分为三类:

      1. 直接提交队列:如 SynchronousQueue,无存储空间,任务直接提交给线程,队列满时创建新线程(适用于 CachedThreadPool)。

      2. 有界队列:如 ArrayBlockingQueue(指定容量),队列满后触发拒绝策略(推荐使用,避免 OOM)。

      3. 无界队列:如 LinkedBlockingQueue(默认容量 Integer.MAX_VALUE),可能导致内存溢出(Executors 默认使用,不推荐)。

    • 选择建议:

      • 高并发场景:优先使用 有界队列(如 ArrayBlockingQueue(1000)),配合合理的 maximumPoolSize 和拒绝策略。

    6. threadFactory(线程工厂)
    • 作用:用于创建线程的工厂,可自定义线程名称、优先级、是否为守护线程等。

    • 最佳实践:

      ThreadFactoryBuilder

      (如 Guava 库)命名线程,便于故障排查:

      ThreadFactory namedFactory = new ThreadFactoryBuilder()
          .setNameFormat("my-thread-pool-%d").build();
    7. handler(拒绝策略)
    • 触发条件:当任务队列已满且线程数达到 maximumPoolSize 时,新任务会被拒绝,需通过拒绝策略处理。

    • 四种内置拒绝策略:

      1. AbortPolicy(默认):直接抛出 RejectedExecutionException,需调用方捕获处理。

      2. CallerRunsPolicy:由提交任务的主线程执行任务(降低提交速度,适用于非异步场景)。

      3. DiscardPolicy:静默丢弃无法处理的任务(可能丢失数据,需谨慎)。

      4. DiscardOldestPolicy:丢弃队列中最旧的任务,尝试提交新任务(存在数据丢失风险)。

    • 自定义策略: 实现 RejectedExecutionHandler 接口,自定义处理逻辑(如记录日志、写入数据库重试等)。

    (四)线程池参数配置最佳实践
    1. 避免使用无界队列:优先选择有界队列(如 ArrayBlockingQueue),防止内存溢出。

    2. 合理设置核心线程数:

      • 通过 Runtime.getRuntime().availableProcessors() 获取 CPU 核心数。

      • CPU 密集型:corePoolSize = CPU核心数 + 1

      • IO 密集型:corePoolSize = CPU核心数 * 2(或通过压测确定)。

    3. 选择合适的拒绝策略:

      • 关键业务:使用 AbortPolicy 并捕获异常,记录日志或通知管理员。

      • 非关键业务:可使用 DiscardPolicy 或自定义重试逻辑。

    4. 监控线程池状态: 通过 getQueue().size()getActiveCount() 等方法监控队列长度和线程活跃数,动态调整参数。

    (五)总结
    线程池类型核心场景是否推荐风险提示
    FixedThreadPool固定并发任务(如数据库连接)无界队列可能导致 OOM
    CachedThreadPool少量短期任务(如临时计算)可能创建海量线程导致 CPU 耗尽
    SingleThreadExecutor顺序执行任务(如单例资源操作)无界队列风险
    ScheduledThreadPool定时 / 周期性任务(如缓存清理)需注意最大线程数限制
    手动创建 ThreadPoolExecutor所有生产环境场景(自定义参数)需根据业务精准配置七大参数

    核心原则:线程池的配置没有 “银弹”,需结合业务特点(任务类型、并发量、数据重要性)和硬件资源(CPU、内存)综合调优,优先使用有界队列和合理的拒绝策略,避免系统崩溃。

四、多线程的优缺点

优点

  1. 提高 CPU 利用率,适合 IO 密集型任务(如网络请求、文件读写)。

  2. 善程序响应速度,避免主线程阻塞。

  3. 简化编程模型,可通过线程拆分复杂任务。

缺点

  1. 线程安全问题复杂,需额外处理共享资源竞争。

  2. 线程切换带来上下文开销(尤其线程数量过多时)。

  3. 调试和测试难度大,难以复现偶发的竞态条件。

五、适用场景

  1. IO 密集型任务:如网络爬虫(多个线程同时下载网页)、文件处理(多线程读写不同文件)。

  2. 实时交互程序:GUI 应用(主线程处理界面,子线程处理数据加载)。

  3. 任务拆分:将复杂计算拆分为多个子任务并行处理(如视频渲染、大数据分析)。

  4. 服务器应用:多线程处理客户端请求(如 Web 服务器的线程池模型)。

六、注意事项

  1. 避免过度使用线程:线程数量过多会导致内存占用过高、上下文切换频繁,反而降低性能。

  2. 线程安全设计:对共享资源的操作必须加锁或使用原子操作,确保数据一致性。

  3. 合理使用线程池:通过线程池复用线程,避免频繁创建 / 销毁线程的开销(如 Java 的ExecutorService、Python 的concurrent.futures)。

  4. 处理线程阻塞:避免线程长时间阻塞(如死锁、无限制等待),可设置超时机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值