Java多线程基础-10:代码案例之定时器

news2024/7/6 20:56:23

定时器就是一个闹钟。它可以设定一个时间,当时间到,就可以执行某个指定的代码。

定时器是实际开发中的一种非常常用的组件。比如网络通信中,如果对方 500ms 内没有返回数据,则要求断开连接尝试重连;又比如一个 Map,希望里面的某个 key 3s 之后过期(自动删除)。类似于这样的场景就需要用到定时器。

Java标准库(java.util)中提供了一个定时器类:Timer。Timer 类的核心方法为 schedule()。

目录

一、Timer类的使用

1、schedule()方法

2、定时器的应用场景

3、用Timer管理多个任务

二、代码实现Timer

a. 如何实现Timer对象可以同时管理多个任务?

b. 定时器可能是有多个线程在执行 schedule 方法,那么就希望在多线程下操作 PriorityQueue 还能线程安全。

1、实现思路

a.初步代码

b.构造线程来具体执行任务

c.给队列中的MyTask元素设定优先级

d.解决CPU的忙等问题

2、完整代码


一、Timer类的使用

下面是使用Timer类创建定时器的代码示例,通过创建Timer对象timer,与调用timer的schedule()方法完成定时器的设定。

import java.util.Timer;
import java.util.TimerTask;

public class Test {
    public static void main(String[] args) {
        //1-创建Timer对象
        Timer timer = new Timer();
        //2-设定定时效果:5秒
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello");
            }
        },5000);
        System.out.println("BBB");
    }
}

运行代码结果如下:立即执行System.out.println("BBB");语句打印出BBB,等待5s后,执行定时器schedule()中设定的语句,打印"hello"。

1、schedule()方法

schedule()方法的作用是在某个特定的时间后,安排执行某个指定的操作。它有多个重载:

这些重载的共同点是,第一个参数为被安排的任务task,第二个参数(或后面的所有参数共同)表示设定的任务将要执行的时间。

以schedule(TimerTask task, long delay)为例,它的两个参数分别是:

  • task:被安排的任务。
  • delay:在任务执行之前的等待时间(毫秒数)。
Timer timer = new Timer();
timer.schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("hello");
   }
}, 3000);

这里的第一个参数TimerTask,本质上就是一个Runnable。查看TimerTask的源码可以发现,它其实是一个实现了Runnable接口的抽象类。因此,当创建TimerTask对象时,也需要重写实现run()方法。run()方法中定义的行为,表示任务具体要干什么。

new TimerTask() { ... }:创建一个匿名内部类,继承自TimerTask,并重写了run()方法。)

解析

程序启动后,立即打印出BBB,但未立即执行计时器timer中的代码。等待5秒后,程序打印hello。

这里有一个小细节:打印完毕hello后,程序并未终止退出。这是因为Timer里内置的前台线程阻止了当前进程结束。事实上,run方法的执行正是靠Timer内部的线程控制在时间到了之后执行的。(后面会讲解如何自己实现一个Timer类,Timer内部的线程是如何控制定时的,在那里会有只管的感受。)

2、定时器的应用场景

定时器的应用场景很多,尤其是在网络编程中。

实际中,很多的“等待”并不是无期限的,而是应当指定一个超时时间。比如打开浏览器,访问CSDN。如果此时的网络状况不是很好,那么加载的时间就可能会非常久。所以,浏览器会设置一个超时时间,如果达到了当前的超时时间还没有等待结果,就会提醒用户不要再等待下去了(504 gateway time out)。

3、用Timer管理多个任务

定时器内部可以管理很多个任务。如以下代码中,同时给定时器注册5个任务:

import java.util.Timer;
import java.util.TimerTask;

public class Test {
    public static void main(String[] args) {
        //1-创建定时器对象timer
        Timer timer = new Timer();

        //2-多次调用schedule()方法,给定时器注册多个任务
        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello5");
            }
        },5000);

        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello4");
            }
        },4000);

        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello3");
            }
        },3000);

        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello2");
            }
        },2000);

        timer.schedule(new TimerTask(){
            @Override
            public void run() {
                System.out.println("hello1");
            }
        },1000);

        System.out.println("hello0");
    }
}

运行程序后,立即打印hello0,之后每隔一秒进行一次打印。 

这里就像传说中时间复杂度为O(0)的休眠排序法(笑)(手动滑稽)


二、代码实现Timer

代码实现Timer主要要考虑到两个问题:

a. 如何实现Timer对象可以同时管理多个任务?

虽然任务有很多,但它们的触发事件是不同的。因此,只需要有一个或一组工作线程来负责每次找这些任务中时间最先达到的即可。

换句话说,一个线程先执行最早的的任务,做完了之后再执行第二早的任务,以此类推。而所有任务中,时间到了的就执行,没到的就再等等。

如何实现“每次找到这些任务中时间最先到达的任务”?——堆!

堆heap是这里要用到的核心数据结构!(排序的效率要低于堆,且插入新的元素时,要维持原序列的规律比较困难。

Java标准库中就提供了 PriorityQueue.

b. 定时器可能是有多个线程在执行 schedule 方法,那么就希望在多线程下操作 PriorityQueue 还能线程安全。

要保证线程安全,就要更进一步:Java标准库中提供了带优先级的阻塞队列,能够解决线程安全这个问题:PriorityBlockingQueue

通过查看优先级阻塞队列的源码可知,它遵循和优先级队列一样的顺序。可以根据任务的执行时间来建小根堆。不断取队头元素,取出的就是其中最小的元素。需要注意的是,该结构只有阻塞的入队列和阻塞的出队列,没有阻塞的查看队头元素方法。

1、实现思路

a.初步代码

首先创建一个MyTimer来模拟Timer,是一个自定义的定时器类。该类中封装有核心数据结构PriorityBlockingQueue。另外,创建一个类MyTask用于描述一个要执行的任务。一个要执行的任务中包含两方面信息,即:具体要做什么+什么时候做。

要特别注意的是,为了后续的判定方便,MyTask中的时间属性设定为绝对时间。

MyTimer定时器类中定义schedule()方法。其中根据传入的参数构造Task,将其插入队列中即可。注意,schedule()参数中的delay指的是相对时间,如3000,表示的是任务要在3000毫秒后执行。

因此,在构造MyTask时,要有一步绝对时间与相对时间之间的转换:

this.time = System.currentTimeMillis() + delay;

System.currentTimeMillis() 用于获取当前毫秒级别的时间戳,即当前时刻和基准时刻的ms数之差(基准时刻是1970年1月1日00:00:00.000)

代码展现如下:

class MyTask{
    public Runnable runnable;
    public long time;   //为了方便后续判定,使用的是绝对的时间戳

    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay; //转换:相对时间delay + 当前时间戳 => 绝对的时间戳
    }
}

//自定义定时器类
class MyTimer {
    //核心数据结构,带有优先级的阻塞队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    //此处的delay是一个相对时间,表示间隔多少时间后执行该任务
    public void schedule(Runnable runnable, long delay) {
        //根据参数构造MyTask,插入队列即可
        MyTask myTask = new MyTask(runnable,delay);
        queue.put(myTask);
    }
}

b.构造线程来具体执行任务

在计时器类的构造方法中构造扫描线程,用来扫描优先级阻塞队列中的各个任务是否到达可执行的时间。

逻辑很简单:先从queue中取出一个任务,这个任务应是所有任务中时间最小的。再获取当前时间curTime。通过比较curTime与myTask.time的大小来判断是否达到了该任务的执行时间。如果到达了,则执行myTask.runnable.run();如果没有达到,那么只能把刚才取出的任务放回队列中。

//注:该代码不是最终版,仅表明思路

class MyTimer {
    ...    //其他代码


    // 在这里构造线程,负责不停地扫描队首元素,判断该任务是否可以执行
    public MyTimer() {
        Thread t = new Thread(()->{
            while (true) {
                try {
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();  //获取当前时间
                    if(curTime < myTask.time) {
                        //当前时间小于任务时间:时间还没到
                        //暂不执行,要把刚才取出的任务塞回队列中
                        queue.put(myTask);
                    }else{
                        //时间到了,执行任务
                        myTask.runnable.run();  //执行任务
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

但是上面的代码存在两个严重的问题:

1、当前队列里的MyTask元素是按照什么规则来表示优先级的?

2、while(true)带来了CPU的忙等问题。

c.给队列中的MyTask元素设定优先级

PriorityBlockingQueue与PriorityQueue指定建堆顺序的方式是类似的。既可以通过将比较器Comparator传入构造器,也可以直接在元素中实现Comparable接口和compareTo方法。

这里我们只需让MyTask类实现Comparable接口,并以时间time为依据实现compareTo()方法即可。

回顾:PriorityQueue指定比较顺序的方式


在优先级队列中,元素的排序依赖于它们的比较结果。可以通过实现Comparable接口并定义compareTo()方法来指定对象之间的排序规则。这样,优先级队列就能根据对象的比较结果对元素进行自动排序。


如果不希望修改原始对象的类(或无法修改它),也可以创建一个实现了Comparator接口的单独类用于比较对象,并将该比较器作为参数传递给优先级队列的构造函数。使用比较器可以在不修改原始类的情况下定义对象之间的排序规则。

  • 如果在构造Priorityqueue时没有提供比较器,而是使用实现了Comparable接口的对象的compareTo方法进行比较,那么优先级队列将根据该方法的比较结果进行排序。
  • 如果对象没有实现Comparable接口或没有定义compareTo方法,也没有额外提供比较器,那么在添加元素时可能会抛出classCastException,因为优先级队列无法确定对象之间的顺序。
class MyTask implements Comparable<MyTask>{
    public Runnable runnable;
    public long time;   //为了方便后续判定,使用的是绝对的时间戳

    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay; //转换:相对时间delay + 当前时间戳 => 绝对的时间戳
    }

    //指定比较规则
    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time-o.time);
    }
}

 (tips:在写compareTo()时,可以不可以记忆到底是谁减去谁。可以先随便写一种,然后运行看看效果。如果不对再改成另一种相减方式。)

d.解决CPU的忙等问题

忙等,即虽然CPU确实在“等待”,但它也没捞着休息。就好比原本规定早上8点出门,我早上醒来后看了一眼手表,此时是7点,发现时间还没到;而过1分钟我又看一次表,发现是7点01,时间还没到;又过了1分钟我又去看表,此时是7点02,时间还没到……剩下的时间里我光顾着看表,也没有好好休息。

忙等在上述代码中表现为,while (true) 转的太快了,造成了无意义的 CPU 浪费。比如第一个任务设定的是 1 min 之后执行某个逻辑,但是这里的 while (true) 会导致每秒钟访问队首元素几万次,而当前距离任务执行的时间还有很久,剩下的时间里CPU光顾着进进出出访问队首元素了。

我们需要在等待的过程中释放CPU。

有同学可能会提出使用sleep(),但sleep()不是一个好的选择,因为sleep()的时间必须是固定的。如果sleep()的时间过长,恰好错过了任务的执行时间(睡过头了),就不妙了。

使用wait()就比较合适,可以随时提前结束。在等待过程中随时有新的任务过来,CPU就可以随时去处理。

所以代码逻辑更改为,如果时间还没到,则将刚取出的队首元素放回队列,并进入wait()等待直到时间到。而在插入队列元素时,必须调用notify()方法唤醒锁对象。代码如下:

//自定义定时器类
class MyTimer {
    //显式地指定锁对象:locker
    private Object locker = new Object();

    //核心数据结构,带有优先级的阻塞队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    //此处的delay是一个相对时间,表示间隔多少时间后执行该任务
    public void schedule(Runnable runnable, long delay) {
        //根据参数构造MyTask,插入队列即可
        MyTask myTask = new MyTask(runnable,delay);
        queue.put(myTask);
        //唤醒正在等待的线程*********************************
        synchronized (locker) {
            locker.notify();
        }
    }

    // 在这里构造线程,负责不停地扫描队首元素,判断该任务是否可以执行
    public MyTimer() {
        Thread t = new Thread(()->{
            while (true) {
                try {
                    //wait 必加锁
                    synchronized (locker) {
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();  //获取当前时间
                        if(curTime < myTask.time) {
                            //当前时间小于任务时间:时间还没到
                            //暂不执行,要把刚才取出的任务塞回队列中
                            queue.put(myTask);
                            //等待该任务的时间到*****************************************
                            locker.wait(myTask.time - System.currentTimeMillis());
                        }else{
                            //时间到了,执行任务
                            myTask.runnable.run();  //执行任务
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

为什么要在schedule方法中唤醒线程?其实也不难理解。

打个比方,比如当前时刻是 14:00 ,约定了 14:30 要执行上课这个任务。取出队首元素,发现时间是14:00,还没到,按逻辑就需要 wait 等待 30 分钟。

而在等待过程中,一个新的任务来了,14:10 要去接水。这样一来,就不能放任刚才的 wait 继续等了,而是需要唤醒 wait,此时工作线程就会重新取队首元素,这时取到的元素就是14:10去接水这个任务。这样做能够保证无论什么时候插入新任务,工作线程都能正确地把最小时间的任务取到。

在上述代码中,在schedule 方法中使用notify的目的是通知等待在locker对象上的线程。这是为了确保当添加新任务时,如果有线程正在等待队列中的任务执行完成,它能够被唤醒并重新检查队列。
因为在定时器类中的线程通过locker.wait(myTask.timeSystem.currentTimeMillis())进行等待,以等待下一任务的执行时间到来。如果没有通知等待的线程,即使有新任务加入队列,等待的线程也会继续等待,而不会重新检查队列是否有更早需要执行的任务。
因此,在schedule方法中调用notify是为了确保正在等待的线程能够及时得到通知,以重新检查队列并执行更早的任务。

2、完整代码

import java.util.*;
import java.util.concurrent.PriorityBlockingQueue;


class MyTask implements Comparable<MyTask>{
    public Runnable runnable;
    public long time;   //为了方便后续判定,使用的是绝对的时间戳

    public MyTask(Runnable runnable, long delay) {
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + delay; //转换:相对时间delay + 当前时间戳 => 绝对的时间戳
    }

    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time-o.time);
    }
}

//自定义定时器类
class MyTimer {
    //显式地指定锁对象:locker
    private Object locker = new Object();

    //核心数据结构,带有优先级的阻塞队列
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    //此处的delay是一个相对时间,表示间隔多少时间后执行该任务
    public void schedule(Runnable runnable, long delay) {
        //根据参数构造MyTask,插入队列即可
        MyTask myTask = new MyTask(runnable,delay);
        queue.put(myTask);
        synchronized (locker) {
            locker.notify();
        }
    }

    // 在这里构造线程,负责不停地扫描队首元素,判断该任务是否可以执行
    public MyTimer() {
        Thread t = new Thread(()->{
            while (true) {
                try {
                    //wait 必加锁
                    synchronized (locker) {
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();  //获取当前时间
                        if(curTime < myTask.time) {
                            //当前时间小于任务时间:时间还没到
                            //暂不执行,要把刚才取出的任务塞回队列中
                            queue.put(myTask);
                            locker.wait(myTask.time - System.currentTimeMillis());
                        }else{
                            //时间到了,执行任务
                            myTask.runnable.run();  //执行任务
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        //注册任务
        myTimer.schedule(()->{
            System.out.println("AAA");
        },4000);
        myTimer.schedule(()->{
            System.out.println("BBB");
        },3000);
        myTimer.schedule(()->{
            System.out.println("CCC");
        },2000);
        myTimer.schedule(()->{
            System.out.println("DDD");
        },1000);
        System.out.println("EEE");
    }
}

运行,查看结果:

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

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

相关文章

华为OD机试真题 Python 实现【网上商城优惠活动(一)】【2022 Q4 100分】,附详细解题思路

目录 一、题目描述二、输入描述三、输出描述四、补充说明五、Python算法源码六、效果展示1、输入2、输出3、说明 一、题目描述 某网上商城举办优惠活动&#xff0c;发布了满减、打折、无门槛3种优惠券&#xff0c;分别为&#xff1a; 1.每满100元优惠10元&#xff0c;无使用数…

测试开发 —— 快速定位问题

写在前面 这两天工作实在是有点小忙&#xff0c;感觉好久没更新了&#xff0c;但是平时也是有感而发的比较多&#xff0c;今天遇到一个问题&#xff0c;感觉挺有意思&#xff0c;处理过程也非常有意义&#xff0c;希望能给大家一个借鉴吧。 测试平台又又又出问题了 今天一位…

ARM_cortex-A7核UART总线

实验一&#xff1a;键盘输入一个字符a,串口工具显示b‘ 实验二&#xff1a;实现现象&#xff1a;键盘输入一个字符串&#xff0c;串口工具回显输入的字符串 uart4.h #ifndef __UART4_H__ #define __UART4_H__#include "stm32mp1xx_gpio.h" #include "stm32m…

vue+relation-graph绘制关系图实用组件

先在终端执行命令 vue create relationgraph创建一个vue2的项目 然后在编辑器中打开新创建的项目 在终端中执行命令 npm install relation-graph --save引入依赖 这样 我们relation-graph就进来了 然后 我们在需要使用的组件中编写代码如下 <template><div>&…

【HTML5+Springboot】农产品质量溯源大数据管理系统源码

一、前言 1.技术框架说明 开发工具&#xff1a;idea或eclipse前端框架&#xff1a;easyui &#xff0c;开发语言&#xff1a;java后端框架&#xff1a;spring bootmybatismysql数 据 库&#xff1a;mysql移 动 端&#xff1a;h5(扫码溯源)技术架构&#xff1a;spring bootmyb…

React Dva项目引入antd UI框架

上文 React 搭建DvaJS开发环境中我们大家了一个Dva的开发环境 那么 下面 我们就用dva项目引入一下antd 我们平时做react开发 主要也都会选择它 我们直接在项目终端执行 npm install antd^4.24.2 babel-plugin-import --save这样 我们的依赖包就进来了 babel-plugin-import是…

【精致的美少女-InsCode Stable Diffusion 美图活动一期】

&#x1f4a7; 【精致的美少女 − I n s C o d e S t a b l e D i f f u s i o n 美图活动一期】 \color{#FF1493}{【精致的美少女-InsCode Stable Diffusion 美图活动一期】} 【精致的美少女−InsCodeStableDiffusion美图活动一期】&#x1f4a7; &#x1f337; 仰望…

青岛大学_王卓老师【数据结构与算法】Week04_07_双向链表的删除_学习笔记

本文是个人学习笔记&#xff0c;素材来自青岛大学王卓老师的教学视频。 一方面用于学习记录与分享&#xff0c;另一方面是想让更多的人看到这么好的《数据结构与算法》的学习视频。 如有侵权&#xff0c;请留言作删文处理。 课程视频链接&#xff1a; 数据结构与算法基础–…

Standalone是什么?Standalone集群的三类进程

Standalone是什么 Standalone模式是Spark自带的一种集群模式&#xff0c;不同于前面本地模式启动多个进程来模拟集群的环境&#xff0c;Standalone模式是真实地在多个机器之间搭建Spark集群的环境&#xff0c;完全可以利用该模式搭建多机器集群&#xff0c;用于实际的大数据处…

面向订单交付的ETO项目管理数字化解决方案︱高远科技PMO副总经理董方好

北京高远华信科技有限公司PMO副总经理董方好先生受邀为由PMO评论主办的2023第十二届中国PMO大会演讲嘉宾&#xff0c;演讲议题&#xff1a;面向订单交付的ETO项目管理数字化解决方案。大会将于8月12-13日在北京举办&#xff0c;敬请关注&#xff01; 议题简要&#xff1a; 订单…

机器学习洞察 | JAX,机器学习领域的“新面孔”

在之前的《机器学习洞察》系列文章中&#xff0c;我们分别针对于多模态机器学习和分布式训练、无服务器推理进行了解读&#xff0c;本文将为您重点介绍 JAX 的发展并剖析其演变和动机。下面&#xff0c;就让我们来认识一下 JAX 这一新崛起的深度学习框架—— 亚马逊云科技开发…

threejs课程笔记-20 向量点乘、叉乘

向量点乘dot 点乘是向量的一种运算规则&#xff0c;点乘也有其它称呼&#xff0c;比如点积、数量积、标量积。 threejs三维向量Vector3封装了一个点乘相关的方法.dot()&#xff0c;本节课主要目的就是让大家能够灵活应用点乘方法.dot() 已知向量a和向量b 已知两个向量a和b&…

设计模式3:单例模式:静态内部类模式是怎么保证单例且线程安全的?

上篇文章&#xff1a;设计模式3&#xff1a;单例模式&#xff1a;静态内部类单例模式简单测试了静态内部类单例模式&#xff0c;确实只生成了一个实例。我们继续深入理解。 静态变量什么时候被初始化&#xff1f; public class Manager {private static class ManagerHolder …

探索 Jetson Nano 为 myCobot 280 机械臂提供的强大功能

探索 Jetson Nano 为 myCobot 280 提供的强大功能&#xff0c;机器人技术的一个有前途的组合 介绍 近年来&#xff0c;科学技术的发展给我们的生活带来了许多新的产品和服务&#xff0c;包括机器人在各个领域的集成。机器人已经成为我们生活中必不可少的一部分&#xff0c;从…

C语言求鸡兔同笼问题案例讲解

前言&#xff1a; 作者本人在今年4月份参加了一个C语言考试&#xff0c;编程大题里有一道鸡兔同笼问题&#xff1b;本来以为简简单单&#xff0c;几分钟搞定&#xff0c;拿个满分&#xff1b;结果翻车了&#xff0c;因为我在考场的时候想着&#xff0c;母鸡到底有几只脚呢&…

FlinkCDC第二部分-搭建Flink单机服务,ctrl就完事~

Flink版本&#xff1a;1.16 环境&#xff1a;Linux CentOS 7.0、jdk1.8 基础文件&#xff1a;flink-1.16.2-bin-scala_2.12.tgz、flink-connector-jdbc-3.0.0-1.16.jar、flink-sql-connector-mysql-cdc-2.3.0.jar 1. 在目录/home/flink下解压flink-1.16.2-bin-scala_2.12.tg…

基于 R 对卫星图像进行无监督 kMeans 分类

一、前言 本文将向您展示如何使用 R 对卫星图像执行非常基本的 kMeans 无监督分类。我们将在 Sentinel-2 图像的一小部分上执行此操作。 Sentinel-2 是由欧洲航天局发射的一颗卫星&#xff0c;其数据可在此处免费访问。 我要使用的图像显示了 Neusiedl 湖的北部&#xff08;奥地…

系统移植 根文件系统的移植 7.5

根文件系统的移植 根文件系统&#xff1a;根目录下的所有文件和工具的集合 根文件系统是内核启动后挂载的第一个文件系统系统引导程序会在根文件系统挂载后从中把一些基本的初始化脚本和服务等加载到内存中去运行文件系统层次结构标准 文件具体的属性只能在内核中看到&#xf…

django-vue-admin curd_demo 快速crud教程

django-vue-admin curd_demo 快速crud教程 快速CRUD开发教程&#xff1a;https://bbs.django-vue-admin.com/article/9.html 如何在 env.py 文件配置Mysql数据库&#xff1a;https://bbs.django-vue-admin.com/question/4.html 导入导出配置教程&#xff1a;https://bbs.djang…

Linux MySQL三种安装方式

MySQL 三种常用安装方式&#xff1a; 离线安装&#xff1a; 在mysql官网进行下载&#xff0c;步骤如下&#xff1a; 然后找到这个&#xff1a; 因为我这里使用的OS为CentOS7&#xff0c;大家可以按自己的系统进行选择。 最后通过XFTP/SCRTXF将文件传到虚拟机上。 然后将…