2023.1.11线程池学习()

什么是线程池?
  • 举个例子,我们在操作数据库时需要先跟数据库建立连接,拿到数据库连接之后才能操作数据库,当操作完数据库后将连接关闭。创建和销毁都是比较耗时间的,但真正跟业务相关的操作都是耗时比较短的。
    每进行一个数据库操作时,都会创建连接、用完销毁,为了提升效率,后面出现了数据库连接池 。系统启动的时候创建出很多的数据库连接放到池子里,使用时直接从池子里获取一个连接,使用完成后将连接放回到池子中,中间省去了创建和销毁的时间,这样提高了整体的性能。

  • 线程池和数据库连接池的类似,使用线程去处理任务时,需要先创建线程,可能创建线程的时间比处理业务的时间还要长。如果能同数据库连接池一样,提前创建好线程,这样在使用时,直接从池子里获取,使用完不销毁线程,而是放回池子里,这样就节省了创建和销毁的时间。同事也提高了效率。

    总结:线程池能够提高响应速度、加速处理任务、降低线程创建销毁需要的系统资源 。同时线程池也能更好的管理并发线程的数量。

线程池应用场景?线程池的优点?
  • 加快请求响应(提高响应速度)
    比如用户在饿了么上查看某商家外卖,需要聚合商品库存、店家、价格、红包优惠等等信息返回给用户,接口逻辑涉及到聚合、级联等查询,从这个角度来看接口返回越快越好,那么就可以使用多线程方式,把聚合/级联查询等任务采用并行方式执行,从而缩短接口响应时间。这种场景下使用线程池的目的就是为了缩短响应时间,往往不去设置队列去缓冲并发的请求,而是会适当调高corePoolSize和maxPoolSize去尽可能的创造线程来执行任务。

  • 加速处理大量任务(提高吞吐量)
    比如业务中台每10分钟就调用接口统计每个系统/项目的PV/UV等指标然后写入多个sheet页中返回,这种情况下往往也会使用多线程方式来并行统计。和"时间优先"场景不同,这种场景的关注点不在于尽可能快的返回,而是关注利用有限的资源尽可能的在单位时间内处理更多的任务,即吞吐量优先。这种场景下我们往往会设置队列来缓冲并发任务,并且设置合理的corePoolSize和maxPoolSize参数,这个时候如果设置了太大的corePoolSize和maxPoolSize可能还会因为线程上下文频繁切换降低任务处理速度,从而导致吞吐量降低。

    以上两种使用场景和JVM里的ParallelScavengeCMS垃圾收集器有较大的类比性,ParallelScavenge垃圾收集器关注点在于达到可观的吞吐量,而CMS垃圾收集器重点关注尽可能缩短GC停顿时间。

  • 优点:

    • 降低资源消耗:通过重复利用已创建的线程来降低线程创建和线程销毁的资源消耗。
    • 提高响应速度:执行任务时,不需要再等待线程创建完成就能直接获取线程立即执行。
    • 更好的管理线程:线程是系统的稀缺资源,无限的创建线程不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以对线程进行统一管理,调优和监控。
线程池怎么创建?

线程池可以自动创建,也可以手动创建。自动创建体现在Executors工具类中,常见的有:
newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor、newScheduledThreadPool;手动创建可以更灵活的控制线程池的各项参数,体现在代码中即ThreadPoolExecutor类构造器上

ThreadPoolExecutor	通过 直接new()方式创建,包含七个参数
Executors 	通过 类名.newXxxThreadXxx()方法获取一个线程池实例

①手动建线程池:

  • ThreadPoolExecutor():最原始的创建线程池的⽅式,它包含了 7 个参数可供设置。
    构造函数:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        //下方都是校验,主要看参数
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
    

    参数:

    • corePoolSize核心线程大小(数量)。当提交了一个任务到线程池中,即使当前线程池中有空余的线程可以处理任务,但还是会创建一个新的线程来执行任务,等到工作线程数达到核心线程大小(数量)时,就不创建新的线程了。
      如果调用了线程池的prestartAllCoreThreads方法,线程池会提前按照核心线程数量将线程创建好,并启动。
    • maximumPoolSize线程池允许创建的最大线程数量。通过构造函数中的判断可以得知,该值必须大于或等于1。已创建的线程数量小于 maximumPoolSize时,会创建新的线程执行任务。注意:如果使用了无界队列,那么不管有多少任务,都会加入队列,这个参数就不起作用了。
      说明:无界队列,没有大小限制(默认是Integer.MAX_VALUE)的队列。 有界队列,有固定大小限制。
    • keepAliveTime空闲线程的存活时间。当线程数量大于corePoolSize时,并且存活时间达到keepAliveTime时间,空闲的线程会被销毁直到只剩下corePoolSize数量为止。如果任务很多,并且每个任务执行时间较短,避免线程重复创建和回收,可以调大存活时间,提高线程利用率。
    • unit:keepAliveTime时间的单位。常用的枚举类java.util.concurrent.TimeUnitTimeUnit.SECONDS
    • workQueue任务队列。被提交但是未被执行的任务,用于缓存待处理任务的阻塞队列。
      eg:
    new ArrayBlockingQueue<>(1)
    
    • threadFactory生成线程池中工作线程的线程工厂。用于创建线程,一般默认的即可,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
      eg:
    Executors.defaultThreadFactory();//默认
    Executors.privilegedThreadFactory();
    
    • handle拒绝策略。当前队列已满,并且工作线程数量大于maximumPoolSize时,如何拒绝新来的线程请求的策略。
      说明:工作队列中已经放不下任务了,并且已达到最大可创建的线程数量时。新来的任务不会放入队列中,而是会按照拒绝策略处理。
      eg:
    (r, executors) -> {
        System.out.println("被拒绝策略的任务:" + r.toString());
                })	
    

②调用工具类创建:

  • Executors.newFixedThreadPool(int corePoolSize)
    特点:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;
    构造方法:newFixedThreadPool创建的线程池corePoolSizemaximumPoolSize值是相等的,它使用的是LinkedBlockingQueue执行长期任务性能好,有固定线程数的线程池。
    说明:corePoolSize、maximumPoolSize都是ThreadPoolExecutor的参数
  • Executors.newCachedThreadPool()
    特点:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;总结:可扩容,遇强则强
  • Executors.newSingleThreadExecutor()
    特点:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;一池一线程
  • Executors.newScheduledThreadPool(int corePoolSize)
    特点:创建⼀个可以执⾏延迟任务的线程池;
  • Executors.newSingleThreadScheduledExecutor()
    特点:创建⼀个单线程的可以执⾏延迟任务的线程池;
  • Executors.newWorkStealingPool(int parallelism)
    特点:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】
线程池的关闭

线程池中提供了两种关闭方法,作用不同

  • shutdown():当线程池调用shutdown时,线程池拒绝接受新提交的任务,内部会将队列中等待执行&&进行中的任务执行完成后,线程销毁,线程池关闭。

  • shutdownNow():当线程池调用了shutdownNow时,会将队列中等待处理的任务清除,内部会将进行中的任务执行完成后,线程销毁,线程池关闭。

    当调用者两个方法之后,线程池会遍历内部的工作线程,然后调用每个工作线程的interrrupt方法给线程发送中断信号,内部如果无法响应中断信号的可能永远无法终止,所以如果内部有无限循环的,最好在循环内部检测一下线程的中断信号,合理的退出。调用者两个方法中任意一个,线程池的isShutdown方法就会返回true,当所有的任务线程都关闭之后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true

线程池的工作流程?线程的创建流程?:

在这里插入图片描述
举个栗子:现有一个线程池,corePoolSize=10,maximumPoolSize=20,队列长度为100。线程池在执行任务时会先创建出10个核心线程数。接下来新提交的任务会进入到队列中,一直到队列放满,此时会创建出对于的线程来执行任务(最多20个),如果这时还有新提交的任务就会执行拒绝策略。
用不说人话的方式说工作流程(此过程为默认顺序,也可以手动控制):

  • ①当任务提交的时候,会先判断当前线程池中的线程数量是否达到核心线程数(corePoolSize)
    • ②如果没达到核心线程数量(corePoolSize),则会继续创建新的线程来执行任务,即使现在已有空闲的线程。注意:如果调用了prestartAllCoreThreads()方法,线程池则会提前创建好核心线程数量(corePoolSize)的线程
    • ③如果已达到核心线程数量(corePoolSize),则会判断阻塞队列(workQueue)是否已满。
      • ④阻塞队列未满,则会放入阻塞队列中进行等待,等待被线程执行。
      • ⑤阻塞队列已满,此时会判断线程数量是否到达最大线程数(maximumPoolSize)。
        • ⑥阻塞队列已满并且未达到最大线程数量,则会创建新的线程去执行任务。
        • ⑦阻塞队列已满并且已达到最大线程数量,则会执行拒绝策略来处理新来的任务。

在任务不断增加的过程中,线程池会逐一进行下面四个参数的判断
核心线程数(corePoolSize)
阻塞队列、工作队列(workQueue)
最大线程数(maximumPoolSize)
拒绝策略

workQueue队列(没深扣,浅看了一下)

SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM
ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务

handle 拒绝策略(没深扣,浅看了一下)

AbortPolicy:中断抛出异常
DiscardPolicy:默默丢弃任务,不进行任何通知
DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)

线程池怎么实现线程复用的?(知道就行)

在线程池中同一个线程执行不同的任务。
假设现在有 100 个任务,我们创建一个固定线程的线程池(FixedThreadPool),核心线程数和最大线程数都是 3,那么当这个 100 个任务执行完,都只会使用三个线程。
例子:

public static void main(String[] args) {

     	ExecutorService threadPool = Executors.newFixedThreadPool(3);
        try {
            for (int i = 0; i < 100; i++) {
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName()+ "办理业务");
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                });
            }
            threadPool.shutdown();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
输出结果:
pool-1-thread-1办理业务
pool-1-thread-3办理业务
pool-1-thread-2办理业务
pool-1-thread-1办理业务
pool-1-thread-3办理业务
pool-1-thread-2办理业务
pool-1-thread-1办理业务
pool-1-thread-3办理业务
pool-1-thread-2办理业务

线程复用原理:

  • 通过Thread创建的线程。一个线程必须对应一个任务。这样的话每一个线程只能执行一个任务。
    而线程池将线程和任务进行了解耦,线程是线程,任务是任务。同一个线程可以在阻塞队列中不断获取新的任务执行。其核心原理在于对Thread进行了封装,并不是在每一次执行任务时调用Thread.start()方法来创建新线程,而是让每一个线程去执行“循环任务”,在“循环任务”之中再去检查是否有需要被执行的任务,如果有直接调用run方法,将run方法当做一个普通的方法去执行。通过这种方式可以将单个固定的线程跟所有的任务串联起来。

  • 另一个版本的说法:
    线程采用的是生产者消费者模型,也就是一个贤臣对应一个人物。
    生产者和消费者通过中间容器(线程池)进行解耦。生产者负责提交任务给中间容器,消费者负责从中间容器中获取任务执行。

Callable 和 Runnable

Callable和Runnable都可以理解为是任务。任务逻辑封装在run方法内。Callable和Runnable的区别在于Runnable 没有返回值,并且不能抛出异常throws(可以try cache)。Callable可以有返回值并且可以抛出异常和try cache。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}
 
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

小栗子:

public class ThreadPoolDemo4 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        try {
            for (int i = 0; i < 20; i++) {
                //提交任务 并 打印执行结果
                Future<String> futureResult = threadPool.submit(new Task());
                System.out.println(futureResult.get());
            }
            threadPool.shutdown();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

class Task implements Callable<String>{
    @Override
    public String call() throws Exception {
        int idx = new Random().nextInt(4) + 1;
        String result = "";
        switch (idx) {
            case 1: {result = Thread.currentThread().getName()+"号线程,执行结果为:腊八蒜";break;}
            case 2: {result = Thread.currentThread().getName()+"号线程,执行结果为:糖蒜";break;}
            case 3: {result = Thread.currentThread().getName()+"号线程,执行结果为:生蒜";break;}
            default: {result = Thread.currentThread().getName()+"号线程,执行结果为:大脑瓜崩,吃什么吃!";break;}
        }
        return result;
    }
}


执行结果:
pool-1-thread-1号线程,执行结果为:大脑瓜崩,吃什么吃!
pool-1-thread-2号线程,执行结果为:糖蒜
pool-1-thread-3号线程,执行结果为:大脑瓜崩,吃什么吃!
pool-1-thread-4号线程,执行结果为:糖蒜
pool-1-thread-5号线程,执行结果为:糖蒜
pool-1-thread-1号线程,执行结果为:腊八蒜
pool-1-thread-2号线程,执行结果为:糖蒜
pool-1-thread-3号线程,执行结果为:腊八蒜
pool-1-thread-4号线程,执行结果为:腊八蒜
pool-1-thread-5号线程,执行结果为:生蒜
Future 和 FutureTask

FutureTask实现了Future接口。
Future接口用来存储异步任务的执行结果。当线程池提交了一个Callable任务时会返回Future对象,可以通过get()方法获取到任务的执行结果。过程:主线程提交(submit())了一个任务给子线程,子线程去执行任务,执行完成后主线程获取(get())任务结果。
Future接口常用方法:

  • get():获取任务结果。若任务未执行完成则会一直阻塞,方法抛出 ExecutionException异常。
  • get(long timeout, TimeUnit unit):设置一个时间,用来获取执行结果。超过时间范围会抛出 TimeoutException 异常。如果超时,则需要手动取消任务 cancel。
  • cancel(boolean mayInterruptIfRunning):取消执行任务。传入参数用来分别是否立即中断正在执行的任务。
  • isDone():判断任务是否执行完毕。执行完毕不保证是成功执行。仅仅是一种状态表示这个任务已完结,可能任务中间的过程中出现异常、或是被中断但都执行结束。表示这个任务结束不会再执行。
  • isCancelled()判断任务是否被取消
		/**
         * future
         */
        ExecutorService service = Executors.newFixedThreadPool(1);
        Future<String> future = service.submit(new Callable<String>() {
            @Override
            public String call(){
                return Thread.currentThread().getName()+"执行结果……";
            }
        });
        try {
            System.out.println("get():" + future.get());
            System.out.println("get(long timeout, TimeUnit unit):" + future.get(3,TimeUnit.SECONDS));
            System.out.println("isDone():" + future.isDone());
        }catch (Exception e3){
            e3.printStackTrace();
        }

		/**
         * futureTask
         */
        FutureTask<String> stringFutureTask = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {return "执行结果";}
        });
        service.execute(stringFutureTask);
        try {
            System.out.println(stringFutureTask.get());
        }catch (Exception e3){
            e3.printStackTrace();
        }
线程池优化(先不钻牛角尖了,等后面在研究吧……)

后面再研究。

线程池扩展方法(beforeExecutor()、afterExecutor()、terminated())
  • beforeExecutor(Thread t, Runnable r):在执行任务之前调用方法,有两个参数第一个执行任务的线程,第二个是任务。
  • afterExecutor(Runnable r, Throwable t):任务执行完成之后调用的方法,2个参数,第1个参数表示任务,第2个参数表示任务执行时的异常信息,如果无异常,第二个参数为null
  • terminated():线程池最终关闭之后调用的方法。所有的工作线程都退出了,最终线程池会退出,退出时调用该方法
    eg:
public static void main(String[] args) {

        ThreadPoolExecutor executor = new ThreadPoolExecutor(1,
                5,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1),
                Executors.defaultThreadFactory(),
                (r,executors)->{//拒绝策略
                    System.out.println("无法处理的任务:" + r.toString());
                }){
            @Override
            protected void beforeExecute(Thread t, Runnable r) {
                System.out.println("在任务执行前执行 beforeExecute(); t:" + t.getName() + ";r:" + r.toString());
            }
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                if(t!=null)
                    System.out.println("任务执行完毕后执行 afterExecute(); r:" + r.toString() + ";t:" + t.toString());
                System.out.println("任务出现异常&&执行完毕后执行 afterExecute(); r:" + r.toString());
            }
            @Override
            protected void terminated() {
                System.out.println("线程池关闭后执行 terminated();");
            }
        };
        try {
            for (int i = 0; i < 5; i++) {
                Future<Object> futureResult = executor.submit(new Callable<Object>() {
                    @Override
                    public Object call() throws Exception {
                        return Thread.currentThread().getName() + "号线程开始执行任务====================================!";
                    }
                });
                try {
                    System.out.println(futureResult.get());
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            executor.shutdown();

        }catch (Exception e){
            e.printStackTrace();
        }
    }
//输出结果

在任务执行前执行 beforeExecute(); t:pool-1-thread-1;r:java.util.concurrent.FutureTask@5bc2a3fc
任务完毕执行完毕后执行 afterExecute(); r:java.util.concurrent.FutureTask@5bc2a3fc
pool-1-thread-1号线程开始执行任务====================================!
在任务执行前执行 beforeExecute(); t:pool-1-thread-1;r:java.util.concurrent.FutureTask@550546f4
任务完毕执行完毕后执行 afterExecute(); r:java.util.concurrent.FutureTask@550546f4
pool-1-thread-1号线程开始执行任务====================================!
在任务执行前执行 beforeExecute(); t:pool-1-thread-1;r:java.util.concurrent.FutureTask@513ac88c
任务完毕执行完毕后执行 afterExecute(); r:java.util.concurrent.FutureTask@513ac88c
pool-1-thread-1号线程开始执行任务====================================!
在任务执行前执行 beforeExecute(); t:pool-1-thread-1;r:java.util.concurrent.FutureTask@14384ccf
任务完毕执行完毕后执行 afterExecute(); r:java.util.concurrent.FutureTask@14384ccf
pool-1-thread-1号线程开始执行任务====================================!
在任务执行前执行 beforeExecute(); t:pool-1-thread-1;r:java.util.concurrent.FutureTask@5a2bd0f7
任务完毕执行完毕后执行 afterExecute(); r:java.util.concurrent.FutureTask@5a2bd0f7
pool-1-thread-1号线程开始执行任务====================================!
线程池关闭后执行 terminated();

Process finished with exit code 0



参考
线程池详解
线程池学习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值