JavaEE:多线程(1):线程是啥?怎么创建和操作?

news2024/9/22 21:23:17

进程的问题

本质上来说,进程可以解决并发编程的问题

但是有些情况下进程表现不尽如人意

1. 如果请求很多,需要频繁的创建和销毁进程的时候,此时使用多进程编程,系统开销就会很大

2. 一个进程刚刚启动的时候,需要把依赖的代码和数据从磁盘加载到内存中

但是从系统分配一个内存不是件容易事情

申请内存的时候需要指定大小,系统内部把各种大小的空闲内存,通过一定的数据结构组织起来

实际申请的时候要去这样的空间中查找,找到大小合适的空间,再进行分配

所以引入一个概念,线程

线程

线程称为轻量级进程,保持进程的独立调度执行,同时省去了分配资源释放资源带来的额外开销

使用PCB描述一个线程

多个线程的PCB的内存指针指向同一个内存空间

这意味着只用创建第一个线程的时候需要从系统分配资源,后续的线程就不必分配,直接共用前面的那份资源就可以了

除了内存之外,文件描述符表(硬盘资源)也是多个线程共用一份的

把能够进行资源共享的线程分成组,称为线程组

一个进程可以有1个PCB,也可以有多个PCB(意味着这个进程包含了一个线程组,也就是包含多个线程)

上面图中这多个线程合起来就是一个进程

进程和线程的关系

有线程之前,进程要扮演两个角色:资源分配的基本单位;调度执行的基本单位

有线程之后,进程专注于资源分配,线程专注于调度执行

例子:一个滑稽吃100只鸡,消耗的时间比较多

第一个方案:搞两个房间,每个滑稽吃50只,速度大幅度增加

这个相当于多进程,创建新的进程就要申请更多的资源(房间和桌子)

第二个方案,房间和桌子数量不变,但是滑稽数量增加到两个

这种就是多线程,资源开销更小 

更多的滑稽,每个滑稽分到的鸡就更少了,但是速度更快了

但是线程分配越多越好吗?

当引入的线程达到一定数量之后,再继续引入线程就没办法提升了,因为线程之间会竞争CPU资源,但是CPU资源是有限的,非但提高不了效率,还会增加调度的开销

而且多个线程之间会打架,可能导致代码中出现逻辑错误

这种就是线程安全问题

另外,多线程共享资源也有副作用,1号和2号滑稽抢鸡腿,1号抢到了,2号勃然大怒:都别吃了!直接把整个桌子给破坏了

也就是说,一个线程如果抛出异常而且没有处理好的话,可能会导致整个进程被终止

总结一下

1. 进程包含线程

2. 每个线程是一个独立的执行流,可以执行代码并参与CPU调度中(每个线程都有状态,优先级,记账信息和上下文)

3. 每个线程都有自己的资源,进程中的线程共享一份资源

4. 进程和线程之间,不会相互影响。但是如果同一个进程中某个线程抛出异常,可能导致进程中其他线程异常终止

5. 同一个进程中的线程之间会互相干扰,引起线程安全问题

6. 线程太多会导致调度开销过多的问题


多线程编程

Java推荐多线程编程,系统提供了多线程编程的API

Java里一般把跑起来的程序称为进程,没有运行起来的程序(exe),称为可执行文件

一个进程里的第一个线程称为主线程,main方法就是主线程的入口

创建线程

这里的run类似于main方法,是一个Java进程的入口方法;不需要程序员手动调用,会在合适的时机(线程创建好了之后),被JVM自动调用执行

这种风格的函数被称为回调函数

(回调函数是一种特殊的函数,它作为参数传递给另一个函数,并在被调用函数执行完毕后被调用)

为什么要override呢?

重写的本质是扩展现有的类,Thread标准库里面的run是不知道你的需求是什么,必须要手动指定

//1. 创建一个自己的类来继承Thread(在java.lang里面,自动导包)
class MyClass extends Thread{
    @Override
    public void run(){
        System.out.println("hello world");
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        //2. 根据刚才的类创建出实例(线程实例才是真正的线程)
        Thread t = new MyClass();
        //3.调用Thread的start方法,才会真正调用API,在系统内核中创建出线程
        //对于同一个Thread对象来说,start只能调用一次
        t.start();
    }
}

操作系统的内核:操作系统最核心的功能模块,负责管理硬件,给软件提供稳定的运行环境

例子:

我们平时运行的idea,qq音乐这些应用程序都是运行在用户态了

但是这些程序有时候需要针对一些系统提供的软硬件资源进行操作,这些操作需要调用系统的api,进一步在内核钟完成这样的操作

为什么要划分出用户态和内核态?

为了稳定,防止应用程序把硬件设备或者软件资源给搞坏了

系统封装了一些API,这些API属于合法操作,这些应用程序只能调用这些合法API,就不会被系统/硬件设备造成伤害

操作系统 = 内核+配套的应用程序


观察线程流

例子

按照之前的理解,如果一个代码中出现两个死循环,最多只能执行一个,另一个循环就进不去了

但真正运行程序可以看到两个循环都在执行

此处调用start创建线程之后,兵分两路;一路沿着main方法继续执行,打印hello main

另一路进入到线程的run方法,打印hello thread  

此时两个线程并发执行,但是这些线程执行的先后顺序是不确定

因为操作系统的内核中有一个调度器模块,这个模块的实现方式是一种类似于随机调度的效果

随即调度?(也是抢占式执行)

1. 一个线程什么时候被调度到CPU上执行,时机是不确定的

2.一个线程什么时候从CPU上下来,给别的线程让位,时机也是不确定的


不只是可以用打印来观察,我们还可以用jdk中的jconsole工具来进一步分析出线程流

点进去,选择本地连接,连接到我们刚刚的程序中(注意刚刚的程序要运行起来才能在这里找到)

 

 

一个Java进程中,包含的线程很多,除了我标出来的两个线程

其余的线程都是JVM自带的线程,进行一些垃圾回收,监控统计各种指标,把统计指标通过网络的方式传输给其他程序


由于我们的程序中的两个循环都是死循环,循环体就单纯打印,一旦程序运行起来,这俩循环就会转地飞快;也会导致CPU占用率比较高,进一步提高电脑的功耗

我们可以在循环中加入一个Thread提供的静态方法sleep来降低循环速度

class MyThread2 extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("hello thread");
            try {
                sleep(1000);//每秒钟打印一次
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread t = new MyThread2();
        t.start();

        while(true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

这里为什么不能直接用sleep(1000)呢?

因为sleep这个过程中可能被提前唤醒

为什么第一个sleep只有一个try catch的处理选项而不能throws,而第二个sleep可以throws异常?

如果这里加上throws,修改方法签名,此时就无法构成重写了,因为父类的run没有throws这个异常,子类重写的时候就不能throws异常 

我们可以看见两个线程交替运行,而且先执行main还是thread是不一定的

但是,由于我们主线程main调用start方法之后就立即往下执行打印了,内核就要通过刚才线程的API构建出线程出来,并且执行run

因为线程创建本身就有开销,所以第一轮打印的时候,在创建线程开销本身的影响下,hello thread会比hello main慢一点打印出来


创建线程其他方式

上面是线程创建的第一种方式:继承Thread,重写run

下面介绍第二种方式:实现Runnable接口,重写run

class MyThread3 implements Runnable{
    @Override
    public void run() {
        while(true){
            System.out.println("hello runnable");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo3 {
    public static void main(String[] args) {
        Runnable runnable = new MyThread3();//只是一段可执行的代码
        Thread t = new Thread(runnable);//还要搭配Thread类,才能真正在系统中创建出线程
        t.start();
        while(true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

这种写法其实就是把线程和要执行的任务进行了解耦合了


第三种写法:继承Thread,重写run,但是使用匿名内部类

写{ }意思是要定义一个类,这个新类继承自Thread,此处{ }中可以定义子类的属性和方法。这里最主要重写run方法

public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

第四种方法:实现Runnable,重写run,实现匿名内部类

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello runnable");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        t.start();
        while(true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

第五种方法:使用lambda表达式(推荐使用)

Thread构造方法有好几个版本,编译器在编译的时候挨个往里匹配,其中匹配到Runnable这个版本的时候发现run方法无参数,正好能和lambda对上


Thread其他属性和方法

Thread(String name) :自己创建的线程,默认是按照Thread-0 1 2 3 ...

给线程起名字不会影响线程执行,只是方便调试

⚠线程之间的名字是可以重复的

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"这是我的线程");
        t.start();
    }

 运行起来查看一下线程

这里的“这是我的线程”就是刚刚创建的新线程


getId():JVM自动分配的身份标识,会保证唯一性

isDaemon:判断是否是守护线程(后台线程)

后台?

前台线程的运行会阻止进程结束

后台线程的运行不会阻止进程结束

观察我们上面的线程执行图,列表中已经没有main了,但是t仍在执行

此时的t就属于是前台线程,只要前台线程没执行完,进程就不会结束

把这个线程改成后台

我们发现啥都没打印,说明后台线程无法阻止进程结束

JVM里其他线程都是后台线程

isAlive:表示内核中的线程PCB是否还存在

当我们输入t.start()的时候就真正在内核中创建出这个PCB,此时isAlive就是true

⚠Java定义的线程对象的生命周期和内核中的PCB生命周期是不完全一样的


中断线程

让run()方法提前执行完毕,也就能提前终止线程了

    private static boolean isQuit = false;
    public static void main(String[] args) {
        //lambda表达式本质上就是函数式接口,也就是匿名内部类,内部类访问外部类成员天经地义
        Thread t = new Thread(()->{
            while(!isQuit){
                System.out.println("我是一个线程,工作中");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程工作完毕");
        });
        t.start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("让进程结束");
        isQuit = true;
    }

上面的代码是让main线程打印完毕之后才会设置isQuit,t线程的循环才会结束

如果是先isQuit = true之后再打印“让进程结束”的话,是主线程先进行打印还是t线程先进性打印就不确定了

诶?那能不能把isQuit写进main函数里把它变成局部变量呢?

哦吼,编译出错!

lambda表达式讲过一个语法:变量捕获,可以访问到外面定义的局部变量。那为啥还报错??

看看这里的报错信息,访问到的局部变量得是final或者事实final修饰的

根据报错信息,我们得在上面加一个final

但是下面的isQuit = true又报错了,因为final定义的变量不能修改

所以嘞,isQuit无法作为局部变量

再继续挖~为啥Java这里对于变量捕获有final的限制?

isQuit是局部变量的时候是属于main方法的栈帧中,但是Thread lambda是有自己独立的栈帧的

这样可能导致main方法执行完了,栈帧销毁了,同时Thread的栈帧还存在,还想用isQuit。因为变量捕获本质上是传参,也就是让lambda表达式在自己的栈帧中创建一个新的isQuit,并把外面的isQuit的值拷贝进来

Java为了防止isQuit不同步,干脆不让你修改isQuit,就是让你加个final


上面那样写还不够优雅,下面推荐一手更优雅的写法

Thread提供了这种方法,用于获取当前的线程实例t

public class ThreadDemo13 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            //isInterrupted 判定标志位
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("我是一个线程, 正在工作中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程执行完毕!");
        });

        t.start();

        Thread.sleep(3000);
        // 使用一个 interrupt 方法, 来修改刚才标志位的值,设置标志位
        System.out.println("让 t 线程结束");
        t.interrupt();
    }
}

t.interrupt相当于isQuit = true

但是上面的代码有一个报错异常,其实也是这里的interrupt导致sleep出现异常

在在执行sleep的过程中,调用interrupt,大概率sleep的时间还没到,被提前唤醒了

提前唤醒的话程序会做两件事:

1. 抛出interruptedException(接着被catch到)

2. 清除Thread对象的isInterrupted标志位(sleep清除的)

sleep(1000),可能1000ms都没到就要终止线程,相当于前后两个矛盾的操作;

所以sleep清除标志位可以给程序员更多的可操作空间:

1)让线程提前结束(加break)

2)让线程不结束,继续执行(不加break)

3)让线程执行一些逻辑之后再结束(写其他代码再break)

要想让进程结束,只需要在catch里面加上break就行了

不想要打印异常信息就把这一行注释掉就行了

实际开发中,catch里面需要写什么代码?

1. 尝试自动回复的程序

2. 记录日志(非严重的问题)

3. 发出报警(严重问题)

4. 少数正常业务逻辑(比如文件操作)


等待线程

前面提到,多个线程底层的执行顺序是不确定的

但是我们可以在应用程序中,通过一些api来影响线程的执行顺序

join关键字:影响线程结束的先后顺序

实现t2线程等待t1线程,就要让t1先结束,t2后结束;join可以使t2线程阻塞

t.join();

在main线程中调用上面这个代码,意思是让main线程等待t线程结束(t执行,main阻塞)

具体来说,执行join的时候会先看t线程是否正在运行,如果t运行中,main线程就会阻塞

t运行结束,main线程就会从阻塞状态恢复,继续往下执行


练习:让主线程创建一个新线程,由新线程负责1+2+...+1000的运算

public class ThreadDemo15 {
    private static int result = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i <= 1000 ; i++) {
                result += i;
            }
        });
        t.start();
        //使用sleep,但是不知道t线程要执行多久,sleep里面的时间不好填
        //Thread.sleep(1000);

        //使用join,严格按照t线程执行结束来作为等待条件
        //什么时候t运行结束,什么时候join结束等待
        t.join();

        System.out.println("result: " + result);
    }
}

如果数据量大一点,可能这个线程就会计算的很慢,比如

i <= 10_0000_0000L

我们可以分多几个线程一起计算

三个线程兵分三路,并发执行(并发 = 并行(t和t2在两个不同核心上同时执行)+并发(t和t2在同一个核心上分时复用执行))

关于join的细挖

第一个join是死等,就一根筋一定要等到你的t线程执行完才能继续下一个线程

第二个是带有超时时间的等,等有一个时间上限的(超时时间),如果等待的时间超时了,那就不等了


获取线程引用

如果是继承Thread,直接使用this拿到线程实例

class MyThread5 extends Thread{
    @Override
    public void run() {
        System.out.println(this.getId() + ", " + this.getName());
    }
}
public class ThreadDemo16 {
    public static void main(String[] args) {
        MyThread5 t1 = new MyThread5();
        MyThread5 t2 = new MyThread5();
        t1.start();
        t2.start();
        System.out.println(t1.getId() + ", " + t1.getName());
        System.out.println(t2.getId() + ", " + t2.getName());
    }
}

如果是Runnable 或者 lambda的方式,this就无能为力了,此时this已经不再指向Thread对象了

就只能使用Thread.currentThread

public class ThreadDemo17 {
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            Thread t = Thread.currentThread();
            System.out.println(t.getName());
        });
        Thread t2 = new Thread(()->{
            Thread t = Thread.currentThread();
            System.out.println(t.getName());
        });

        t1.start();
        t2.start();
    }
}

线程的状态/线程安全详见下一篇博客

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

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

相关文章

spring 笔记四 SpringMVC 组件解析

文章目录 SpringMVC 组件解析SpringMVC的执行流程SpringMVC的执行流程SpringMVC组件解析SpringMVC注解解析 SpringMVC 组件解析 SpringMVC的执行流程 SpringMVC的执行流程 ① 用户发送请求至前端控制器DispatcherServlet。 ② DispatcherServlet收到请求调用HandlerMapping处…

《PCL多线程加速处理》-滤波-统计滤波

《PCL多线程加速处理》-滤波-统计滤波 一、效果展示二、实现方式三、代码一、效果展示 提升速度随着点云越多效果越明显 二、实现方式 1、原始的统计滤波实现方式 #include <pcl/filters/statistical_outlier_removal.h>pcl::PointCloud<pcl::PointXYZ

imazing 2.17.16中文版怎么备份iPhone手机照片

imazing 2.17.16中文版备份功能非常强大&#xff0c;在进行备份之前&#xff0c;需要对其进行设置&#xff0c;才能充分使用备份功能。当移动设备与电脑第一次连接成功后&#xff0c;会询问是否要立即备份&#xff0c;个人的建议是选择“稍后”&#xff0c;小伙伴可以先对软件进…

ssm基于HTML5的OA办公系统论文

基于HTML5的OA办公系统 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;作为一个一般的企业都开始注重与自己的信息展示平台&#xff0c;实现基于HTML5的OA办公系统在技术上已成熟。本文介绍了基于HTML5的OA办公系统的开发全过程。通过分析企业对于博客网站的需…

WPF 显示PDF、PDF转成图片

1.NuGet 安装 O2S.Components.PDFView4NET.WPF 2.添加组件 工具箱中&#xff0c;空白处 右键&#xff0c;选择项 WPF组件 界面&#xff0c;选择NuGet安装库对面路径下的 O2S.Components.PDFView4NET.WPF.dll 3.引入组件命名空间&#xff0c;并使用 <Windowxmlns"htt…

spring 笔记六 SpringMVC 获得请求数据

文章目录 SpringMVC 获得请求数据获得请求参数获得基本类型参数获得POJO类型参数获得数组类型参数获得集合类型参数请求数据乱码问题参数绑定注解requestParam获得Restful风格的参数获得Restful风格的参数自定义类型转换器获得Servlet相关API获得请求头RequestHeaderCookieValu…

【漏洞复现】华脉智联指挥调度平台/script_edit/fileread.php文件读取漏洞

Nx01 产品简介 深圳市华脉智联科技有限公司&#xff0c;融合通信系统将公网集群系统、专网宽带集群系统、不同制式、不同频段的短波/超短波对讲、模拟/数字集群系统、办公电话系统、广播系统、集群单兵视频、视频监控系统、视频会议系统等融为一体&#xff0c;集成了专业的有线…

SpringBoot中日志的使用log4j2

SpringBoot中日志的使用log4j2 1、log4j2介绍 Apache Log4j2 是对 Log4j 的升级&#xff0c;它比其前身 Log4j 1.x 提供了重大改进&#xff0c;并提供了 Logback 中可用的许多改 进&#xff0c;同时修复了 Logback 架构中的一些问题&#xff0c;主要有&#xff1a; 异常处理…

超聚变服务器(原华为服务器)网站模拟器

一、超聚变服务器&#xff08;原华为服务器&#xff09;网站模拟器&#xff1a; 原来了解服务器可以从他的网站上进行了解&#xff0c;模拟器做的很好了。 https://support.xfusion.com/server-simulators/ 有很多的模拟器&#xff0c;今天主要看下BMC的设置 有很多的在线工具…

后端接口开发-web前台请求接口对后台数据库增删改查-实例

一、后端接口开发的逻辑是&#xff1a; 1.Application项目启动 2.前台接口Url请求后台 3.Controller控制拿到前台请求参数&#xff0c;传递给中间组件Service 4.Service调用Mapper.java 5. mapper.java映射到mapper.xml中的mybatis语句&#xff0c;类似Sql语句操作数据库 6.其…

DB-GPT大模型私有化部署搭建

一、前言 随着大模型的发布迭代&#xff0c;大模型变得越来越智能&#xff0c;在使用大模型的过程当中&#xff0c;遇到极大的数据安全与隐私挑战。在利用大模型能力的过程中我们的私密数据跟环境需要掌握自己的手里&#xff0c;完全可控&#xff0c;避免任何的数据隐私泄露以及…

Java 基础学习(九)API概述、Object、String、正则表达式

1 API概述 1.1 API概述 1.1.1 什么是API API(Application Programming Interface)&#xff0c;意为&#xff1a;应用程序接口。API就是已经写好的的程序或功能&#xff0c;程序要需要时可以直接调用&#xff0c;无需再次编写。 API可以大致分为如下几类&#xff1a; 编程语…

语音识别功能测试:90%问题,可以通过技术解决

现在市面上的智能电子产品千千万&#xff0c;为了达到人们使用更加方便的目的&#xff0c;很多智能产品都开发了语音识别功能&#xff0c;用来语音唤醒进行交互&#xff1b;另外&#xff0c;各大公司也开发出来了各种智能语音机器人&#xff0c;比如小米公司的“小爱”&#xf…

微服务学习:Gateway服务网关

一&#xff0c;Gateway服务网关的作用&#xff1a; 路由请求&#xff1a;Gateway服务网关可以根据请求的URL或其他标识符将请求路由到特定的微服务。 负载均衡&#xff1a;Gateway服务网关可以通过负载均衡算法分配请求到多个实例中&#xff0c;从而平衡各个微服务的负载压力。…

AGI魔盒,会放出冥王PLUTO还是阿童木?

人机共生&#xff0c;是科幻作品永恒的主题。其中&#xff0c;《冥王PLUTO》可能是最早探讨人类与机器人如何在冲突中共存的漫画作品。 如果说阿童木是人机共生的“和平使者”&#xff0c;启蒙了几代人对机器人的信任和热爱,那么冥王PLUTO就是阿童木的反面&#xff0c;一个心怀…

了解如何在linux使用podman管理容器

本章主要介绍使用 podman 管理容器。 了解什么是容器&#xff0c;容器和镜像的关系 安装和配置podman 拉取和删除镜像 给镜像打标签 导出和导入镜像 创建和删除镜像 数据卷的使用 管理容器的命令 使用普通用户管理容器 使用普通用户管理容器 对于初学者来说&#xff0c;不太容…

如何在iPad Pro上实现SSH远程连接服务器并进行云端编程开发【内网穿透】

文章目录 前言1. 在iPad下载Code APP2.安装cpolar内网穿透2.1 cpolar 安装2.2 创建TCP隧道 3. iPad远程vscode4. 配置固定TCP端口地址4.1 保留固定TCP地址4.2 配置固定的TCP端口地址4.3 使用固定TCP地址远程vscode 前言 本文主要介绍开源iPad应用IDE如何下载安装&#xff0c;并…

C语言之文件操作(下)

C语言之文件操作&#xff08;下&#xff09; 文章目录 C语言之文件操作&#xff08;下&#xff09;1. 文件的顺序读写1.1 文件的顺序读写函数1.1.1 字符输入/输出函数&#xff08;fgetc/fputc&#xff09;1.1.2 ⽂本⾏输⼊/输出函数&#xff08;fgets/fputs&#xff09;1.1.3 格…

亿赛通电子文档安全管理系统 SQL注入漏洞复现

0x01 产品简介 亿赛通电子文档安全管理系统&#xff08;简称&#xff1a;CDG&#xff09;是一款电子文档安全加密软件&#xff0c;该系统利用驱动层透明加密技术&#xff0c;通过对电子文档的加密保护&#xff0c;防止内部员工泄密和外部人员非法窃取企业核心重要数据资产&…

【Jenkins】节点 node、凭据 credentials、任务 job

一、节点 node Jenkins在安装并初始化完成后&#xff0c;会有一个主节点&#xff08;Master Node&#xff09;&#xff0c;默认情况下主节点可以同时运行的任务数是2&#xff0c;可以在节点配置中修改&#xff08;系统管理/节点和云管理&#xff09;。 Jenkins中的节点&#…