天天看点

线程池使用

线程池

线程池是一种常用的并发编程技术,它可以在程序启动时创建一定数量的线程,并且将这些线程保存在一个池子中,等待来自任务队列的任务。当一个任务到来时,线程池中的一个空闲线程会被分配给该任务,执行完任务后,该线程又会返回到线程池中,等待下一个任务的到来。通过复用线程,线程池可以避免线程的频繁创建和销毁,提高程序的性能和效率。

Executors

Java中的Executors类提供了一些简单的工厂方法,用于创建常见的线程池。这些工厂方法返回的线程池都是ThreadPoolExecutor的实例,可以根据需要进行自定义配置。以下是一些常用的工厂方法:

1. newFixedThreadPool(int nThreads):创建一个固定大小的线程池,池子中有nThreads个线程。

2. newCachedThreadPool():创建一个可缓存的线程池,池子中的线程数可以根据需要自动增加或减少。

3. newSingleThreadExecutor():创建一个只有一个线程的线程池,所有任务都在同一个线程中按顺序执行。

4. newScheduledThreadPool(int corePoolSize):创建一个固定大小的线程池,用于执行定时任务和周期性任务。

Executors类还提供了一些其他的工厂方法,比如newWorkStealingPool、newSingleThreadScheduledExecutor等,可以根据具体需求进行选择。

需要注意的是,虽然Executors类提供了一些便捷的工厂方法,但是在实际应用中,我们仍然需要根据具体情况进行适当的配置和调整,以保证线程池的性能和稳定性。特别是在创建可缓存线程池时,需要注意控制线程数,避免线程数量过多导致系统资源耗尽。

ThreadFactory详解

ThreadFactory是Java中的一个接口,用于创建新的线程。它定义了一个方法newThread(Runnable r),用于创建并返回一个新的Thread对象,传入的Runnable对象则作为线程的执行体。ThreadFactory通常用于自定义线程池中的线程创建过程,可以通过它来设置线程的名称、优先级、是否为守护线程等。

ThreadFactory接口只有一个抽象方法newThread(Runnable r),因此可以使用Lambda表达式或方法引用来实现。例如,可以使用匿名内部类来实现ThreadFactory接口:

ThreadFactory threadFactory = new ThreadFactory() {
    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setName("MyThread-" + t.getId());
        t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
};           

上述代码创建了一个自定义的ThreadFactory对象,通过重写newThread方法来创建新的线程。在这个例子中,新建的线程会被命名为"MyThread-"加上线程的唯一标识符,设置为普通优先级。这个ThreadFactory对象可以被用于创建线程池中的线程。

在使用线程池时,很多情况下需要自定义ThreadFactory来创建线程。这样可以更好地掌控线程的创建过程,从而更好地控制线程的性能和行为。

总之,ThreadFactory是一个用于创建新线程的接口,通过实现它的newThread方法来自定义线程的创建过程。它通常用于自定义线程池中的线程创建过程,可以通过它来设置线程的名称、优先级、是否为守护线程等。

ThreadPoolExecutor源码分析

ThreadPoolExecutor是Java中实现线程池的一个类,它提供了灵活的线程池实现机制,可以通过配置不同的参数来调整线程池的大小、任务队列和拒绝策略等。下面对ThreadPoolExecutor的源码进行分析。

  1. 构造函数

ThreadPoolExecutor有多个构造函数,其中最常用的是以下构造函数:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 
                           TimeUnit unit, BlockingQueue<Runnable> workQueue) { ... }           

该构造函数接收5个参数,分别是:

  • corePoolSize:线程池中的核心线程数,即线程池中始终存在的线程数。
  • maximumPoolSize:线程池中允许的最大线程数。
  • keepAliveTime:非核心线程的闲置时间,超过该时间就会被回收。
  • unit:keepAliveTime的时间单位。
  • workQueue:任务队列,用于存放等待执行的任务。
  1. 线程池状态

ThreadPoolExecutor定义了一个内部枚举类,用于表示线程池的状态,包括RUNNING、SHUTDOWN、STOP、TIDYING和TERMINATED。线程池的状态可以通过一个volatile字段ctl来表示,该字段使用高3位表示线程池的状态,低29位表示工作线程的数量。

  1. 提交任务

ThreadPoolExecutor的submit方法用于提交一个任务,它会将任务包装成一个FutureTask对象,并将其加入到任务队列中。如果当前线程池中的线程数小于corePoolSize,则会创建一个新的工作线程来执行任务,否则会将任务加入到任务队列中等待执行。如果任务队列已满,且当前线程池中的线程数小于maximumPoolSize,则会创建新的工作线程来执行任务。如果线程池已经关闭,则会拒绝任务。

  1. 执行任务

线程池中的工作线程会不断从任务队列中取出任务并执行,直到线程池关闭或者发生异常。ThreadPoolExecutor使用一个内部类Worker来表示工作线程,它实现了Runnable接口,并包含了一个Thread对象,用于执行任务。Worker在执行任务时会调用runWorker方法,该方法会循环从任务队列中获取任务并执行,直到线程池关闭或者发生异常。

  1. 线程池关闭

当线程池调用shutdown方法时,线程池的状态会变为SHUTDOWN,此时线程池会停止接收新的任务,并等待已提交的任务执行完毕。当线程池调用shutdownNow方法时,线程池的状态会变为STOP,此时线程池会尝试停止所有正在执行的任务,并返回未执行的任务。当线程池中的所有任务都执行完毕后,线程池的状态会变为TERMINATED。

  1. 拒绝策略

当任务队列已满并且线程池中的线程数已经达到maximumPoolSize时,ThreadPoolExecutor会拒绝任务。ThreadPoolExecutor提供了4种拒绝策略,分别是AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。其中AbortPolicy是默认的拒绝策略,它会抛出RejectedExecutionException异常。

以上是ThreadPoolExecutor的源码分析,通过分析ThreadPoolExecutor的源码,我们可以深入理解线程池的实现机制,并且可以根据实际需求来灵活地配置线程池的参数和拒绝策略。

线程池的实现原理

可以使用一个线程来实现很多任务的处理,具体实现方式可以通过以下两种方式实现:

  1. 任务队列:可以将需要执行的任务放入一个任务队列中,然后由一个线程循环从任务队列中取出任务进行处理。这种方式可以减少线程的创建和销毁,提高线程的复用率和系统的性能。例如,下面是一个简单的示例代码,演示了如何使用任务队列来实现多任务处理:
// 创建任务队列
LinkedBlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();

// 创建处理任务的线程
Thread thread = new Thread(() -> {
      while (true) {
        // 从任务队列中取出一个任务进行处理
        Runnable task = null;
        try {
          task = taskQueue.take();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        task.run();
      }
 });

// 启动线程
thread.start();

// 提交任务
taskQueue.add(() -> {
    // 第一个任务的处理逻辑
});
taskQueue.add(() -> {
    // 第二个任务的处理逻辑
});           

在上述代码中,创建了一个任务队列taskQueue,并使用一个线程来处理任务。线程会不断地从任务队列中取出任务进行处理,直到任务队列为空。然后,通过向任务队列中添加任务的方式来提交任务。

  1. 线程池:可以使用线程池来管理和调度多个任务的执行,将多个任务分配给线程池中的多个线程来并发执行。线程池可以充分利用系统资源,提高系统的性能和并发能力。例如,下面是一个简单的示例代码,演示了如何使用线程池来实现多任务处理:
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(10);

// 提交任务
executor.submit(() -> {
    // 第一个任务的处理逻辑
});
executor.submit(() -> {
    // 第二个任务的处理逻辑
});

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

在上述代码中,创建了一个具有10个线程的线程池,然后通过executor.submit方法来提交任务,线程池会自动调度可用的线程来执行任务。最后,通过executor.shutdown方法来关闭线程池。

需要注意的是,无论是使用任务队列还是线程池来实现多任务处理,都需要考虑线程安全和并发访问的问题,以避免出现线程安全问题。同时,还需要根据具体的业务场景和性能需求来选择适合的实现方式。

线程池状态对应的值

线程池中的 ctl 变量是一个原子整数,用于存储线程池的状态和线程数量信息。具体来说,线程池的状态信息存储在 ctl 变量的高 3 位,线程数量信息存储在 ctl 变量的低 29 位。

线程池对应的值如下:

- RUNNING:表示线程池正在运行中,此时 ctl 变量的高 3 位为 111(即 -1 << 29),低 29 位表示线程数量信息。

- SHUTDOWN:表示线程池已经关闭,不再接受新的任务,但是会执行已经提交的任务,此时 ctl 变量的高 3 位为 000(即 0),低 29 位表示线程数量信息。

- STOP:表示线程池已经关闭,不再接受新的任务,也不会执行已经提交的任务,此时 ctl 变量的高 3 位为 001(即 1 << 29),低 29 位表示线程数量信息。

- TIDYING:表示线程池正在关闭中,所有任务已经执行完毕,但是还有一些线程正在执行清理操作,此时 ctl 变量的高 3 位为 010(即 2 << 29),低 29 位表示线程数量信息。

- TERMINATED:表示线程池已经彻底关闭,所有任务已经执行完毕,所有线程也已经终止,此时 ctl 变量的高 3 位为 011(即 3 << 29),低 29 位表示线程数量信息。

Executor 和 Executors 的区别

Executor 是一个接口,而 Executors 是一个工具类。

Executor 接口定义了一种用于执行任务的标准接口,它只包含一个方法 execute(Runnable command),用于执行一个 Runnable 类型的任务。Executors 则是一个工具类,它提供了一些静态方法来创建不同类型的 Executor 对象。

ExecutorService 是 Executor 接口的子接口,它们之间的关系是 ExecutorService 继承了 Executor 接口,并且在 Executor 接口的基础上提供了更丰富的线程管理功能。

线程池中 submit() 和 execute() 方法的区别

线程池中的 submit() 和 execute() 方法都可以用来提交任务给线程池执行,它们的主要区别如下:

1. 返回值不同:

submit() 方法会返回一个 Future 对象,可以用来获取任务执行的结果或取消任务的执行。而 execute() 方法没有返回值。

2. 抛出异常不同:

submit() 方法会将任务执行过程中抛出的异常封装在 Future 对象中,可以在调用 Future.get() 方法时获取到异常并处理。而 execute() 方法会将异常直接抛到调用者线程中,需要在任务代码中捕获并处理。

3. 任务类型不同:

submit() 方法可以提交 Runnable 类型的任务和 Callable 类型的任务,而 execute() 方法只能提交 Runnable 类型的任务。

4. 执行方式不同:

submit() 方法可以异步执行任务并返回 Future 对象,可以使用 Future.get() 方法阻塞等待任务执行完成。而 execute() 方法是同步执行任务,任务完成后才会返回,不能使用 Future.get() 方法阻塞等待任务执行完成。

总的来说,submit() 方法可以提交 Callable 类型的任务,并且可以获取任务执行的结果或取消任务的执行,适用于需要异步执行任务并获取结果的场景。而 execute() 方法只能提交 Runnable 类型的任务,并且是同步执行任务,适用于不需要获取任务执行结果的场景。在实际应用中,我们需要根据具体的需求和场景来选择适合的方法来提交任务给线程池执行。

线程组,为什么在 Java 中不推荐使用

Java 中的线程组(ThreadGroup)是一种用于管理线程的机制,它可以将多个线程归为一组,并且可以对整个线程组进行操作,例如设置线程组的优先级、中断线程组中的所有线程等。但是在 Java 中,并不推荐使用线程组,主要有以下几个原因:

1. 线程组的层次结构不够灵活:线程组的层次结构是树形结构,但是在实际应用中,往往需要更加灵活的线程组结构。例如,如果需要将线程分组并且允许一个线程同时属于多个线程组,那么线程组就无法满足这种需求。

2. 线程组的功能受限:线程组提供的功能相对较少,只能设置线程组的优先级、中断线程组中的所有线程等,而不能实现更加复杂的功能,例如限制线程组中的线程数量、设置线程组的执行顺序等。

3. 线程组的性能问题:线程组在创建和管理线程时会带来一定的性能开销,尤其是在大规模应用中使用线程组可能会导致严重的性能问题。

4. 线程组的安全问题:线程组的安全性问题也比较突出,如果线程组没有被正确地管理和使用,可能会导致线程安全问题的发生。

总的来说,虽然线程组提供了一种管理线程的机制,但是在实际应用中,由于线程组的层次结构不够灵活、功能受限、性能问题和安全问题等方面的原因,Java 中并不推荐使用线程组,而是推荐使用更加灵活和功能强大的线程池来管理线程。