Java多线程案例——线程池

Java多线程案例——线程池,第1张


⭐️前言⭐️

🍉博客主页: 🍁【如风暖阳】🍁
🍉精品Java专栏【JavaSE】、【Java数据结构】、【备战蓝桥】、【JavaEE初阶】
🍉欢迎点赞 👍 收藏 ⭐留言评论 📝私信必回哟😁

🍉本文由 【如风暖阳】 原创,首发于 CSDN🙉

🍉博主将持续更新学习记录收获,友友们有任何问题可以在评论区留言

🍉博客中涉及源码及博主日常练习代码均已上传码云(gitee)


📍内容导读📍
  • Java多线程案例之线程池
    • 🍅1.线程池是什么
      • 1.1 线程池
      • 1.2 用户态与内核态
    • 🍅2.标准库中的线程池
      • 2.1 ThreadPoolExecutor
      • 2.2 Executors
      • 2.3 工厂模式
    • 🍅实现线程池

Java多线程案例之线程池 🍅1.线程池是什么 1.1 线程池

本来多进程就是解决并发编程的方案,但是进程有点太重量了(创建和销毁开销比较大),因此引入了线程,线程比进程要轻量很多。即便如此,如果在某些场景中需要频繁的创建和销毁线程,线程的创建销毁开销也就无法忽视了。

为了解决这样的问题,我们引入了线程池:

使用线程的时候,不是说用的时候才创建,而是提前创建好,放到一个“池子”里(类似于字符串常量池),当我们需要使用线程的时候,直接从池子里面取一个线程出来,当我们不需要这个线程的时候,就把这个线程还回池子中(此时我们的 *** 作就会比创建销毁线程效率更高)。

为了更好的理解线程池的概念我们引入下边这个例子:

在我们找工作时,面试结束之后会面临两个结果,1是已经通过oc(offer call),2是没有通过,但是公司不会直接告诉你你没有通过,而是完全不通知(公司会把你放到“人才储备池”里),此时假设A公司要招100人而目前只招够了60个人,剩下的40个名额公司将会从“人才储备池”中直接挑选hc(head count),直接发offer,从而避免了需要再次笔试面试等一系列的流程来招纳新人。

如果是真的创建/销毁线程,就会涉及到用户态和内核态的切换。
如果不是真的创建销毁线程,而是放到池子里,就相当于全在用户态搞定了这个事情。
在用户态 *** 作会比较高效,切换到内核态以后 *** 作就可能很低效。
线程池最大的好处就是减少每次启动、销毁线程的损耗

下边便是用户态和内核态的具体介绍

1.2 用户态与内核态

用户态就是应用程序执行的代码,内核态就是 *** 作系统内核执行的代码,一般认为,用户态和内核态之间的切换,是一个开销比较大的 *** 作。

比如我们去银行办理业务时,需要去复印一份文件,如果我们自己去复印,就能够很快的完成并交给工作人员,但如果让工作人员去完成这个打印文件的工作就会比较耗费时间(工作人员需要完成的工作很多,不能及时调度去完成打印文件工作)。
此时的我们便是用户态,工作人员便是内核态。

🍅2.标准库中的线程池

在Java的标准库中,有下边这两者方式来创建线程池

2.1 ThreadPoolExecutor


ThreadPoolExecutor的使用是相对复杂的,因为里边有很多的参数,ThreadPoolExecutor的构造方法的参数是什么意思,便是一道经典的面试题:()中的含义在下边引用中有介绍

  • corePoolSize :核心线程数(正式员工的数量)
  • maximumPoolSize:最大线程数(正式工+临时工)
  • keepAliveTime:线程保持活动的时间(描述临时工摸鱼可以摸多久)
  • unit:keepAliveTime的时间单位(ms,s,minute)
  • workQueue:阻塞队列,组织了线程池要执行的任务
  • threadFactory:线程的创建方式,通过这个参数,来设定不同的线程的创建方式
  • RejectedExecutionHandler:拒绝策略,当任务队列满了的时候,又来了新的任务,要根据具体的业务场景来选取具体的拒绝策略

ThreadPoolExecutor里面包含的线程的数量并不是一成不变的,而是能够根据任务量来自适应,如果任务比较多,就会多创建一些线程,如果任务比较少,就会少创建一些线程(所以核心线程数和最大线程数的数值,应该通过实验的方式来确定比较合适)

我们可以把线程池想象成一个“公司”,公司里面的每个员工,就相当于是一个线程。
员工分为两类:
1,正式工(corePoolSize):签了劳动合同的,不能随便辞退
2,临时工:没有签劳动合同,随时可以踢掉
正式员工允许摸鱼(这样的线程即使是空闲,也不会被销毁)
临时工不允许摸鱼(如果临时工线程摸鱼的时间到了一定程度,就会被销毁)
如果我们要解决的任务场景任务量比较稳定,就可以设置corePoolSize和maximumPoolSize尽量接近(临时工就可以少一些)
如果我们要解决的任务场景任务量波动较大,既可以设置corePoolSize和maximumPoolSize相差大一些(临时工就可以多一些)

2.2 Executors

由于ThreadPoolExecutor使用起来比较复杂,标准库又提供了Executors类,相当于对ThreadPoolExecutor又进行了一层封装,这个类相当于一个“工厂类”,通过这个类提供的一组工厂方法,就可以创建出不同风格的线程池实例了。(工厂模式在2.3中讲解)

Executors 创建线程池的几种方式:

  • newFixedThreadPool: 创建固定线程数的线程池(完全没有临时工的版本)
  • newCachedThreadPool: 创建线程数目可变的线程池.(完全没有正式员工,全是临时工)
  • newSingleThreadExecutor: 创建只包含单个线程的线程池. (只在特定场景下使用)
  • newScheduledThreadPool: 能够设定延时时间的线程池(插入的任务能够过一会再执行),相当于进阶版的定时器。

代码示例:

 public static void main(String[] args) {
        /*使用以下标准库中的线程池,先创建出一个线程池中的实例*/
        ExecutorService service = Executors.newFixedThreadPool(10);
        //给这个实例里面加入一些任务
        for (int i = 0; i < 5; i++) {
            service.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
}
//


如果增加任务数量,将循环次数改为20,仍然能够完成20次的打印
这是因为当前线程池中有10个工作线程,此时这10个工作线程就会从任务队列中,先取出10个任务,然后并发执行这些任务,这些线程谁先执行完了当前的任务,谁就会去任务队列中重新取一个新的任务,直到把线程池任务队列中的任务都取完了,此时线程池的工作线程就阻塞等待(等待新的任务到来)。

在这几个工厂方法里面,调用了ThreadPoolExecutor的构造方法,同时把对应的参数进行了传递,并返回了这样的ThreadPoolExecutor实例 (通过这种模式,可以根据需求创建不同的线程池,比直接使用ThreadPoolExecutor创建线程池减少了参数,更加方便)

这两种模式各自有各自的好处,应该在适合的场景选取对应的模式即可

2.3 工厂模式

Executors类的几种创建线程池的方式就是工厂模式的体现,工厂模式也是一种设计模式,和单例模式是并列的关系,工厂模式存在的意义,就是弥补构造方法的一些缺陷。

工厂模式主要是为了创建实例,正儿八经的创建实例是通过构造方法,但是构造方法的限制比较多:
1.构造方法的名字,必须是固定的(类名)
2.如果需要多个版本的构造方式,就只能依赖构造方法的重载,但是重载方法又要求方法参数的个数和类型不一致。
比如下边这个例子:

假设我们有个类Point,我们希望通过笛卡尔坐标和极坐标两种方式来构造这个点

class Point {
		public Point(double x,double y){...}       -->笛卡尔坐标
		public Point(double r,double a){...}       -->极坐标
}

但是这俩方法的参数个数和类型都一样,这个时候编译不了,而且构造方法的方法名是固定的,光看方法名并不知道咱们是使用哪种方式构造

为了解决这个问题:
我们就不使用构造方法来构造实例了,而是使用其他的方法来进行构造实例,这样的用来构造实例的方法就被称为“工厂方法”。
工厂方法其实就是普通的方法,这个工厂方法里面会调用对应的构造方法,并进行一些初始化的 *** 作,并返回这个对象的实例。

🍅实现线程池
  • 核心 *** 作为 submit, 将任务加入线程池阻塞队列中,并创建线程
  • 使用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务.
  • 使用一个 BlockingQueue 组织所有的任务
  • 每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
  • 指定一下线程池中的最大线程数 MAX_WORKER_COUNT 当当前线程数超过这个最大值时, 就不再新增线程了.
static class Worker extends Thread {
        private BlockingQueue<Runnable> queue=null;
        public Worker(BlockingQueue<Runnable> queue) {
            this.queue=queue;
        }

        //工作线程
        @Override
        public void run() {
            //从阻塞队列中取任务
            while (true) {
                try {
                    Runnable command=queue.take();
                    command.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
}
static class ThreadPool {
        //包含一个阻塞队列,用来组织任务
        private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
        //该list用于存放当前的工作线程
        private List<Thread> workers=new ArrayList<>();
        //最大工作线程数目
        private static final int MAX_WORKER_COUNT=10;
        //submit方法,把任务加入到线程池的阻塞队列中,同时也可以负责创建线程
        public void submit(Runnable command) throws InterruptedException {
            if(workers.size()<MAX_WORKER_COUNT) {
                //如果当前工作线程的数量不足线程数目上限,就创建出新的线程
                //工作线程专门搞一个类来实现
                //Worker内部要能够取到队列的内容,就需要把这个队列实例通过Worker的构造方法传过去
                Worker worker=new Worker(queue);
                worker.start();
                workers.add(worker);
            }
            queue.put(command);
        }
}

检测实现的线程池:

 public static void main(String[] args) throws InterruptedException {
        ThreadPool pool=new ThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
}

更改循环次数变为20次,也能完成20次的打印,说明实现成功。


⚡️最后的话⚡️

总结不易,希望uu们不要吝啬你们的👍哟(^U^)ノ~YO!!如有问题,欢迎评论区批评指正😁

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/919964.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存