线程池的理解

为什么要有线程池

对于服务端的程序,经常面对的是客户端传入的短小(执行时间短、工作内容较为单一) 任务,需要服务端快速处理并返回结果。如果服务端每次接受到一个任务,创建一个线程,然 后进行执行,这在原型阶段是个不错的选择,但是面对成千上万的任务递交进服务器时,如果 还是采用一个任务一个线程的方式,那么将会创建数以万记的线程,这不是一个好的选择。
因 为这会使操作系统频繁的进行线程上下文切换,无故增加系统的负载,而线程的创建和消亡 都是需要耗费系统资源的,也无疑浪费了系统资源。
线程池技术能够很好地解决这个问题,它预先创建了若干数量的线程,并且不能由用户 直接对线程的创建进行控制,在这个前提下重复使用固定或较为固定数目的线程来完成任务 的执行。
这样做的好处是,一方面,消除了频繁创建和消亡线程的系统资源开销,另一方面, 面对过量任务的提交能够平缓的劣化。

线程池的本质

线程池的本质就是使用了一个线程安全的工作队列连接工作者线程和客户端 线程,客户端线程将任务放入工作队列后便返回,而工作者线程则不断地从工作队列上取出 工作并执行。
当工作队列为空时,所有的工作者线程均等待在工作队列上,当有客户端提交了 一个任务之后会通知任意一个工作者线程,随着大量的任务被提交,更多的工作者线程会被 唤醒。
其实java线程池的实现原理很简单,说白了就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。

Execute原理

当一个任务提交至线程池之后:

  1. 线程池首先当前运行的线程数量是否少于corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,则进入2.
  2. 判断BlockingQueue是否已经满了,倘若还没有满,则将线程放入BlockingQueue。否则进入3.
  3. 如果创建一个新的工作线程将使当前运行的线程数量超过maximumPoolSize,则交给RejectedExecutionHandler来处理任务。
    例子:也就是说有人去银行,银行有三个工作人员,如果现在服务人数少于3,就直接去找工作人员
    如果工作人员都在服务,就去排队,排队银行只能排10人,如果这个时候已经满10人在排队了,就跳3由handler处理。
    当ThreadPoolExecutor创建新线程时,通过CAS来更新线程池的状态ctl.
    CAS是一种非阻塞算法,用于实现多线程环境下的原子操作。在ThreadPoolExecutor创建新线程时,会先检查当前线程池的状态ctl,然后根据需要创建新的线程,并通过CAS来更新线程池的状态ctl。这个过程确保了线程池状态的正确性和并发性,避免了多线程环境下的竞争和冲突,从而保证了线程池的稳定性和性能。

七个参数

可以通过new ThreadPoolExecutor()然后按ctrl键看源码

@NotNull是一种Java注解,用于标记被注解的参数、字段、方法返回值等不能为null。在Java中,如果一个变量可以为null,那么在使用它时需要进行非空判断,否则可能会出现空指针异常。使用@NotNull注解可以帮助开发者在编译时检查代码中的空指针问题,从而提高代码的健壮性和可维护性。

corePoolSize( 核心线程数 )

线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize=5, 即使有其他空闲线程能够执行新来的任务, 也会继续创建线程;
如果当前线程数为corePoolSize=5,继续提交的任务被保存到阻塞队列中,等待被执行;
如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。

maximumPoolSize ( 最大线程数 )

线程池中允许的最大线程数。

workQueue ( 任务队列 )

用于保存等待执行的任务的阻塞队列。在这里插入图片描述在这里插入图片描述● workQueue 用来保存等待被执行的任务的阻塞队列. 在JDK中提供了如下阻塞队列:
○ ArrayBlockingQueue: 基于数组结构的有界阻塞队列,按FIFO排序任务;
○ LinkedBlockingQueue: 基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQueue;
○ SynchronousQueue: 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue;
○ PriorityBlockingQueue: 具有优先级的无界阻塞队列;
LinkedBlockingQueue比ArrayBlockingQueue在插入删除节点性能方面更优,但是二者在put(), take()任务的时均需要加锁,SynchronousQueue使用无锁算法,根据节点的状态判断执行,而不需要用到锁,其核心是Transfer.transfer().
什么是FIFO
FIFO(First In First Out)排序任务是指任务按照它们被添加到任务队列中的顺序进行执行的一种任务调度方式。在FIFO排序任务中,先加入任务队列的任务会被优先执行,而后加入的任务会被放置在队列的末尾,等待前面的任务执行完成后才会被执行。
这种任务调度方式通常被用于简单的任务队列中,因为它不需要对任务进行复杂的优先级计算,而且保证了任务的执行顺序,从而避免了任务之间的相互干扰。

keepAliveTime( 空闲线程存活时间 )

线程空闲时的存活时间,即当线程没有任务执行时,该线程继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize时才有用, 超过这个时间的空闲线程将被终止;(也就是银行平时只有五个客服,节假日新增到10个,然后后面的五个人是加班的,如果他们空闲了一段时间没有任务做,就下班(终止线程))

Unit( 时间单位 )

keepAliveTime的单位
TimeUnit.SECONDS等

threadFactory ( 线程工厂 )

创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为DefaultThreadFactory
怎么自定义线程工厂
自定义线程工厂可以通过实现ThreadFactory接口来实现。ThreadFactory接口只有一个方法newThread,该方法用于创建一个新的线程。具体实现步骤如下:
4. 创建一个类,实现ThreadFactory接口。
5. 在实现的newThread方法中,创建一个新的线程,并设置线程的名称、优先级、是否为守护线程等属性。
6. 返回创建的新线程。
下面是一个简单的示例:

public class MyThreadFactory implements ThreadFactory {
    private int threadNumber = 1;

    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, "MyThread-" + threadNumber);
        thread.setPriority(Thread.MAX_PRIORITY);
        thread.setDaemon(true);
        threadNumber++;
        return thread;
    }
}

在上面的示例中,MyThreadFactory实现了ThreadFactory接口,并重写了newThread方法。在newThread方法中,创建了一个新的线程,并设置了线程的名称为"MyThread-"加上线程编号,优先级为最高,守护线程为true。每创建一个新线程,线程编号加1。

ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(100), new MyThreadFactory());

handler ( 拒绝策略 )

线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
● AbortPolicy: 直接抛出异常,默认策略;
● CallerRunsPolicy: 用调用者所在的线程来执行任务;(谁叫你来找谁去)
● DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;(最早排队的人给丢弃,欺负老实人)
● DiscardPolicy: 直接丢弃任务;
也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务

创建线程池三种方式

newFixedThreadPool

newFixedThreadPool是Java中的一个静态方法,用于创建一个指定数量线程的线程池。该方法返回一个ExecutorService类型的对象,可以用于提交任务、关闭线程池等操作。
具体实现中,newFixedThreadPool方法内部调用了ThreadPoolExecutor的构造方法,传递了以下参数:
● nThreads - 指定线程池中线程的数量。
● nThreads - 指定线程池中线程的最大数量,因为是固定线程池,所以最大数量和初始数量相同。
● 0L - 空闲线程存活时间,表示线程池中的线程在空闲时间超过该值时会被回收。
● TimeUnit.MILLISECONDS - 空闲线程存活时间的时间单位,这里设置为毫秒。
● new LinkedBlockingQueue() - 任务队列,用于存放等待执行的任务。这里使用了一个无界的队列LinkedBlockingQueue,表示可以不限制任务队列的长度。
通过这些参数的设置,newFixedThreadPool方法创建了一个固定大小的线程池。当线程池中的线程数量达到最大值时,新的任务会被放置在任务队列中等待执行,直到线程池中有空闲的线程可用。如果任务队列已满且线程池中的线程数量等于最大值,新的任务将被拒绝执行并抛出异常
下面是一个使用newFixedThreadPool方法创建线程池的例子:

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

public class FixedThreadPoolExample {

    public static void main(String[] args) {
        // 创建一个固定大小为5的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        // 提交10个任务
        for (int i = 1; i <= 10; i++) {
            executorService.execute(new Task(i));
        }

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

    static class Task implements Runnable {
        int taskId;

        public Task(int taskId) {
            this.taskId = taskId;
        }

        @Override
        public void run() {
            System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
        }
    }
}

在上面的示例中,首先创建了一个固定大小为5的线程池executorService,然后提交了10个任务。每个任务都是一个Task对象,Task实现了Runnable接口,用于执行具体的任务。在Task的run方法中,打印了当前任务的编号和执行该任务的线程名称。
输出结果如下:

Task 1 is running on thread pool-1-thread-1
Task 2 is running on thread pool-1-thread-2
Task 3 is running on thread pool-1-thread-3
Task 4 is running on thread pool-1-thread-4
Task 5 is running on thread pool-1-thread-5
Task 6 is running on thread pool-1-thread-1
Task 7 is running on thread pool-1-thread-2
Task 8 is running on thread pool-1-thread-3
Task 9 is running on thread pool-1-thread-4
Task 10 is running on thread pool-1-thread-5

从输出结果可以看出,线程池中最多同时执行5个任务,其余任务被放置在任务队列中等待执行。由于线程池的大小是固定的,因此线程池中的线程可以被重复利用,从而提高了线程的利用率和执行效率。
线程池的线程数量达corePoolSize后,即使线程池没有可执行任务时,也不会释放线程。

FixedThreadPool的工作队列为无界队列LinkedBlockingQueue(队列容量为Integer.MAX_VALUE), 这会导致以下问题:
● 由于工作队列是无界的,当任务过多时,可能会导致内存占用过高,甚至导致OOM(Out Of Memory)异常。
● 由于线程池中的线程数量不超过corePoolSize,因此maximumPoolSize和keepAliveTime参数将不起作用。即使任务队列中有大量的等待任务,也不会创建新的线程去执行这些任务,因为线程池中的线程数量已经达到了最大值,无法再创建新的线程。
● 由于使用了无界队列,FixedThreadPool永远不会拒绝任务,即使任务队列中已经有大量的等待任务,也不会执行饱和策略,这可能会导致系统的负载过高,出现性能问题。

newSingleThreadExecutor

newSingleThreadExecutor是Java中的一个静态方法,用于创建一个只有一个线程的线程池。该方法返回一个ExecutorService类型的对象,可以用于提交任务、关闭线程池等操作。
源码如下:

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

具体实现中,newSingleThreadExecutor方法内部调用了ThreadPoolExecutor的构造方法,传递了以下参数:
● 1 - 指定线程池中线程的数量。由于是单线程池,因此线程数量为1。
● 1 - 指定线程池中线程的最大数量。由于是单线程池,因此最大数量也为1。
● 0L - 空闲线程存活时间,表示线程池中的线程在空闲时间超过该值时会被回收。
● TimeUnit.MILLISECONDS - 空闲线程存活时间的时间单位,这里设置为毫秒。
● new LinkedBlockingQueue() - 任务队列,用于存放等待执行的任务。这里使用了一个无界的队列LinkedBlockingQueue,表示可以不限制任务队列的长度。
通过这些参数的设置,newSingleThreadExecutor方法创建了一个只有一个线程的线程池。当有新的任务提交时,该任务会被放置在任务队列中等待执行,由唯一的线程执行任务。如果任务队列中有多个任务等待执行,线程会按照任务的先后顺序依次执行。
下面是一个使用newSingleThreadExecutor方法创建线程池的例子:

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

public class SingleThreadExecutorExample {

    public static void main(String[] args) {
        // 创建一个只有一个线程的线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        // 提交5个任务
        for (int i = 1; i <= 5; i++) {
            executorService.execute(new Task(i));
        }

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

    static class Task implements Runnable {
        int taskId;

        public Task(int taskId) {
            this.taskId = taskId;
        }

        @Override
        public void run() {
            System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
        }
    }
}

在上面的示例中,首先创建了一个只有一个线程的线程池executorService,然后提交了5个任务。每个任务都是一个Task对象,Task实现了Runnable接口,用于执行具体的任务。在Task的run方法中,打印了当前任务的编号和执行该任务的线程名称。
输出结果如下:

Task 1 is running on thread pool-1-thread-1
Task 2 is running on thread pool-1-thread-1
Task 3 is running on thread pool-1-thread-1
Task 4 is running on thread pool-1-thread-1
Task 5 is running on thread pool-1-thread-1

从输出结果可以看出,线程池中只有一个线程,每个任务都是在该线程中执行的。由于任务是按照提交的顺序依次执行的,因此输出结果中任务的编号也是依次递增的。

newCachedThreadPool

newCachedThreadPool是Java中的一个静态方法,用于创建一个可根据需要创建新线程的线程池。该方法返回一个ExecutorService类型的对象,可以用于提交任务、关闭线程池等操作。
源码如下:

 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

具体实现中,newCachedThreadPool方法内部调用了ThreadPoolExecutor的构造方法,传递了以下参数:
● 0 - 指定线程池中线程的初始数量。由于是可缓存线程池,因此初始数量为0。
● Integer.MAX_VALUE - 指定线程池中线程的最大数量。由于是可缓存线程池,因此最大数量为Integer.MAX_VALUE。
● 60L - 空闲线程存活时间,表示线程池中的线程在空闲时间超过该值时会被回收。这里设置为60秒。
● TimeUnit.SECONDS - 空闲线程存活时间的时间单位,这里设置为秒。
● new SynchronousQueue() - 任务队列,用于存放等待执行的任务。这里使用了一个同步队列SynchronousQueue,表示只有在线程池中有空闲线程时才会执行任务,否则会创建新的线程。
通过这些参数的设置,newCachedThreadPool方法创建了一个可缓存线程的线程池。当有新的任务提交时,线程池会根据实际情况创建新的线程执行任务。如果线程池中有空闲线程,任务会被分配给空闲线程执行;如果线程池中没有空闲线程,会创建新的线程执行任务。如果线程池中的线程在空闲时间超过60秒,会被回收。如果任务队列中有大量等待执行的任务,也可能会导致创建大量的新线程,从而导致系统的负载过高。
使用newCachedThreadPool方法创建线程池的例子:

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

public class CachedThreadPoolExample {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();

        for (int i = 1; i <= 10; i++) {
            final int taskNumber = i;
            executor.execute(new Runnable() {
                public void run() {
                    System.out.println("Task " + taskNumber + " is running.");
                }
            });
        }

        executor.shutdown();
    }
}

这个例子创建了一个可以自动调整线程数的线程池,通过newCachedThreadPool()方法创建。然后,我们使用一个for循环来提交10个任务,每个任务都打印出自己的编号。由于线程池是自动调整的,所以它会根据任务数量自动创建或销毁线程。最后,我们调用shutdown()方法来关闭线程池。

关闭线程池

关闭线程池的方式有两种:shutdown()和shutdownNow()。
shutdown()方法会平缓地关闭线程池。它会等待已经提交的任务执行完毕,但不接受新的任务。如果在调用shutdown()方法后再提交新的任务,它们将被拒绝执行。
shutdownNow()方法会立即关闭线程池。它会尝试停止所有正在执行的任务,并返回等待执行的任务列表。如果有一些任务无法停止,则会保留这些任务并返回它们的列表。
一般来说,我们应该优先使用shutdown()方法,因为它会平缓地关闭线程池,等待正在执行的任务完成。如果我们使用shutdownNow()方法,可能会导致正在执行的任务被强制停止,可能会引起不良影响。但是,如果我们需要立即关闭线程池,可以使用shutdownNow()方法。

参考文章

《Java并发编程艺术》
https://pdai.tech/md/java/thread/java-thread-x-juc-executor- ThreadPoolExecutor.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值