Java多线程案例之线程池

举报
未见花闻 发表于 2022/07/31 21:34:33 2022/07/31
【摘要】 本篇文章将介绍多线程案例,线程池,线程在Linux中也叫做轻量级线程,尽管线程比进程较轻,但是如果线程的创建和销毁频率高了,开销也还是有的,为了进一步提高效率,引入了线程池,和字符串常量池类似,把线程提前创建好,放到一个“池子”里面,后面使用的时候,速度就快了,但是代价就是空间,线程池本质上也还是空间换时间。

⭐️前面的话⭐️

本篇文章将介绍多线程案例,线程池,线程在Linux中也叫做轻量级线程,尽管线程比进程较轻,但是如果线程的创建和销毁频率高了,开销也还是有的,为了进一步提高效率,引入了线程池,和字符串常量池类似,把线程提前创建好,放到一个“池子”里面,后面使用的时候,速度就快了,但是代价就是空间,线程池本质上也还是空间换时间。

📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创!
📆华为云首发时间:🌴2022年7月31日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《java核心技术》,📚《java编程思想》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!

🎵1.线程池概述

🎶1.1什么是线程池

线程池和字符串常量池一样,都是为了提高程序运行效率而提出的效率,程序中每创建一个线程就会把该线程加载到一个“池子”中去,其实这个池子就是List,当程序下次需要调用该线程的时候,可以直接从线程池中去取,而不用花费更大的力气去重新创建和销毁线程,从而使程序的运行效率提高,线程池也是管理线程的方式之一。

🌳那为什么从线程池中“拿”线程会比直接创建线程要更加高效呢?

因为使用线程池调度线程是在用户态实现的,而线程的创建是基于内核态实现的。那为什么说用户态比内核态更加高效呢?因为你将任务交给内核态时,内核态不仅仅只去完成你交给它的任务,大概率还会伴随完成其他的任务,而你将任务交给用户态时,用户态只去完成你所交代的任务,所以综上所述,用户态效率更高。

🎶1.2Java线程池标准类

java也提供了相关行程池的标准类ThreadPoolExecutor,也被称作多线程执行器,该类里面的线程包括两类,一类是核心线程,另一类是非核心线程,当核心线程全部跑满了还不能满足程序运行的需求,就会启用非核心线程,直到任务量少了,慢慢地,非核心线程也就退役了,通俗一点核心线程就相当于公司里面的正式工,非核心线程相当于临时工,当公司人手不够的时候就会请临时工来助力工作,当员工富余了,公司就会将临时工辞退。

jdk8中,提供了4个构造方法,我主要介绍参数最多的那一个构造方法,其他3个构造方法都是基于此构造方法减少了参数,所以搞懂最多参数的构造方法,其他构造方法也就明白了。

//参数最多的一个构造方法
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) 
  • corePoolSize表示核心线程数。
  • maximumPoolSize表示最大线程数,就是核心线程数与非核心线程数之和。
  • keepAliveTime非核心线程最长等待新任务的时,就是非核心线程的最长摸鱼时间,超过此时间,该线程就会被停用。
  • unit 时间单位。
  • workQueue任务队列,通过submit方法将任务注册到该队列中。
  • threadFactory线程工厂,线程创建的方案。
  • handler拒绝策略,由于达到线程边界和队列容量而阻止执行时使用的处理策略。

线程池构造方法参数

其他几个构造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue)

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) 

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler)

🌳那核心线程数最合适值是多少呢?假设CPU有N核心,最适核心线程数是N?是2N?是1.5N?只要你能够说出一个具体的数,那就错了,最适的核心线程数要视情况而定,没有一个绝对的标准的值。

在具体使用线程池时,往往使用的是Executor,因为 ExecutorThreadPoolExecutor所实现的一个接口,由于标准库中的线程池使用较复杂,对于ThreadPoolExecutor类中的方法我们就不介绍了,最重要的一个方法是submit方法,这个方法能够将任务交给线程池去执行,接下来我们来理一理线程池最基本的工作原理,我们来尝试实现一个简单的线程池。
线程池标准类的族谱

🎵2.线程池的实现

🎶2.1线程池的基本工作原理

线程池是通过管理一系列的线程来执行程序员所传入的任务,这些任务被放在线程池对象中的一个阻塞队列中,然后线程池会调度线程来执行这些任务,优先调度核心线程(核心线程会在线程池对象构造时全部创建),如果核心线程不够用了,就会创建非核心线程来帮忙处理任务,当非核心线程一定的时间没有收到新任务时,非核心线程就会被销毁,我们实现线程池的目的是加深对线程池的理解,所以实现的过程中就不去实现非核心线程了,线程池里面的线程全部以核心线程的形式实现。

🌳我们需要实现一个线程池,根据以上的原理需要准备:

  • 任务,可以使用Runnable。
  • 组织任务的数据结构,可以使用阻塞对列。
  • 工作线程(核心线程)的实现。
  • 组织线程的数据结构,可以使用List。
  • 新增任务的方法submit

🎶2.2线程池的简单实现

关于任务和任务的组织就不用多说了,直接使用Runnable和阻塞队列BlockingQueue<Runnable>就可以了,重点说一下工作线程如何描述的,工作线程中需要有一个阻塞队列引用来获取我们存任务的那一个阻塞队列对象,然后重写run方法通过循环不断的获取任务执行任务。

然后根据传入的核心线程数来创建并启动工作线程,将这些线程放入顺序表或链表中,便于管理。

最后就是创建一个submit方法用来给用户或程序员派发任务到阻塞队列,这样线程池中的线程就会去执行我们所传入的任务了。
线程池基本实现逻辑

🌳实现代码:

class MyThreadPool {
    //1.需要一个类来描述具体的任务,直接使用Runnable即可
    //2.有了任务,我们需要将多个任务组织起来,可以使用阻塞队列
    private final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    //3.组织好任务,就可以分配线程池中的线程来执行任务了,所以我们需要描述线程,专门来执行任务
    static class Worker extends Thread {
       //获取任务队列
        private final BlockingQueue<Runnable> queue;
        //构造线程时需要将任务队列初始化
        public Worker(BlockingQueue<Runnable> queue) {
            this.queue = queue;
        }
        //重写线程中的run方法,用来执行阻塞队列中的任务
        @Override
        public void run() {
           while (true) {
               try {
                   //获取任务
                   Runnable runnable = queue.take();
                   //执行任务
                   runnable.run();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
        }
    }
    // 4.线程池中肯定存在不止一个线程,所以我们需要对线程进行组织,这里我们可以使用顺序表,使用链表也可以
    private final List<Worker> workers = new ArrayList<>();

    //根据构造方法指定的线程数将线程存入workers中
    public MyThreadPool(int threadNums) {
        for (int i = 0; i < threadNums; i++) {
            Worker worker = new Worker(this.queue);
            worker.start();
            this.workers.add(worker);
        }
    }
    // 5.创建一个方法,用来将任务存放到线程池中
    public void submit(Runnable runnable) {
        try {
            this.queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

🌳我们来测试一下我们所实现的线程池:

import java.util.ArrayList;
import java.util.List;

public class ThreadPoolProgram {
    private static int NUMS = 1;
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(10);

        for (int i = 0; i < 20; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("第" + NUMS + "个任务!" );
                }
            });
            Thread.sleep(200);
            NUMS++;
        }
    }

🌳运行结果:
线程池运行结果

好了,你知道线程池的工作原理了吗?


下期预告:常见的锁策略以及CAS与ABA相关问题。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。