线程池详解

日升时奋斗,日落时自省

目录

1、线程池解释

2、线程池使用

2.1、代码解析

2.2、创建方式

2.3、ThreadPoolExecutor解析

2.4、拒绝策略

 3、自主实现简单线程池

1、线程池解释

线程池可以从字面意思理解就是一个能装多个线程的池子,是的其实也就是这么理解。

认识他从问题开始 ,为什么要用它,我们已经可以自己实现多线程了,还要它创建是不是多此一举,实则不是的,凡是存在必有道理

了解线程的友友们都知道,线程的出现就是因为进程实现并发编程的时候,太重了(创建和销毁资源开销太大)

这个时候我们还是用了线程来代替进程操作,线程也叫做“轻量级进程” ,创建和销毁线程比创建和销毁进程更高效,此时使用多线程实现并发编程也就更高效了。

但是我们希望趋向于更好的并发编程,较少资源开销,线程上还需要更精进的。

第一种就是 :“轻量级线程” 已经有了,叫做“协程” 也可以叫做“纤程” 但是还没有更进到java标准库中,所以我们还用不了,其他部分语言是有的(java以后会不会有不知道,越更新肯定是越好的)

第二种就是 : 使用线程池 来降低线程创建和销毁的开销的

注:之所以说线程是 轻量级进程 是因为与进程相对比,减小了开销,但是不代表线程真的开销就很小。

需要将事先创建好的线程,放到“池”中,后来需要来再从里面取,用完了再还给池,这样创建和销毁线程就更家高效了。

这里简单的叙述可能一点点的模糊,先理论上简述一下,创建线程和销毁线程都是由操作系统内核完成的,如果从池子里获取和归还给池子,是算自己用户代码就能实现的,不必交给操作系统,因为操作系统慢啊,开销还大,池子更快。(图解一下)

2、线程池使用

2.1、代码解析

在java标准库中,也给咱们提供了现成的线程池,可以直接使用(这里先那一种创建方法进行解释)

ExecutorService pool= Executors.newFixedThreadPool(10);

这里使用ExecutorService来定义接收,线程池数目固定10个,注意这个操作是使用了某个静态方法(newFixedThreadPool),直接构造出一个对象来(这个方法的背后会有new操作,就不需要我们去写了)

这样的方法称为“工厂方法” ,提供这个工厂方法的类,也就是“工厂类”,这个行代码就使用了“工厂模式”(一种设计模式)

是不是感觉这种不用new 的还要搞一个方法来创建对象的有点多此一举,不是哈,是因为构造方法有缺陷,还记得重载吧,构造方法能重载,但是参数类型和个数是不能完全相同的。

 这里也是因为创建线程池使用了种方法调用,所以在这里提一下工厂模式。

2.2、创建方式

创建方式有多种,这里写四种创建方法,其实也都是大体相同。为了创建出来而已

线程池创建后会有任务调度,那就需要一个方法来传这个任务,线程池提供了一个方法submit(任务)可以题给线程池提交若干任务

第一种 刚刚见过的

创建方法(newFixedThreadPool)

 创建一个固定线程 线程数量可控根据不同任务情况 设置线程数量

public static void main1(String[] args) {
        //创建线程池
        ExecutorService pool= Executors.newFixedThreadPool(10);
        for (int i = 0; i < 100; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第一种线程池创建方式");
                }
            });
        }
    }

第二种

创建方法 (newCachedThreadPool)

适用于短时间大量任务情况,线程数量动态变化,任务多了就多几个线程,如果任务少了,就少些线程

public static void main2(String[] args) {
        ExecutorService pool=Executors.newCachedThreadPool();
        for (int i = 0; i < 50; i++) {
            int n=i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("方法二");
                }
            });
        }
    }

第三种

创建方法(newSingleThreadExecutor)

(1)复用线程 不需要频繁创建销毁 

(2)提供了任务队列 和 拒绝策略

public static void main3(String[] args) {
        ExecutorService pool=Executors.newSingleThreadExecutor();
        for (int i = 0; i < 20; i++) {
            int  n=i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("方法三");
                }
            });
        }
    }

第四种

创建方法(newScheduledThreadPool)

类似于定时器,也是让任务延时执行,只不过执行的时候不是由扫描线程自己执行了,而是由单独的线程池来执行

public static void main(String[] args) {
        ExecutorService pool=Executors.newScheduledThreadPool(10);
        for (int i = 0; i < 50; i++) {
            int n=i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("方法六"+n);
                }
            });
        }
    }

以上所有的创建方法都是通过包装ThreadPoolExecutor来实现出来的

其实这里有一个小问题可以提一下?(就是创建线程池的代码)

为什么在这里要int n=i 一下再打印出来,直接打印i可以吗?

答案:不可以, 因为i是主线程的局部变量(在主线程的栈上)随着主线程的执行结束就会销毁,很可能主线程这里for循环执行完了,当前run的任务在线程池还没有排到呢此时就已经销毁了,那剩下的任务谁做

这里的是n是一个变量捕获 run方法属于Runnable这个方法的执行时机,不是立刻马上执行而是再未来的某个节点执行(线程池里的线程就像是在排队等待,对应到谁了谁去执行任务)

为了避免线程之间生命周期不同,作用域差异,导致后序执行run的时候i已经销毁,于是这里采用了变量捕获,这里相当于run方法把当前主线程的i拷贝了一份,这时的n就是在执行run的一个局部变量

针对捕获变量:在java中,JDK1.8之前,要求变量捕获,只能捕获final修饰的变量,其实就是为了捕获不会改变的变量,在JDK1.8开始,有所更正,要求不一定非得带final关键字,只要满足条件,代码没有修改这个变量也可以进行捕获如当前场景

2.3、ThreadPoolExecutor解析

ThreadPoolExecutor里面有啥呢,有的挺多的,可以在javaAPI8官方文档里面找java.util.concurrent包里面找,可以找到,下面是对该类的参数进行一个解析

 说到线程池的线程数量,其实都是根据情况而定,没有一定的答案,只有尝试才知道那种是最好的,在测试中选择。

不同的程序特点不同,设置线程的数量也是不同的

分为两种极端类型:

(1)CPU密集型: 每个线程要执行的任务都是需要CPU进行一系列的算术运算,此时线程池线程数,最多也不应该超过CPU核数,因为没有CPU就这么多核,设置更大也得空着,线程数多了也没有用不是嘛(这里说的核数是逻辑核心个数 )

注:现在电脑的CPU一般是8核16线程8个物理核心,16个逻辑核心,每个逻辑核心都可以执行一个线程,一个物理核心可以有多个逻辑核心

(2)IO密集型 每个线程干的工作就等待IO(读写硬盘,网卡,等待用户输入等),不吃CPU,此时这样的线程就处于阻塞状态,不参与CPU调度,是不是感觉线程一下不归CPU核心数管了,这不多多益善嘛,说是这么说的,计算机资源有限,所以不太可能,但仍然可以创建多个线程超过核心数

当然了以上提及都是理想化,因为大项目不会就CPU密集型,或者就IO密集型;真实情况,一部分需要CPU,一部分是IO密集型 ,这就取决于是那种类型的更多,CPU密集型多,线程数就少一点,反之线程数就多一点(两种类型相辅相成,要想知道设置多少个线程数为做优,需要针对程序进行测试,测试结果说最优才是最优,没有具体的衡量标准)

2.4、拒绝策略

拒绝策略在java标准库中专门提供了,四种方法下面是一些例子作为解释,拒绝策略像是我们生活中不同拒绝方法。

 3、自主实现简单线程池

实现简单的线程池不会有太大的代码量

思路:

(1)需要一个队列(阻塞队列)(这篇博客中的生产者消费者模型解释了阻塞队列)前面解释过线程池的任务像是排队一样,没有接到任务就会先阻塞这

(2)该类的构造方法中创建n线程并执行任务

(3)写一个submit方法进行提交任务(阻塞队列入队列操作)

以上三点就可以写一个简单的线程池,当然就是简单实现,所以没有拒绝策略等多中方法

自定义一个类 我自定义叫做 MyThreadPool 后面的所有代码都是包括在这个类里面的

首先创建一个阻塞队列

 //此处不涉及到时间  此处只有任务 就直接 使用Runable ,在定时器中是因为还有个是时间,所以需要进行自定义一个类来表示
    private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();

这里可以创建一个对象 原因: 防止后面出现测试的时候,出现对象不一致的问题(后面在解释)

 private  Object locker=new Object();   //直接每次都可以获得这个this 那就是一定的相同

然后写啥,该出创建的都已经创建了,该写这个我们线程池的构造方法

(1)构造方法的参数是线程数

(2)任务如何来如何执行,需要从阻塞队列中出队列

//n 表示线程的数量
    public MyThreadPool(int n){
        //构造方法 中 创建线程
        for (int i = 0; i < n; i++) {
            Thread t=new Thread(()->{
               while(true){
                   try {
                       //任务出队列 + 接收任务
                       Runnable runnable=queue.take();
                       //这里做个打印可以看是不是同一个对象
                       System.out.println(locker);
                       //任务执行
                       runnable.run();
                   } catch (InterruptedException e) {
                       throw new RuntimeException(e);
                   }
               }
            });
            t.start();
        }
    }

就剩下最后一步,写一个submit方法 用来提交任务,上面为什么能接收到任务(阻塞队列中为什么有任务),就是因为在该方法中进行任务提交,放入阻塞队列中。

public void submit(Runnable runnable){
        //其实这里可以理解为获取先线程任务   放到线程池中
        try {
            //阻塞队列存放任务
            queue.put(runnable);
            System.out.println(locker);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

把以上四个代码放在一个我们自定义的类(MyThreadPool)中,就是一个简单的线程池 

现在所有代码都走完了,开始解释为什么要创建一个对象,还要打印出来看是不是相同的对象,这也是因为我看到这样的问题了,所以放到这里写一下。

有友友一定认为什么情况这个对象都是相同的,因为在一个类里面不都一样吗,如果这里没有创建这个对象的话,直接打印this对象,这里也是一样的,是的,但是有友友会在这里习惯性的使用匿名内部类结果就不一样了,匿名内部类搭配this在这里就会导致对象不一致

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值