多线程初阶(四)定时器及线程池

news2025/1/11 18:46:38

目录

前言:

定时器

使用标准库中定时器

模拟实现定时器

线程池

使用标准库中的线程池

代码实现

ThreadPoolExecutor类介绍

构造方法参数介绍

拒绝策略介绍

模拟实现线程池

代码实现

提出问题

小结:


前言:

    这篇文章同上一篇文章都是介绍多线程下的一些案例。我们可以通过这些案例更加深入的了解多线程编程。

定时器

    所谓定时器就是指定一些任务,在后面的某一时刻执行。这里先使用标准库中的定时器。标准库中用Timer类来表示定时器。使用schedule方法来提交任务和指定任务执行时间。

注意: 

  • delay是指定从当前代码执行时间开始以后多久时间,单位为毫秒
  • task就算具体指定的任务,我们可以清楚看到它的类型为TimerTask。TimerTask是一个抽象类,它实现了Runnable接口

使用标准库中定时器

public class ThreadDemo23 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("11111");
            }
        }, 3000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("22222");
            }
        }, 2000);
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("33333");
            }
        }, 1000);
    }
}

    注意:这里可以看见是按照时间打印的日志,并且打印完进程并没有结束。这是因为执行任务的线程没有结束,这个线程是前台线程,阻止进程的结束。

模拟实现定时器

思路:

    实现定时器需要添加任务。如果有很多任务,我们每次要执行的都是时间最早的任务,因此我们使用优先级队列PriorityBlockingQueue来保存任务(阻塞队列多线程情况下线程安全)。还需要一个线程始终检测任务队列的线程,时间到了就用这个线程来执行任务。

    这里的任务需要添加时间,因此我们将任务和时间设计为一个类,优先级队列就存储这个对象即 可。这个对象需要有比较性,可以实现Comparable接口,重写compareTo方法,用时间进行比较。也可以直接传一个比较器Comparator对象即可。

    检测线程每次拿出优先级队列里最早的任务比较,如果当前时间还没到,就把这个任务又入队列。否则就执行这个任务。

    同样的,我们也采取schedule方法来提交任务,当我们往优先级队列提交任务时,需要进行时间转换:当前时间 + 所指定的时间。这样就符合需求了。先展示代码,随后介绍多线程情况下线程安全问题。

class MyTask implements Comparable<MyTask> {
    private long time;
    private Runnable runnable;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }
    public long getTime() {
        return time;
    }
    public void run() {
        runnable.run();
    }
    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);
    }
}
class MyTimer {
    //检测线程
    private Thread t = null;
    //存储任务
    private PriorityBlockingQueue<MyTask> priorityBlockingQueue = new PriorityBlockingQueue<>();
    public MyTimer() {
        t = new Thread(()-> {
                while (true) {
                    try {
                        //当代码执行到currentTime这一行,如果被调度走,再执行schedule改变了堆顶元素,即等待时间就变了,但这是没有wait那么notify就
                        //空打了一炮,当线程调度回来时,已经落后于时间了,即错过了依次一次schedule
                        //所以就需要保证原子性,让这个线程只有wait了,才执行notify刷新等待时间,不能空打一炮。那么就增加锁的范围
                        synchronized (this) {
                            //获取最小时间任务
                            MyTask myTask = priorityBlockingQueue.take();
                            //当前时间
                            long currentTime = System.currentTimeMillis();
                            //如果一直取的时间都小于,那么这个线程就会一直取,放。那么就需要wait等待一下
                            //等待的最大时间:currentTime - tmp.getTime()
                            if (currentTime < myTask.getTime()) {
                                priorityBlockingQueue.put(myTask);
                                this.wait(myTask.getTime() - currentTime);
                            } else {
                                myTask.run();
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
        });
        t.start();
    }
    public void schedule(Runnable runnable, long time) {
        //时间转换
        MyTask myTask = new MyTask(runnable, System.currentTimeMillis() + time);
        priorityBlockingQueue.put(myTask);
        //每入一个任务,就需要跟新一下wait的等待时间
        synchronized (this) {
            this.notify();
        }

    }
}

注意:

    多线程情况下,我们如果不使用wait方法进行阻塞。那么这个检测线程就会一直检测,当前时间离最早任务执行时间差距很大的话,就比较浪费系统资源了。因此当发现时间还每到时就进行阻塞(最大阻塞时间为当前时间离最早执行时间差),这里锁首先加到wait的头顶。每当进行schedule提交任务时,就用notify通知wait重新检测一次,因为有可能这次提交的任务比之前提交的任务时间更早

    由于线程调度是随机的,假设检测线程代码执行到wait方法上面(还没有执行wait),就被调度走了。这个时候schedule提交了一次比之前更早要执行的任务,由于检测线程没有阻塞,这里的notify就会空打一炮。当检测线程被调度回来时,它所记的时间还是上一次的,就错过了这次提交。

    基于上述问题,我们扩大wait锁的范围。保证每次notify的时候,检测线程都在阻塞状态。现在就算出现了上述问题,notify这里就会阻塞等待,直到执行wait释放锁后,才能执行notify。

    这里的锁都是在this上加着,如果使用了匿名内部类的方法来实现线程的创建,就会出bug。因为检测线程里的this和schedule方法里的this不是同一个对象。可以使用lambda表达式或者指定一个锁对象,不再用this就可以解决这个问题。

线程池

    线程池里提供了一组线程,我们只需要向其中的阻塞队列提交任务,就会使用线程池里的线程来执行任务。

    线程相比于进程是轻量不少,还有比线程更加轻量的协程。使用线程池也是因为创建一个线程需要操作系统内核来实现,其实还是比较消耗系统资源的。我们使用线程池为我们提供的线程就和内核没有关系,我们通过代码就可以实现,做到更加节省系统资源。

    当我们向内核申请创建一个线程,就算把任务交给了内核,需要执行内核中的代码。但是内核服务于很多程序,我们就不确定内核什么时候执行我们提交的任务,那么就是不可控的。当我们直接使用线程池的里线程就和内核没有关系,这个时候相对于就是可控的

使用标准库中的线程池

  1. newCachedThreadPool()方法线程数量是动态变化的。(根据任务的大小来确定)
  2. newFixedThreadPool()方法可以指定线程池中具体的线程数量
  3. newScheduledThreadPool()方法使用线程池里的线程让任务延迟执行
  4. newSingleThreadExecutor()方法指定一个线程来执行任务。

代码实现

public class ThreadDemo26 {
    public static void main(String[] args) {
        //线程池里的线程都是前台线程,阻止进程的结束
        //创建线程池,指定线程数量为10个
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //这里涉及变量捕获,变量的生命周期不一样,就存在变量捕获
        //变量捕获的前提需要这个变量没有改变
        //变量捕获先把这个变量存起来,使用时再创建这个变量,把值赋过去
        for(int i = 0; i < 1000; i++) {
            int n = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(n);
                }
            });
        }
    }
}

注意:

    这里使用的是指定具体线程数量的线程池。使用submit方法提交任务。

    这里涉及变量捕获,变量i在主线程里是局部变量,如果想要在线程池中的线程使用这个变量就存在变量捕获(变量的生命周期不一样)。当线程使用的时候可能主线程中局部变量i已经释放了,变量捕获就是先把这个变量拷贝一份,使用的时候再创建出这个变量。变量捕获有个重要的前提这个变量没有变化,这个变量i显然变化了,那么就无法捕获,所以创建了临时变量n,来进行捕获。

    这里的线程池对象是通过直接通过Executors类调用的方法来获取实例对象。构造方法虽然可以重载,但是无法表示参数完全一致,但是意义却不同的不同构造方法。这里通过不同的普通方法,就可以实例出不同的线程池对象。这样的设计方式称为工厂模式,这样的类称为工厂类,方法称为工厂方法。

    Executors里面都是一些静态方法。它其实是对ThreadPoolExecutor这个类的包装,在包装的时候把Executors设计为了工厂模式。

ThreadPoolExecutor类介绍

    Java标准文档中就提出程序员被敦促使用更方便的Executors工厂方法。ThreadPoolExecutor类比较复杂所以就包装出了这个工厂方法。

构造方法参数介绍

  • corePoolSize:核心线程数。
  • maximumPoolSize:最大线程数。maximumPoolSize - corePoolSize:临时线程数,用来对线程池线程数量动态增加的一些线程。
  • keepAliveTime:临时线程的存活时间。当临时线程很久都没有使用,这些线程就被销毁了。
  • unit:时间单位。
  • workQueue:存储任务的工作队列。线程执行的任务都是从这个队列中拿的。
  • threadFactory:用来创建线程。
  • habdler:当阻塞队列满的时候,再向线程池提交新任务的“拒绝策略”。

拒绝策略介绍

  • 如果队列满了,再添加新任务,直接抛异常。
  • 如果队列满了,哪个线程提交的这个任务,就交给哪个线程执行。
  • ‘如果队列满了,接受新任务,丢弃最早的任务。
  • 如果队列满了,接收新任务,丢弃最新任务。

模拟实现线程池

思路:

    首先提供阻塞队列用来存储提交的任务,然后创建一些线程来从阻塞队列中取任务执行。设计submit方法为阻塞队列中提交任务。

代码实现

class MyThreadPool {
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
    //创建线程,处理任务
    public MyThreadPool(int n) {
        for(int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while (true) {
                    try {
                        Runnable take = queue.take();
                        take.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
        }
    }
    //提交任务
    public void submit(Runnable take) throws InterruptedException {
        queue.put(take);
    }
}

    注意:这里在多线程情况下,调用submit方法,没有线程安全问题。因为这里只涉及到了向阻塞队列里提交任务。

提出问题

线程池中的线程设计多少合适呢?

答:这个需要根据任务量的大小来确定。因为不同的任务量需要在一定效率下执行这些任务线程数量是不同的。我们需要根据实际进行测试,观察系统资源的使用是否符合我们的预期来最终确定线程数量。

小结:

    与大家共勉一句名言:”我希望,自己今后能以一朵花的姿态行走。穿越四季轮回,在无声中不颓废,不失色,一生花开成景,花落成诗“。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/146127.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

简单实现Java定时器

✨✨hello&#xff0c;愿意点进来的小伙伴们&#xff0c;你们好呐&#xff01; &#x1f43b;&#x1f43b;系列专栏&#xff1a;【JavaEE】 &#x1f432;&#x1f432;本篇内容&#xff1a;自己实现Java定时器 &#x1f42f;&#x1f42f;作者简介:一名现大二的三非编程小白&…

【手写 Vue2.x 源码】第九篇 - 对象数据变化的观测情况

一&#xff0c;前言 上篇&#xff0c;主要介绍了数组深层观测的实现&#xff0c;核心几个点如下&#xff1a; 最初仅对数组类型进行了原型方法重写&#xff0c;并未进行递归处理&#xff0c;所以&#xff0c;当时仅实现了数组的单层劫持&#xff1b; 通过对数组进行 observe…

二、Gtk4-GtkApplication and GtkApplicationWindow

1 GtkApplication 1.1 GtkApplication and g_application_run 人们编写编程代码来开发应用程序。什么是应用程序?应用程序是使用库运行的软件&#xff0c;其中包括操作系统、框架等。在GTK 4编程中&#xff0c;GTK应用程序是使用GTK库运行的程序(或可执行程序)。 编写GtkAp…

信创改造,你了解多少?

最近&#xff0c;“信创”一词在IT圈瞬间爆火&#xff0c;那什么是信创&#xff1f;又能带来哪些突破性的改变&#xff1f;跟随佑友一起来详细了解一下… 信创的来源 2016年3月4日&#xff0c;24家专业从事软硬件关键技术研究及应用的国内单位&#xff0c;共同发起成立了一个非…

分布式链路追踪-skywalking入门体验

背景 旁友&#xff0c;你的线上服务是不是偶尔来个超时&#xff0c;或者突然抖动一下&#xff0c;造成用户一堆反馈投诉。然后你费了九牛二虎之力&#xff0c;查了一圈圈代码和日志才总算定位到问题原因了。或者公司内部有链路追踪系统&#xff0c;虽然可以很轻松地通过监控判…

deb dpkg fpm cpack debmake 打包

文章目录deb 简介hello debfpmpreinst postinst prerm postrmcmake cpackdebmakedeb 简介 deb: Linux发行版Debian系列(如Debian, Ubuntu等)的软件包格式, 没有自提取(Self-extracting), 不能直接运行, 需要借助dpkg等安装. Dpkg: Debian Package Manager, Debian包管理器, 中…

Python读取各种形式文件(excel,txt),python基本用法

读取excel,读取结果是dataframe形式。 excelFile ranalyze_search_category.xlsx df pd.DataFrame(pd.read_excel(excelFile)) print(df) 详情&#xff1a;(21条消息) 在Python中使用Pandas.DataFrame对Excel操作笔记一 - 从Excel里面获取说需要的信息_fengqiaoxian的博客-CS…

TensorFlow之模型保存与加载

模型在训练过程中或者在训练之后&#xff0c;模型的执行过程能被保存&#xff0c;也就是&#xff0c;模型能从暂停中恢复以免训练的时间过长。因此&#xff0c;被保存的模型可以被共享&#xff0c;其他人可以重新构建相同的模型。被保存的模型以如下的两种方式进行共享&#xf…

青训营——前端方向练习题(不定项选择题例题)

文章目录 &#x1f4c4;前言 PART1 PART2 PART3 PART4 PART5 PART6 PART7 PART8 PART9 &#x1f4c4;前言 一共有十八题&#xff0c;题目选项为不定项&#xff0c;有单选&#xff0c;也有多选。 PART1 选择题 1&#xff1a; 下列哪些是 HTML5 的新特性&#xff1f; A…

Android 深入系统完全讲解(3)

3 Zygote 虚拟机的流程&#xff0c;学习方法 说完了 init 的启动过程&#xff0c;我们来说说 Zygote 的启动过程。 这里我们看下整个的步骤&#xff0c;主要完成了&#xff1a; 1 startVM() 创建虚拟机 2 startReg() 注册 JNI 方法 3 preload()预加载通用类&#xff0c;这里主…

【信管7.1】质量与质量管理过程

质量与质量管理过程对于我们的项目管理理论相关的学习来说&#xff0c;质量是除了范围、进度、成本之外的另一个核心内容。还记得我们在学习敏捷的时候讲过的项目管理三角形吗&#xff1f;通过之前的课程&#xff0c;我们已经学完了它的三个支点。接下来&#xff0c;我们就要学…

播客丨关于年终总结,程序员有话说

绘声绘影绘声绘影是网易云信独家打造的一档聚焦行业热点、个人成长方面的播客栏目。栏目希望通过邀请不同背景、不同行业、不同阅历的企业研发、产品、运营等相关岗位负责人作为节目嘉宾&#xff0c;以自身职业视角交流行业洞见和发展前景&#xff1b;以过来人的视角分享在时代…

dfs、bfs搜索题型小结

一、全排列 &#xff08;1&#xff09;1199&#xff1a;全排列 原题链接 解析 &#xff08;2&#xff09;剪枝思想 满足等式关系的全排列——dfs剪枝 &#xff08;3&#xff09;P1088 [NOIP2004 普及组] 火星人 原题链接 解析 二、组合&#xff08;选与不选&#xff09;…

web(四)—— CSS基础(选择器进阶、Emmet语法、背景属性、元素显示模式、三大特性)

一、选择器进阶目标&#xff1a;能够理解 复合选择器 的规则&#xff0c;并使用 复合选择器 在 HTML 中选择元素1. 复合选择器1.1 后代选择器&#xff1a;空格作用&#xff1a;根据 HTML 标签的嵌套关系&#xff0c;选择父元素 后代中 满足条件的元素 选择器语法&#xff1a;选…

Maven的安装配置与基本使用

Maven简介&#xff1a; Maven是专门用于管理和构建java项目的工具&#xff0c;它的主要功能有&#xff1a; 提供了一套标准化的项目结构标准化的项目结构&#xff1a; Maven提供了一套标准化的项目结构&#xff0c;所有的IDE使用Maven构建的项目结构完全一样&#xff0c;所有…

【IEEE出版社】人工智能、数据挖掘、机器人、传感等领域SCI,自引率低,对国人友好,评职毕业高分好刊~

1区人工智能类SCI&EI 【出版社】IEEE 【自引率】4.30%&#xff08;低&#xff09; 【国人占比】13.40% 【期刊简介】IF:6.5-7.0&#xff0c;JCR1区&#xff0c;中科院3区 【检索情况】SCI&EI 双检&#xff0c;正刊 【参考周期】3-5个月左右录用 【截稿日期】202…

如何彻底关闭Win10自动更新,Win10永久关闭自动更新的方法

如何彻底关闭Win10自动更新&#xff1f;Win10自动更新的问题是很多用户都遇到的问题&#xff0c;很多时候我们关闭了自动更新&#xff0c;过一段时间系统又自动更新了&#xff0c;由于win10自动更新非常顽固&#xff0c;所以我们要从多个地方下手才能永久关闭其自动更新&#x…

Java中几种常量池的区分

文章目录前言了解一下 ldc 指令字符串常量池在 Java 内存区域的哪个位置1.全局字符串池&#xff08;string pool也有叫做string literal pool&#xff09;2.class文件常量池&#xff08;class constant pool&#xff09;3.运行时常量池&#xff08;runtime constant pool&#…

干货 | Python的面试题目+答案合集

作为一个 Python 新手&#xff0c;你必须熟悉基础知识。 在本期内容中我们将讨论一些 Python 面试的基础问题和高级问题以及答案&#xff0c;以帮助你完成面试。 包括 Python 开发问题、编程问题、数据结构问题、和 Python 脚本问题。 接下来让我们来深入研究这些问题 Pytho…

AD转换芯片精度计算及校正方法

文章目录前言一、转换精度二、重要参数1.线性误差&#xff08;INL&#xff09;和差分线性误差&#xff08;DNL&#xff09;2.失调误差和增益误差三、转换校正总结前言 本文对模数转换芯片的精度进行简要介绍&#xff0c;帮助大家正确选型&#xff0c;并介绍了一个基本的ADC转换…