• 多线程---认识线程


    什么是进程?

    进程,也叫做“任务”,一个跑起来的程序就是进程。也就是说进程是运行起来的程序。在同一时刻,操作系统中的进程有很多,他们是如何管理的呢?

    如何管理进程?

    管理进程实际上就是做两件事儿:

    1. 描述进程:详细的表示一个进程有哪些属性、哪些信息?这是通过一个结构体来实现的,这个结构体里面包含了进程的各种信息,这个结构体叫做PCB(进程控制块);
    2. 组织进程:通过一个数据结构把若干个PCB联系起来,使其可以进行增查改删。这个数据结构一般使用双向链表。

    注:

    1. 创建进程实际上就是创建一个PCB,然后把它放到双向链表中。
    2. 结束进程实际上就是在双向链表中找到这个节点,然后把这个节点删除。
    3. 查看进程列表实际上是遍历整个双向链表。
    4. 一个进程可能是一个PCB,也可能是多个。

    认识PCB

    认识PCB,就是了解PCB里面到底包含了哪些信息:

    • pid:是一个进程的身份标识

      在同一台主机,同一时刻,这些进程的pid是唯一的。通过pid来区分进程。

    • 内存指针:描述进程持有的内存资源

      当我们双击一个可执行文件时,操作系统就要把这个文件的核心数据加载到内存中,同时会在内存中创建进程PCB。这就会给进程分配一定的内存空间,这个内存空间会被分为不同的区域,内存指针就是来描述每个区域是干嘛的。

    • 文件描述符表:描述进程持有的文件资源

      每个进程都可以打开一些文件(存储在硬盘上的数据),文件描述符表里就记录了当前这个进程打开了哪些文件。

    • 进程状态:描述进程当前能否被调用

      就绪状态:进程可以被调度到CPU上执行。
      阻塞状态:进程不能被调度到CPU上执行。

    • 进程优先级:描述进程调用的先后顺序

      在创建进程时,可以通过一些系统调用来干预优先级。

    • 进程上下文:保存当前进程执行过程中产生的中间结果。

      一个进程在CPU执行一会儿之后,会切换到另一个进程执行,在过一段时间之后可能会再次切换回来继续执行。那么此时就需要知道上次执行到哪儿了。进程上下文就是用来保存中间结果的。

    • 进程记账信息:统计一个进程在CPU上执行了多久

      进程在执行时由进程优先级控制执行哪个进程,但是这样就有可能导致某个进程一直执行不到。通过统计进程记账信息,能让进程调度更均衡,避免执行不到某个进程。

    了解进程调度的过程

    其中,进程状态、进程优先级、进程上下文和进程记账信息都是和进程调度相关的信息,那么什么是进程调度呢?

    我们先要明白: 进程是操作系统进行资源分配的基本单位

    进程调度其实是由“并行” + “并发”的方式执行的。

    并行,即:在每个CPU核心上都可以独立的运行一个进程,多个CPU核心就可以同时运行多个进程。

    并发,即:在一个CPU核心上,先运行一下进程1,再运行一下进程2,再运行一下进程3,再运行一下进程1…这样循环执行。只要切换的速度足够快,宏观上看起来三个进程就是在同时运行。

    进程状态、进程优先级、进程上下文和进程记账信息存在的意义,就是支撑“进程调度”

    虚拟地址空间

    虚拟地址空间也是进程中非常关键的概念。

    我们知道在创建进程时,都会给每个进程分配一定的内存空间,用来完成进程的工作。即:
    在这里插入图片描述

    在正常情况下,进程各自使用各自的内存,不会有任何问题。但是如果某个进程使用了野指针,不小心访问到了别的进程的内存且进行了修改。这就是个大问题:它这样做不仅仅影响到了自己的执行,而且还影响到了别人的执行。我们就通过虚拟地址空间来避免这个问题。

    在这里插入图片描述

    我们通过“虚拟地址空间”让每个进程都拥有自己的内存空间,并且和其他进程的内存空间隔离开。当进程要访问内存时通过MMU设备进行虚拟内存空间到真正内存空间的映射,访问真正的内存。如果发现有进程访问的内存越界,MMU设备就会进行拦截,关闭此进程,不让它影响到其他进程。

    面对有些需要让多个进程配合的场景,又引入了进程间通信机制。它的原理就是:找到一块所有进程都能访问的公共资源,然后基于公共资源来交换数据。

    什么是线程?

    虽然多进程已经实现了并发编程,但是有一个巨大的问题:如果频繁的创建进程、销毁进程,那么这个操作就比较低效。

    创建进程的过程:1. 创建PCB 2.给进程分配资源并赋值到PCB中 3. 把PCB插入链表
    销毁进程的过程:1. 把PCB从链表上删除 2. 把PCB持有的资源释放 3. 销毁PCB

    其中,分配资源和释放资源对操作系统来说要做的工作非常多,需要花费大量的时间。

    因此,程序员就发明了“线程”。一个进程默认至少有一个线程,也可能有多个线程。这些线程都可以单独的在CPU上进行调度。最重要的是:同一个进程中的这些线程共用同一份系统资源(内存+文件),创建线程和销毁线程的开销远小于进程。所以,也把线程称为“轻量级进程”。

    前面提到操作系统是通过PCB来描述进程的,更准确的说法是通过一组PCB来描述进程的。
    每一个PCB对应一个线程,而一个进程可能包含多个线程。

    使用多线程有一些优势:

    1. 能够充分利用多核CPU,提高效率。
    2. 只有创建第一个线程的时候需要申请资源,后续再创建新的线程都是共用同一份资源,节省了申请资源的开销;销毁线程的时候,也只有销毁到最后一个线程的时候才释放资源,节省了释放资源的开销。

    使用多线程也有一些问题:

    1. 线程数目不是越多越好。当CPU核心已经饱和时,继续增加线程不会提高效率。反而会因为线程太多,线程的调度开销太大,影响了效率。
    2. 线程之间可能会相互影响到,造成线程安全问题
    3. 如果某个线程发生了意外就可能让整个进程奔溃

    进程 VS 线程

    1. 进程包含线程
    2. 线程比进程更轻量,创建更快,销毁也更快
    3. 同一个进程的多个线程共用同一份系统资源(内存+文件),进程和进程之间则是有各自的系统资源(内存+文件)
    4. 进程是资源分配的基本单位,线程是调度执行的基本单位

    Thread类的属性和方法

    Thread类的属性

    在这里插入图片描述

    public class Test {
        public static void main(String[] args) {
            Thread thread = new Thread();
            System.out.println(thread.getId());
            System.out.println(thread.getName());
            System.out.println(thread.getState());
            System.out.println(thread.getPriority());
            System.out.println(thread.isDaemon());
            System.out.println(thread.isAlive());
            System.out.println(thread.isInterrupted());
    
    //        20
    //        Thread-0
    //        NEW
    //        5
    //        false
    //        false
    //        false
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1. id是线程的唯一标识,不同的线程id不会重复。
    2. name在自己调试的时候会用到,可以自己在线程的构造方法里定义。
    3. state表示线程现在的状态(在下面介绍)
    4. priority在线程调度的时候会使用
    5. daemon:守护线程,也叫后台线程。前台进程:会阻止进程的退出,如果main线程执行完后,前台线程还没执行完,会等待前台线程执行完再退出进程;后台进程:不会组织进程的退出,当main线程执行完就退出进程。 我们创建的线程默认是前台线程。
    6. alive: 判断内核线程在不在。当new 出Thread对象但没有使用start方法启动时,不会把线程放入内核,使用start方法后才会把线程放入内核执行;当线程在内核执行完任务后,就会退出内核,清除内核线程,但是Thread对象还在。
    7. interrupt:线程中断,让线程提前结束,本质是让run方法尽快结束,不是让run方法执行到一半就退出。interrupt有两种情况:1. 如果线程正在执行 则设置标记位为true中断线程 2. 如果线程被阻塞 则唤醒sleep抛出异常 被catch捕获后在catch里处理,有两种中断的方式。
    • 使用线程库里面自带的标记位
        //通过使用标准库里自带的标记位
        public static void main(String[] args) {
            Thread t = new Thread(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // e.printStackTrace();
    
                        // [方式一] 立即结束线程
                        break;
    
                        // [方式二] 啥都不做, 不做理会. 线程继续执行
    
                        // [方式三] 线程稍后处理
                        // Thread.sleep(1000);
                        // break;
                    }
                }
                System.out.println("t 线程执行完了");
            });
    
            t.start();
    
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            t.interrupt();
            System.out.println("设置让 t 线程结束!");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 自定义一个标记位
    //自己设置一个标记位  用来中断退出
        public static boolean isQuit = false;
    
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                while (!isQuit){
                    System.out.println("执行线程");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
    
            while (true){
                System.out.println("执行main");
                isQuit = true;
                System.out.println("手动中断线程");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    Thread类的方法

    构造方法

    在这里插入图片描述
    构造方法主要是用来创建线程的,现在共有七种创建线程的方式,点击查看

    在构造方法里可以添加一个String类型的参数,用来命名线程。

    普通方法

    • start():启动线程。把线程放到内核中执行。

    • interrupt():中断线程。让线程提前退出。

    • join():线程等待。在main中调用join(),就是等待该线程执行完了再执行main线程。

    • sleep():线程休眠。让线程阻塞一段时间。

      PCB在管理线程时有俩个队列:一个就绪队列、一个阻塞队列。调用sleep()就是把线程放到阻塞队列里,等阻塞时间结束再放回就绪队列参与调度。

    线程的状态

    在这里插入图片描述

    线程一共有六大状态,我们可以这样理解:
    在这里插入图片描述

  • 相关阅读:
    java车牌识别系统mysql
    猿创征文|瑞吉外卖——初步了解
    Java学习-面向对象下
    最新720全景云系统/可生成小程序+带PC端+安装教程/价值800元的720云全景系统源码
    HTTP协议
    【蓝牙协议栈】【BR/EDR】【MAP】蓝牙短信访问协议
    std::integer_sequence 源码分析
    c++编译的四个阶段
    服务器数据恢复-vmware ESXI虚拟机数据恢复案例
    springcloud alibaba nacos -1
  • 原文地址:https://blog.csdn.net/weixin_62976968/article/details/134078200