线程池原理 学习笔记Day 1

156 阅读8分钟

hi,我是蛋挞,一个初出茅庐的后端开发,希望可以和大家共同努力、共同进步!


开启掘金成长之旅!这是我参与「掘金日新计划 · 4 月更文挑战」的第 29 天,点击查看活动详情

为什么用线程池?

线程池可以看做是管理了N个线程的池子,和连接池类似。线程池的作用主要有:

  • 控制并发数量:线程并发数量过多,抢占系统资源从而导致阻塞,线程池可以限制线程的数量。
  • 线程的复用:创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率和速度
  • 管理线程的生命周期:对线程进行一些简单的管理,创建,销毁等

认识线程池?

线程池继承体系

在Java 1.5之后就提供了线程池 ThreadPoolExecutor,它的继承体系如下:

图片转存失败,建议将图片保存下来直接上传

  • ThreadPoolExecutor :线程池
  • Executor: 线程池顶层接口,提供了execute执行线程任务的方法
  • Execuors: 线程池的工具类,通常使用它来创建线程池
private static void fixedThreadPool() {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    for (int  i = 0 ; i < 150 ; i++){
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //有5个线程在执行
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":线程执行...");
            }
        });
    }
}

线程池原理[重点]

执行流程

一个任务在线程池中是如何执行的?生活举例:

  1. 老陈要开软件公司,合伙几个核心的程序员做开发 :(线程核心数)
  2. 新的项目过来一个人接收一个项目去做,没有人手了,把新进来的项目放入项目排队池(任务队列)
  3. 如果项目队列中的任务过多,需要招聘一些临时的程序员(非核心线程),但是规定所有的开发总人数不能50(最大线程数)
  4. 如果新的项目进来,核心程序员和临时程序员都没有人手了,并且项目队列也放满了,新来的项目该如何处理呢?①拒绝 ②丢弃老的项目做新的项目 ③老陈自己做新的项目

线程池执行流程 : 核心线程 => 等待队列 => 非核心线程 => 拒绝策略

  • 如果有空闲的线程直接使用,没有空闲的线程并且线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务
  • 线程数量达到了corePoolSize,则将任务移入队,列等待空闲线程将其取出去执行(通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源,整个getTask操作在自旋下完成),如果队列已满,新建线程(非核心线程)执行任务,空闲下来以后,非核心线程会按照时间策略进行销毁
  • 队列已满,总线程数又达到了maximumPoolSize 最大线程数,就会执行任务拒绝策略。

举例:

1请求过来 -> 分配一个核心线程去处理

第6个任务过来 ,5个核心线程都在忙-> 把新的任务放入队列(SynchronousQueue除外,会直接创建线程)

第 16个任务过来,核心在忙,队列已满 -> 创建普通线程处理新任务

第21个任务过来 ,核心在忙,普通线程在忙,队列已满 -> 拒绝策略

如果任务做完,普通线程超过空闲时间,就会被销毁

线程池核心构造器

线程池源码 ThreadPoolExecutor 构造器:

图片转存失败,建议将图片保存下来直接上传

PS: 线程池7个参数的构造器非常重要[重要]

  • CorePoolSize: 核心线程数,不会被销毁
  • MaximumPoolSize : 最大线程数 (核心+非核心) ,非核心线程数用完之后达到空闲时间会被销毁
  • KeepAliveTime: 非核心线程的最大空闲时间,到了这个空闲时间没被使用,非核心线程销毁
  • Unit: 空闲时间单位
  • WorkQueue:是一个BlockingQueue阻塞队列,超过核心线程数的任务会进入队列排队
    • SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务;
    • LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
    • ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小
  • ThreadFactory:使用ThreadFactory创建新线程。 推荐使用Executors.defaultThreadFactory
  • Handler: 拒绝策略,任务超过 最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler
    • AbortPolicy丢弃任务并抛出RejectedExecutionException异常;
    • DiscardPolicy丢弃任务,但是不抛出异常;
    • DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务;
    • CallerRunsPolicy由调用线程处理该任务

可以定义和使用其他种类的RejectedExecutionHandler类来定义拒绝策略。

常见四种线程池

Jdk官方提供了常见四个静态方法来创建常用的四种线程.可以通过Excutors创建

  • CachedThreadPool:可缓存
  • FixedThreadPool :固定长度
  • SingleThreadPool:单个
  • ScheduledThreadPool:可调度

Excutors这个工具类中,util是工具类,以后见到以s结尾也是工具类.Collections Arrays Paths等

CachedThreadPool

可缓存线程池-可以无限制创建

图片转存失败,建议将图片保存下来直接上传

根据源码可以看出:

  • 这种线程池内部没有核心线程,线程的数量是有限制的 最大是Integer最大值。
  • 在创建任务时,若有空闲的线程时则复用空闲的线程(缓存线程),若没有则新建线程。
  • 没有工作的线程(闲置状态)在超过了60S还不做事,就会销毁
  • 适用:执行很多短期异步的小程序或者负载较轻的服务器

实战

private static void cachedThreadPool() {
    //带缓存的线程,线程复用,没有核心线程,线程的最大值是 Integer.MAX_VALUE
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int  i = 0 ; i < 150 ; i++){
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //始终只有一个线程在执行
                System.out.println(Thread.currentThread().getName()+":线程执行...");
            }
        });
    }
}

FixedThreadPool 定长线程池

图片转存失败,建议将图片保存下来直接上传

根据源码可以看出:

  • 该线程池的最大线程数等于核心线程数,所以在默认情况下,该线程池的线程不会因为闲置状态超时而被销毁
  • 如果当前线程数小于核心线程数,并且也有闲置线程的时候提交了任务,这时也不会去复用之前的闲置线程,会创建新的线程去执行任务(必须达到最大核心数才会复用线程)。如果当前执行任务数大于了核心线程数,大于的部分就会进入队列等待。等着有闲置的线程来执行这个任务。
  • 适用:执行长期的任务,性能好很多
private static void fixedThreadPool() {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    for (int  i = 0 ; i < 150 ; i++){
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //有5个线程在执行
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":线程执行...");
            }
        });
    }
}

SingleThreadPool

图片转存失败,建议将图片保存下来直接上传

根据源码可以看出:

  • 有且仅有一个工作线程执行任务
  • 所有任务按照指定顺序执行,即遵循队列的入队出队规则。
  • 适用:一个任务一个任务执行的场景。 如同队列
//单线程的线程池
private static void singleThreadExecutor() {
    //单线程的线程池
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    for (int  i = 0 ; i < 10 ; i++){
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                //始终只有一个线程在执行
                System.out.println(Thread.currentThread().getName()+":线程执行...");
            }
        });
    }
}

ScheduledThreadPool

图片转存失败,建议将图片保存下来直接上传

根据源码可以看出:

  • DEFAULT_KEEPALIVE_MILLIS就是默认10L,这里就是10秒。这个线程池有点像是CachedThreadPool和FixedThreadPool 结合了一下。
  • 不仅设置了核心线程数,最大线程数也是Integer.MAX_VALUE。
  • 这个线程池是上述4个中唯一一个有延迟执行和周期执行任务的线程池。
  • 适用:周期性执行任务的场景(定期的同步数据)
private static void scheduledThreadPool() {
    //带缓存的线程,线程复用,没有核心线程,线程的最大值是 Integer.MAX_VALUE
    ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5);
    //延迟 n 时间后,执行一次,延迟任务
    executorService.schedule(new Runnable() {
        @Override
        public void run() {
            System.out.println("延迟任务执行.....");
        }
    },10, TimeUnit.SECONDS);
    //定时任务,固定 N 时间执行一次 ,按照上一次任务的开始执行时间计算下一次任务开始时间
    executorService.scheduleAtFixedRate(()->{
        System.out.println("定时任务 scheduleAtFixedRate 执行 time:"+System.currentTimeMillis());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },1,1,TimeUnit.SECONDS);

    //定时任务,固定 N 时间执行一次 ,按照上一次任务的结束时间计算下一次任务开始时间
    executorService.scheduleWithFixedDelay(()->{
        System.out.println("定时任务 scheduleWithFixedDelay 执行 time:"+System.currentTimeMillis());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },1,1,TimeUnit.SECONDS);
}

自定义ThreadPoolExecutor

private static void customThreadPool() {
    //核心 4 个 ,最大 10 个 ,60s的空闲销毁非核心6个线程, 队列最大排队 10 个   = 最多同时处理 20个拒绝
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 10,
            60L, TimeUnit.SECONDS,  //最大空闲时间
            new ArrayBlockingQueue<Runnable>(10),   //队列排队10个
            new ThreadPoolExecutor.DiscardPolicy());    //任务满了就丢弃

    for (int  i = 0 ; i < 210 ; i++){
        int finalI = i;
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                //始终只有一个线程在执行
                System.out.println(Thread.currentThread().getName()+":线程执行..."+ finalI);
            }
        });
    }

}

在ThreadPoolExecutor类中几个重要的方法

  • Execute :方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行
  • Submit :方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,实际上它还是调用的execute()方法,只不过它利用了Future来获取任务执行结果
  • Shutdown :不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
  • shutdownNow立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
  • isTerminated:调用ExecutorService.shutdown方法的时候,线程池不再接收任何新任务,但此时线程池并不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。在调用shutdown方法后我们可以在一个死循环里面用isTerminated方法判断是否线程池中的所有线程已经执行完毕,如果子线程都结束了,我们就可以做关闭流等后续操作了。

线程池中的最大线程数

一般说来,线程池的大小经验值应该这样设置:(其中N为CPU的核数)

如果是CPU密集型应用,则线程池大小设置为N+1,如果是IO密集型应用,则线程池大小设置为2N+1 32_2+1=65 64_2+1=129,如果一台服务器上只部署这一个应用并且只有这一个线程池,那么这种估算或许合理,具体还需自行测试验证。

但是,IO优化中,这样的估算公式可能更适合:

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

因为很显然,线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

创建线程的个数是还要考虑 内存资源是否足够装下相当的线程

下面举个例子:

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。

总结

重点

  1. 线程池原理
  2. 线程池可以自定义,并且通过Excutors提供四个常用线程池
  3. 为什么要调优-项目上线设置变好,可用变大。程序在运行过程中出现问题。
  4. 通过java虚拟机参数的方式告诉jvm要使用多少内存,使用哪种垃圾回收器。
  5. jvm组成
  6. Jvm内存溢出  堆溢出  栈溢出 栈空间不足
  7. 判断对象已死 可达性分析
  8. 垃圾回收算法  分代回收:新生代(复制) 老年代(其他两种中一种)
  9. 常用垃圾回收器

图片转存失败,建议将图片保存下来直接上传

线程池小结

  1. 为什么需要 防止线程过多导致系统崩溃 提高效率,节约资源
  2. 原理 核心线程 等待队列 非核心线程 最大线程数, 拒绝策略(拒绝最新并抛异常,拒绝最新,喜新厌旧,全部都执行)
  3. 怎么操作 Executors中提供始终基本线程池,就是四个静态方法。 cached fixed single sheched ================== 可以自定义 new ThreadPoolExecutor...
  4. 怎么确定最大线程数
  5. 理论上先设置一个  n+1 2n+1
  6. 实践后再进行计算