记录一下最近学习到的【线程池】相关的知识

记录一下最近学习到的【线程池】相关的知识

Lewis
2022-02-21 / 0 评论 / 20 阅读 / 正在检测是否收录...
学习目标
  • 【理解】线程池基本概念
  • 【理解】线程池工作原理
  • 【掌握】java内置线程池
  • 【应用】使用java内置线程池完成综合案例
线程池

1.什么是线程池?
答:线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到阻塞队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象。

2.为什么要使用线程池?
答:使用线程池最大的原因就是可以根据系统的需求和硬件环境灵活的控制线程的数量,且可以对所有线程进行统一的管理和控制,从而提高系统的运行效率,降低系统运行运行压力。总结一下:
①:降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
②:提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
③:提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控,避免不停的创建线程导致OOM。

3.创建线程池的7大核心参数?
答:我们查看源码可以看到,jdk 1.8之前创建线程池的方式,基本都是通过new ThreadPoolExecutor()去实现。

public ThreadPoolExecutor(
        int corePoolSize, //核心线程数量
        int maximumPoolSize,//     最大线程数
        long keepAliveTime, //       最大空闲时间
        TimeUnit unit,         //        时间单位
        BlockingQueue<Runnable> workQueue,   //   任务队列
        ThreadFactory threadFactory,    // 线程工厂
        RejectedExecutionHandler handler  //  任务拒绝策略(饱和处理机制)
 ) 
  • corePoolSize 代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程。核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定,例如:执行一个任务需要0.1秒,系统百分之80的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10。当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,既 按照百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理 ;
  • maxinumPoolSize 代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数。最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么,最大线程数=(最大任务数-任务队列长度)单个任务执行时间;既:最大线程数=(1000-200)0.1=80个。
  • keepAliveTime 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setKeepAliveTime 来设置空闲时间。这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可。
  • unit 空闲存活时间的时间类型。
  • workQueue 用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程。
  • ThreadFactory 实际上是一个线程工厂,用来生产线程执行任务。工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,比如用牛逼的GUAVA。一般我们会根据业务来制定不同的线程工厂。
  • Handler 任务拒绝策略,有两种情况,第一种是当我们调用 shutdown 等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这时也就拒绝。
4.线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?
1、一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资
源。阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源。
2、在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。
线程池工作原理

kzw0xdln.png

线程池中线程复用原理
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。
Java内置线程池-ExecutorService介绍

ExecutorService接口是java内置的线程池接口,通过学习接口中的方法,可以快速的掌握java内置线程池的基本使用。常用方法:

  • void shutdown()启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
  • List shutdownNow()停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
  • Future submit(Callable task) 执行带返回值的任务,返回一个Future对象。
  • Future<?> submit(Runnable task) 执行 Runnable 任务,并返回一个表示该任务的Future。
  • Future submit(Runnable task, T result) 执行 Runnable 任务,并返回一个表示该任务的Future。
Java内置线程池-ExecutorService获取

获取ExecutorService可以利用JDK中的Executors 类中的静态方法,常用获取方式如下:

  • static ExecutorService newCachedThreadPool()创建一个默认的线程池对象,里面的线程可重用,且在第一次使用时才创建
  • static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)线程池中的所有线程都使用ThreadFactory来创建,这样的线程无需手动启动,自动执行;
  • static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池
  • static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)创建一个可重用固定线程数的线程池且线程池中的所有线程都使用ThreadFactory来创建。
  • static ExecutorService newSingleThreadExecutor()创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
  • static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)创建一个使用单个 worker 线程的 Executor,且线程池中的所有线程都使用ThreadFactory来创建。
综合案例-秒杀商品

案例介绍:
假如某网上商城推出活动,新上架10部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀挣抢,假如有20人同时参与了该活动,请使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败;

要求:
使用线程池创建线程
解决线程安全问题

思路提示:
既然商品总数量是10个,那么我们可以在创建线程池的时候初始化线程数是10个及以下,设计线程池最大数量为10个;
当某个线程执行完任务之后,可以让其他秒杀的人继续使用该线程参与秒杀;
使用synchronized控制线程安全,防止出现错误数据;

代码步骤:
编写任务类,主要是送出手机给秒杀成功的客户;
编写主程序类,创建20个任务(模拟20个客户);
创建线程池对象并接收20个任务,开始执行任务;

/**
 * @author lvwei
 */
public class ThreadPollLearning {

    /**
     * 案例介绍:
     * 假如某网上商城推出活动,新上架10部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀挣抢,
     * 假如有20人同时参与了该活动,请使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败;
     * <p>
     * 要求:
     * 使用线程池创建线程
     * 解决线程安全问题
     * <p>
     * 思路提示:
     * 既然商品总数量是10个,那么我们可以在创建线程池的时候初始化线程数是10个及以下,设计线程池最大数量为10个;
     * 当某个线程执行完任务之后,可以让其他秒杀的人继续使用该线程参与秒杀;
     * 使用synchronized控制线程安全,防止出现错误数据;
     * <p>
     * 代码步骤:
     * 编写任务类,主要是送出手机给秒杀成功的客户;
     * 编写主程序类,创建20个任务(模拟20个客户);
     * 创建线程池对象并接收20个任务,开始执行任务;
     *
     * @param args
     */
  public class ThreadPollLearning {
    public static void main(String[] args) {
        //1:创建一个线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(15));
        //2:循环创建任务对象
        for (int i = 1; i <= 20; i++) {
            MyTask myTask = new MyTask("客户" + i);
            pool.submit(myTask);
        }
        //3:关闭线程池
        pool.shutdown();

    }
}

public class MyTask implements Runnable {
    //设计一个变量,用于表示商品的数量
    private static int id = 10;
    //表示客户名称的变量
    private String userName;
    public MyTask(String userName) {
        this.userName = userName;
    }
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(userName+"正在使用"+name+"参与秒杀任务...");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (MyTask.class){
            if(id>0){
                System.out.println(userName+"使用"+name+"秒杀:"+id-- +"号商品成功啦!");
            }else {
                System.out.println(userName+"使用"+name+"秒杀失败啦!");
            }
        }
    }
}
0

评论 (0)

取消