线程、多线程以及线程池的关系与用法

线程

  • 定义:线程是程序执行的最小单元,一个进程可以包含多个线程。
  • 创建方式
    • 继承 Thread 类。
    • 实现 Runnable 接口。
    • 实现 Callable 接口(带返回值,这个由用户自己定返回的数据)。
  • 特点:每个线程独立运行,共享进程资源。

多线程任务的常见创建方式

在没有线程池的情况下,创建线程主要有以下几种方式:

(1) 继承 Thread

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

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}
  • 这种方式直接继承 Thread 类并重写 run() 方法
  • 缺点:Java 不支持多继承,如果类已经继承了其他类,则无法再继承 Thread

(2) 实现 Runnable 接口

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Task running: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyTask());
        thread.start(); // 启动线程
    }
}
  • 这种方式更灵活,因为一个类可以同时实现多个接口。
  • 需要手动创建 Thread 对象并将 Runnable 实例传递给它。

(3) 实现 Callable 接口

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

class MyTask implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("Task running: " + Thread.currentThread().getName());
        return 42; // 返回结果
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        FutureTask<Integer> futureTask = new FutureTask<>(new MyTask());
        Thread thread = new Thread(futureTask);
        thread.start();

        System.out.println("Result: " + futureTask.get()); // 获取返回值
    }
}
  • Callable 接口允许任务返回结果,并可以抛出异常。
  • 需要与 FutureTask 配合使用。

多线程

  • 定义:通过多个线程并发或并行执行任务,实现高效处理。

方式 1:直接创建多个线程

这是最基础的多线程实现方式,通过手动创建多个线程对象,并调用 start() 方法启动线程。每个线程独立运行,任务逻辑由线程执行。

实现步骤
  1. 定义任务逻辑
    • 使用 Thread 类或 Runnable 接口定义线程的任务逻辑。
  2. 创建线程对象
    • 每个任务需要对应一个线程对象。
  3. 启动线程
    • 调用线程对象的 start() 方法,启动线程并执行任务。
代码示例
方法 1:继承 Thread
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程的任务逻辑
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在运行");
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建多个线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        // 启动线程
        t1.start(); // 启动线程 1
        t2.start(); // 启动线程 2
    }
}

输出示例(顺序可能不同,因为线程是并发执行的):

线程 Thread-0 正在运行
线程 Thread-1 正在运行
方法 2:实现 Runnable 接口
class MyTask implements Runnable {
    @Override
    public void run() {
        // 线程的任务逻辑
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在运行");
    }
}

public class Main {
    public static void main(String[] args) {
        // 创建多个线程对象
        Thread t1 = new Thread(new MyTask());
        Thread t2 = new Thread(new MyTask());

        // 启动线程
        t1.start(); // 启动线程 1
        t2.start(); // 启动线程 2
    }
}

输出示例(顺序可能不同):

线程 Thread-0 正在运行
线程 Thread-1 正在运行
方法 3:使用匿名内部类

如果任务逻辑简单,可以直接使用匿名内部类来简化代码:

public class Main {
    public static void main(String[] args) {
        // 使用匿名内部类创建线程
        new Thread(() -> {
            System.out.println("线程 1 正在运行");
        }).start();

        new Thread(() -> {
            System.out.println("线程 2 正在运行");
        }).start();
    }
}

输出示例(顺序可能不同):

线程 1 正在运行
线程 2 正在运行
关键点解析
  1. 线程启动
    • 调用 start() 方法会启动线程,并自动调用 run() 方法。
    • 不要直接调用 run() 方法,否则不会启动新线程,而是在当前线程中执行任务。
  2. 线程独立性
    • 每个线程独立运行,任务之间没有共享状态(除非显式共享变量)。
  3. 缺点
    • 频繁创建和销毁线程会导致性能开销。
    • 线程数量过多时,可能导致资源耗尽。

方式 2:使用线程池

线程池是一种更高效的多线程实现方式,通过预先创建一组线程并复用来执行任务,避免频繁创建和销毁线程。

实现步骤
  1. 创建线程池
    • 使用 Executors 工具类快速创建线程池,或者使用 ThreadPoolExecutor 自定义线程池。
  2. 提交任务
    • 将任务(RunnableCallable)提交给线程池。
  3. 关闭线程池
    • 使用 shutdown() 方法优雅地关闭线程池。
代码示例
方法 1:使用 Executors 工具类
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        // 创建固定大小为 2 的线程池
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 提交任务
        pool.submit(() -> {
            System.out.println("任务 1 正在运行,线程:" + Thread.currentThread().getName());
        });

        pool.submit(() -> {
            System.out.println("任务 2 正在运行,线程:" + Thread.currentThread().getName());
        });

        // 关闭线程池
        pool.shutdown();
    }
}

输出示例(顺序可能不同):

任务 1 正在运行,线程:pool-1-thread-1
任务 2 正在运行,线程:pool-1-thread-2
方法 2:使用 ThreadPoolExecutor 自定义线程池
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // 核心线程数
        int corePoolSize = 2;
        // 最大线程数
        int maximumPoolSize = 4;
        // 空闲线程存活时间
        long keepAliveTime = 60L;
        TimeUnit unit = TimeUnit.SECONDS;
        // 任务队列
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(10);
        // 线程工厂
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        // 拒绝策略
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );

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

        // 关闭线程池
        executor.shutdown();
    }
}

输出示例(顺序可能不同):

任务 1 正在运行,线程:pool-1-thread-1
任务 2 正在运行,线程:pool-1-thread-2
任务 3 正在运行,线程:pool-1-thread-1
任务 4 正在运行,线程:pool-1-thread-2
任务 5 正在运行,线程:pool-1-thread-1
关键点解析
  1. 线程复用
    • 线程池中的线程会被复用,减少线程创建和销毁的开销。
  2. 任务队列
    • 当线程池中的线程都在忙碌时,新任务会被放入任务队列等待。
  3. 拒绝策略
    • 如果任务队列已满且线程池达到最大线程数,新任务会被拒绝,触发拒绝策略。
  4. 优雅关闭
    • 使用 shutdown() 方法可以等待所有任务完成后关闭线程池。
    • 使用 shutdownNow() 方法会尝试立即停止所有任务。

直接创建多个线程适合小规模任务,使用线程池适合大规模任务或需要高效管理线程的场景。


线程池

  • 定义:一种管理线程的工具,预先创建一组线程,复用它们来执行多个任务,旨在提高程序性能和资源利用率。
  • 优点
    • 避免频繁创建和销毁线程,减少性能开销。
    • 提供任务队列、拒绝策略等高级功能。
  • 常见线程池
    • 固定大小线程池:Executors.newFixedThreadPool(n)
    • 单线程池:Executors.newSingleThreadExecutor()
    • 可缓存线程池:Executors.newCachedThreadPool()
    • 定时任务线程池:Executors.newScheduledThreadPool(n)

在 Java 中,java.util.concurrent 包提供了多种方式来创建和管理线程池,以下是几种常见的线程池创建方法:

1. 使用 Executors 工具类

Executors 是一个线程池工厂类,提供了多种静态方法来创建不同类型的线程池。这些方法简单易用,但在某些场景下可能导致资源浪费或性能问题(如固定大小的线程池可能导致任务堆积)。

(1) 创建固定大小的线程池

ExecutorService executor = Executors.newFixedThreadPool(5);
  • 特点:线程池中始终保持固定数量的线程(这里是 5 个),超出任务会被放入队列等待
  • 适用场景:适用于任务量相对稳定、需要控制并发数的场景。

(2) 创建单线程的线程池

ExecutorService executor = Executors.newSingleThreadExecutor();
  • 特点:线程池中只有一个线程,所有任务按顺序执行
  • 适用场景:适用于需要保证任务按顺序执行且无需并发的场景。

(3) 创建可缓存的线程池

ExecutorService executor = Executors.newCachedThreadPool();
  • 特点:线程池会根据需要动态创建线程,空闲线程会被回收(默认 60 秒无任务时回收)。
  • 适用场景:适用于大量短期异步任务的场景,但不适合长期运行的任务,因为可能会导致线程过多。

(4) 创建定时任务线程池

ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
  • 特点:支持定时任务和周期性任务
  • 适用场景:适用于需要调度任务的场景。

2. 使用 ThreadPoolExecutor 手动创建线程池

虽然 Executors 提供了便捷的线程池创建方法,但在实际开发中,推荐使用 ThreadPoolExecutor 来手动创建线程池,因为它提供了更细粒度的配置选项

示例代码:

import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 核心线程数
        int corePoolSize = 5;
        // 最大线程数
        int maximumPoolSize = 10;
        // 空闲线程存活时间
        long keepAliveTime = 60L;
        // 时间单位
        TimeUnit unit = TimeUnit.SECONDS;
        // 任务队列
        BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(100);
        // 线程工厂
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        // 拒绝策略
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

        // 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );

        // 提交任务
        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

参数解析:

  1. corePoolSize:核心线程数,线程池中保持的最小线程数
  2. maximumPoolSize:最大线程数,当任务队列满时,线程池最多可以扩展到的线程数
  3. keepAliveTime空闲线程的存活时间,超过这个时间后,多余的非核心线程会被回收。
  4. unitkeepAliveTime时间单位
  5. workQueue任务队列,用于存放待执行的任务
  6. threadFactory线程工厂,用于创建线程。
  7. handler拒绝策略,当线程池无法处理新任务时的处理方式。

3. 常见的拒绝策略

当线程池和任务队列都满时,新的任务将被拒绝,此时会触发拒绝策略。以下是几种常见的拒绝策略:

  • AbortPolicy(默认):抛出 RejectedExecutionException 异常
  • CallerRunsPolicy:由调用线程执行该任务。
  • DiscardPolicy直接丢弃任务,不抛异常。
  • DiscardOldestPolicy丢弃队列中最老的任务,然后尝试重新提交新任务。

什么是拒绝策略?

当线程池和任务队列都已满时(即:1. 线程池中的线程数已经达到最大线程数。2. 任务队列已满),新的任务无法被处理。此时,线程池会根据配置的 拒绝策略 决定如何处理新提交的任务。

拒绝策略具体应用

1. AbortPolicy(默认策略)
  • 行为:直接抛出 RejectedExecutionException 异常,表示任务被拒绝。
  • 代码示例
    ExecutorService executor = new ThreadPoolExecutor(
        1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy() // 默认策略
    );
    
    executor.submit(() -> {
        try {
            Thread.sleep(1000); // 模拟任务耗时
        } catch (InterruptedException e) {}
        System.out.println("Task 1");
    });
    
    executor.submit(() -> System.out.println("Task 2"));
    executor.submit(() -> System.out.println("Task 3")); // 超过容量,触发拒绝策略
    
  • 输出结果
    Task 1
    Task 2
    Exception in thread "main" java.util.concurrent.RejectedExecutionException: ...
    
  • 适用场景
    • 希望在任务无法执行时立即发现问题并处理异常。
    • 适用于对任务丢失零容忍的场景。
2. CallerRunsPolicy
  • 行为:由调用线程(提交任务的线程)执行该任务。
  • 代码示例
    ExecutorService executor = new ThreadPoolExecutor(
        1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.CallerRunsPolicy()
    );
    
    executor.submit(() -> {
        try {
            Thread.sleep(1000); // 模拟任务耗时
        } catch (InterruptedException e) {}
        System.out.println("Task 1");
    });
    
    executor.submit(() -> System.out.println("Task 2"));
    executor.submit(() -> System.out.println("Task 3")); // 超过容量,由主线程执行
    
  • 输出结果
    Task 1
    Task 2
    Task 3
    
  • 特点
    • 提交任务的线程(如主线程)会被阻塞,直到它完成任务的执行。
    • 这是一种“降级”机制,可以防止任务丢失。
  • 适用场景
    • 适用于任务量波动较大的场景,允许任务稍微延迟执行
    • 不适合对性能要求极高的场景,因为调用线程可能会被阻塞。
3. DiscardPolicy
  • 行为:直接丢弃任务,且不抛出任何异常。
  • 代码示例
    ExecutorService executor = new ThreadPoolExecutor(
        1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.DiscardPolicy()
    );
    
    executor.submit(() -> {
        try {
            Thread.sleep(1000); // 模拟任务耗时
        } catch (InterruptedException e) {}
        System.out.println("Task 1");
    });
    
    executor.submit(() -> System.out.println("Task 2"));
    executor.submit(() -> System.out.println("Task 3")); // 被丢弃,无任何提示
    
  • 输出结果
    Task 1
    Task 2
    
  • 特点
    • 任务被静默丢弃,不会有任何反馈
    • 适用于对任务丢失不敏感的场景。
  • 适用场景
    • 任务优先级较低,或者任务丢失对系统影响较小的情况。
    • 不适合需要高可靠性的场景。
4. DiscardOldestPolicy
  • 行为:丢弃任务队列中最老的任务(最早进入队列的任务),然后尝试重新提交当前任务。
  • 代码示例
    ExecutorService executor = new ThreadPoolExecutor(
        1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.DiscardOldestPolicy()
    );
    
    executor.submit(() -> {
        try {
            Thread.sleep(1000); // 模拟任务耗时
        } catch (InterruptedException e) {}
        System.out.println("Task 1");
    });
    
    executor.submit(() -> System.out.println("Task 2"));
    executor.submit(() -> System.out.println("Task 3")); // 触发 DiscardOldestPolicy
    
  • 输出结果
    Task 1
    Task 3
    
  • 特点
    • 最老的任务被丢弃,新任务有机会被执行。
    • 可能导致某些任务永远无法执行。
  • 适用场景
    • 任务有时间敏感性,较新的任务比旧任务更重要
    • 不适合对任务完整性有严格要求的场景。

触发条件

拒绝策略会在以下情况下触发:

  1. 线程池已达到最大线程数:线程池中的线程都在忙碌。
  2. 任务队列已满:新任务无法加入队列等待。

总结对比

策略名称行为适用场景
AbortPolicy抛出异常,拒绝任务需要快速发现问题并对异常进行处理的场景
CallerRunsPolicy由调用线程执行任务允许任务稍有延迟,但不能丢失的场景
DiscardPolicy静默丢弃任务对任务丢失不敏感的场景
DiscardOldestPolicy丢弃最老的任务,重试新任务新任务比旧任务更重要的场景

4. 注意事项

  1. 避免使用 Executors 默认线程池
    Executors 提供的线程池在某些场景下可能导致内存泄漏或性能问题,例如 newFixedThreadPoolnewSingleThreadExecutor 使用的是无界队列,可能会导致任务堆积,最终耗尽内存。

  2. 合理设置线程池参数
    根据任务类型(CPU 密集型或 I/O 密集型)和硬件资源(CPU 核心数)合理设置线程池大小。例如:

    • CPU 密集型任务:线程数 ≈ CPU 核心数 + 1。
    • I/O 密集型任务:线程数可以适当增加,以充分利用等待时间。
  3. 及时关闭线程池
    使用完线程池后,务必调用 shutdown()shutdownNow() 方法释放资源。

5. 线程池中的任务提交方式

在线程池中,任务的提交方式主要依赖于 RunnableCallable 接口,而不是直接创建线程。以下是两种常见的任务提交方式:

(1) 提交 Runnable 任务

ExecutorService executor = Executors.newFixedThreadPool(5);

executor.submit(() -> {
    System.out.println("Task running: " + Thread.currentThread().getName());
});

executor.shutdown();
  • Runnable 任务不返回结果,适用于不需要获取任务执行结果的场景。

(2) 提交 Callable 任务

ExecutorService executor = Executors.newFixedThreadPool(5);

Future<Integer> future = executor.submit(() -> {
    System.out.println("Task running: " + Thread.currentThread().getName());
    return 42; // 返回结果
});

System.out.println("Result: " + future.get()); // 获取返回值

executor.shutdown();
  • Callable 任务返回结果,适用于需要获取任务执行结果的场景。
  • Future 对象用于获取任务的返回值或检查任务状态

使用线程池时,不需要手动创建线程。线程池已经为你管理了线程的生命周期,你只需要关注任务逻辑即可。具体来说:

  • 如果任务实现了 Runnable 接口,可以通过 submit()execute() 方法提交任务。
  • 如果任务实现了 Callable 接口,可以通过 submit() 方法提交任务,并通过 Future 获取结果

换句话说,在使用线程池的情况下,传统的创建线程的方式(如继承 Thread 类或直接创建 Thread 对象)已经被线程池的机制所取代。


关系总结

  • 线程 是基础,是多线程的基本组成单位。
  • 多线程 是目标,通过多个线程并发执行任务。
  • 线程池 是多线程的优化工具,用于高效管理线程。

为什么推荐使用线程池?

  1. 性能优化
    线程的创建和销毁开销较大,线程池通过复用线程显著减少了这种开销。

  2. 资源控制
    线程池可以限制并发线程数,避免系统资源耗尽。

  3. 任务管理
    线程池提供了任务队列、拒绝策略等机制,方便管理和调度任务。

  4. 代码简洁
    使用线程池后,代码更加清晰,无需手动管理线程的生命周期。

通俗比喻

  • 线程:像工人,负责干活。
  • 多线程:雇佣多个工人同时干活。
  • 线程池:提前组建一个工人团队,按需分配任务,避免频繁招聘和辞退。

线程是基础,多线程是目标,线程池是实现多线程的高效工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值