【多线程】Java多线程基础(14)- 使用线程池

51 阅读6分钟

线程池

什么是线程池

线程池是一种管理和复用线程的技术,它在应用程序启动时创建一组线程,并将这些线程保存在一个池中,当需要处理任务时,从线程池中获取一个空闲线程来执行任务,并在任务完成后将线程放回池中

线程池有哪几种

根据不同的实现方式和功能特性,线程池可以分为以下几种:

  1. 固定大小线程池(FixedThreadPool):线程池的大小固定,一旦创建就不会改变。当有任务提交到线程池中时,线程池中有空闲线程时就会立即执行任务,如果没有空闲线程,则任务会被暂时存储在任务队列中,直到线程池中有空闲线程时再执行任务。
  2. 可缓存线程池(CachedThreadPool):线程池的大小不固定,可以根据需要自动调整。当有任务提交到线程池中时,如果线程池中有空闲线程,则会立即执行任务,如果没有空闲线程,则会创建新的线程来执行任务。当线程空闲一段时间后,如果没有任务需要执行,则会被销毁,从而减少系统资源的占用。
  3. 单线程池(SingleThreadExecutor):线程池中只有一个线程,所有的任务都是串行执行的。当一个任务执行完成后,才会执行下一个任务。单线程池通常用于需要按照顺序执行任务的场景,如事件队列处理、日志处理等。
  4. 定时线程池(ScheduledThreadPool):线程池可以按照一定的时间间隔或者固定的延迟时间周期性地执行任务。定时线程池通常用于定时任务或者周期性任务的场景。
  5. 分离式线程池(WorkStealingPool):线程池中的线程可以根据任务类型和负载情况自动调整数量,从而提高系统的效率和性能。分离式线程池通常用于需要处理大量任务的场景,如并行计算、图像处理等。

线程池的使用

Java标准库中的各种线程池实现都实现了ExecutorService接口,该接口继承了Executor接口,提供了更多的方法用于管理和控制线程池的执行。

submit()方法是ExecutorService接口中定义的一个方法,用于向线程池提交CallableRunnable类型的任务,返回一个Future对象,用于获取任务的执行结果或者取消任务的执行。

提交任务也可以使用execute()方法,下面对这两个方法进行区分。

submit() 与 execute() 区分

ThreadPoolExecutor.execute() 方法没有返回值,只能提交一个 Runnable 任务。如果任务执行过程中发生了异常,线程池会将异常记录下来并输出日志,但不会将异常抛出给调用者。

ThreadPoolExecutor.submit() 方法可以提交一个 Runnable 或 Callable 任务,并返回一个 Future 对象。Future 对象可以用来获取任务的执行结果,或者在任务执行过程中等待任务完成。如果任务执行过程中发生了异常,Future 对象会将异常包装在 ExecutionException 中并抛出给调用者。调用者可以通过捕获 ExecutionException 来处理任务执行过程中的异常。

因此,submit() 方法相对于 execute() 方法有以下优势:

  1. 可以获取任务执行结果:submit() 方法返回一个 Future 对象,可以通过该对象获取任务的执行结果。
  2. 可以捕获任务执行过程中的异常:submit() 方法将任务执行过程中的异常包装在 ExecutionException 中并抛出给调用者,调用者可以通过捕获 ExecutionException 来处理异常。

我们使用线程池是使用接口去指向它,比如

// 创建一个固定大小为5的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务
executor.submit(new RunnableTask());
// 关闭线程池
executor.shutdown();

案例

// 这是一个使用newFixedThreadPool的例子,

public class Main {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池:
        ExecutorService es = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 6; i++) {
            es.submit(new Task("任务" + i));
        }
        // 关闭线程池:
        es.shutdown();
    }
}
class Task implements Runnable {
    private final String name;

    public Task(String name) {
        this.name = name;
    }
    @Override
    public void run() {
        System.out.println(name + "开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(name + "结束");
    }
}

输出:

任务3开始
任务0开始
任务1开始
任务2开始
任务2结束
任务3结束
任务0结束
任务1结束
任务4开始
任务5开始
任务4结束
任务5结束

可以看出一次启动了四个线程, 而另外两个线程是等待前面任务完毕之后才启动的。

// 使用可缓存线程池,线程没有空闲的,就新建。

public class Main {
    public static void main(String[] args) {
        // 创建一个带缓存的线程池:
        ExecutorService es = Executors.newCachedThreadPool();
        for (int i = 0; i < 6; i++) {
            es.submit(new Task("任务" + i));
        }
        // 关闭线程池:
        es.shutdown();
    }
}

使用定时缓存池

它的使用:

ScheduledThreadPool可以用于需要定期执行的任务,例如定时备份、定时清理、定时推送等。在使用ScheduledThreadPool时,您需要遵循以下步骤:

  1. 创建ScheduledThreadPoolExecutor实例,指定线程池大小和其他参数。
  2. 创建Runnable或Callable对象,表示要执行的任务。
  3. 使用schedule()方法将任务提交到ScheduledThreadPoolExecutor,并指定任务的执行时间或延迟时间。
  4. 如果任务是重复执行的任务,则可以使用*scheduleAtFixedRate()scheduleWithFixedDelay()*方法指定任务的重复执行间隔。(那就不要关闭线程池了,或者设定时间关闭线程池)
  5. 如果需要取消任务,可以使用ScheduledFuture对象的cancel()方法取消任务的执行。
public class Main {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池:
        ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
        for (int i = 0; i < 6; i++) {
            ses.scheduleAtFixedRate(new Task("one-time"),1 ,2, TimeUnit.SECONDS);
        }
        // 关闭线程池:
//        ses.shutdown();
    }
}

线程池的主要优点包括

  1. 降低线程创建和销毁的开销。线程的创建和销毁会消耗大量的系统资源,使用线程池可以避免这些开销,提高系统的性能和稳定性。它内部其实是使用完当前线程将其加入到空闲队列,除非超过keepalive时间才会销毁
  2. 提高线程的复用率。线程池可以复用已经创建的线程来执行任务,避免了频繁创建和销毁线程的开销,提高了线程的复用率和系统的效率。
  3. 控制并发数。线程池可以控制同时执行的线程数量,避免过多的线程竞争资源导致系统性能下降,同时也可以避免因为过多的线程导致系统崩溃。
  4. 提高系统的响应速度。线程池可以在任务到达时立即执行,提高了系统的响应速度和效率。

线程池的实现通常包括以下几个组件:

  1. 任务队列。用于存储待执行的任务,包括线程池中正在执行的任务和待执行的任务。
  2. 线程池管理器。用于创建、管理和销毁线程池中的线程,包括线程的创建、销毁、启动和停止等操作。
  3. 线程工厂。用于创建线程池中的线程,通常包括线程名称、优先级、是否为守护线程等属性。
  4. 线程池执行器。用于从任务队列中获取待执行的任务,并将任务分配给线程池中的空闲线程来执行。

线程池是多线程编程中常用的技术之一,可以提高系统的效率和稳定性。但是,在使用线程池时需要注意线程安全、资源竞争等问题,以避免线程池成为系统的瓶颈。