• 多线程(1)


    多线程


    前言 : 上文主要了解到了进程, 那么为啥需要引入进程呢?

    或者说为啥要有进程呢?

    其实这里最主要的目的是为了解决 并发编程 这样的问题。


    了解 :

    这里 cpu 进入了多核心的时代,想要进一步提高程序的执行速度,就需要充分的利用CPU的多核心资源.

    cpu 再往小了做 就比较困难 ,此时先要通过单核心提高执行速度就遇到了困难, 既然一个核心不行那么多个核心呢?显然是可以的,要不一句话人多力量大吗。

    但是不是说 cpu的核心多了,程序一下就跑的快了 , 还需要你的程序代码能供把这些 cpu 核心给用上 。

    引出多线程

    回到进程这里, 关于我们的多进程编程,已经是可以解决并发编程的问题(可以利用cpu的多核资源) ,但是因为频繁的创建和销毁进程对cpu的资源开销是比较大的 ,调度进程同样也是 ,另外 使用进程会拖慢了我们的运行速度 。

    正因为 使用 进程, 开销比较大速度比较慢 , 为了解决这个问题 我们的线程就应运而生 , 这里线程也称为 轻量级进程

    那么为啥说进程的开销比较大呢 ?

    这里主要就在 资源分配 / 回收上 , 之前说过 进程是分配资源的最小单位, 我们想要创建进程就需要给它分配资源 (内存 资源, 硬盘资源等), 此时分配资源就需要花费一定的时间.


    那么为啥说线程的开销小速度快呢 ?

    其实也就是线程将分配资源/ 回收上 / (申请资源和回收资源) 的这一部分开销给省略了


    为了好理解 这里举一个例子 :


    那种流水线的厂子不知道大家进没进过,鄙人有幸在里面做过一天的事情,可以说是非常的累的 。

    假设 有一个厂是生产手机的 ,一天能生产 1w部手机, 但是市场上需求量非常高,导致一天生产 1w部手机可能不够,此时厂长就想要提高产量,让每天有 2 w 部手机产出 ,这里就有两种解决方式

    1.在其他的方法重新开一个厂,然后两个厂同时生产那么此时是不是一天就可以达到2w部,


    2.既然是流水线,那么是不是可以多增加一条流水线,此时是不是同样可以使 一天 2 w 部 (假设之前的厂只有一条流水线,但大部分不可能是这个情况)


    对比一下这两种方法,你觉的是 1 好 还是 2 好 显然是 2 好,因为重新开一个厂 ,需要挑选场地, 建厂, 装流水线等都是需要花钱的,而 方法二 新增一条流水线 ,就不需要花额外的钱去 挑选场地,建厂 。


    这里是不是就可以认为 方法一 是使用进程 ,而方法二是线程呢?


    既然将 进程 想象成 厂 , 线程想象程流水线, 这里就可以得出 进程和线程之前的关系 。

    一个进程可以包含一个线程,也可以包含多个线程(注意 : 这里不能没有)

    好理解 : 厂吗不可能没有流水线的,要不然咋生产商品赚钱呢 做慈善吗, 同样厂可以拥有一条,也可以拥有多条.

    另外 : 正因为我们的厂内可以拥有多条流水线,那么是不是只有创建厂和新建第一条流水线的时候开销比较大,之后添加的流水线,就直接在厂内添加即可? 放到进程和线程上来说,也就是我们启动第一个线程的时候开销是比较大的 去申请 资源 (没有资源咋办事吗),后面的进程就省事了, 因为是不会去申请和释放资源的直接用即可.


    通过上面总结 : 一个进程可以包含多个线程程 , 这些线程之间共用同一份资源


    补充: 这里的资源指的是 内存 和 文件描述符表

    内存 : 线程1 new 的对象 在 线程 2 , 3 , 4 等 都可以直接使用

    文件描述符表 : 线程 1 打开的文件 , 在线程 2 , 3 , 4 等轴都可以直接使用 .

    另外 : 操作实际调度的时候,是以线程为单位进行调度的.

    回到上文 所说的进程调度就不够准确, 准确来说是以线程为单位来调度的 , 如果站在上文的角度 说进程调度就相当于每一个进程只有一个线程的情况.

    补充 : 这里每个进程中的线程都是独立在CPU上进行调度的 , 换句话来说 线程是操作系统调度执行的基本单位


    另外 : 一个线程就是一个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码.


    看完上面, 有没有想过 我们要如何去描述一个线程呢 ?

    在上文 介绍的 进程 通过 PCB来描述的,那么线程是不是也是呢 ?

    答案 : 我们的线程同样也是通过PCB来描述的.


    注意 :这里别晕,其实PCB对应的就是线程,为啥上文说是对应进程呢?因为是站在一个进程只有一个线程的角度,所以才会所对应PCB


    此时就有趣了,既然 线程也是通过 PCB 来描述的,那么进程又可以拥有多个线程,那么进程是不是会对应多个PCB呢 ?


    没错 我们的进程 是可能对应多个PCB的,正因线程同样也是被PCB描述的,进程中又含有多个线程所以,进程就会对应多个PCB了,

    在这里插入图片描述

    那么还有一个问题 :上文介绍 过一对PCB的属性 状态 ,上下文, 优先级 ,记账信息,是每个线程都有自己的还是共同使用一个呢 ?

    答案 : 每个线程都会拥有自己的各自记录各自的,但是 同一进程里的PCB之间, pid是一样的,内存指针和文件描述符表也是一样的.


    描述 看完 , 再来像一个问题 , 既然我们使用多线程 比较轻量 ,能提高效率,那么我们能不能一直创建线程呢?

    答案 不是的 ,虽然创建线程是能提高我们的 效率但是,线程一多,会导致多个线程进行资源的竞争,此时肯定会有没有抢到的线程(资源就那么一份),此时就会拖慢我们的效率。


    举例 :

    图一 :

    在这里插入图片描述


    图二 :

    在这里插入图片描述


    总结 : 进程和线程的区别 (注意 :这里是非常经典的面试题)


    1.进程是包含线程的 , 一个进程可以含有多个线程

    2.进程和线程都是处理并发编程的场景

    3.进程比较重 , 线程比较轻 .

    为啥进程重 因为频繁的创建和消费进程的时候效率是非常低的(消耗的资源多) , 而线程就不会 ,

    为啥线程轻 因为只有启动第一个线程的时候才会去创建和消耗资源,其他的线程是公用同一份资源的。

    4.进程是具有独立性的, 每一个进程之间都拥有各自的虚拟地址空间,一个进程挂了,其他进程是不受影响的,而线程则不会,因为线程之间公用同一块资源,所以当其中一个线程出现了异常就可能导致整个进程崩溃

    5.进程是操作系统分配资源的基本单位

    6.线程是操作系统调度的执行的基本单位


    理论知识 看完,下面来看我们java如何进行多线程操作

    Thread

    引用 :

    线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).

    Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.


    下面我们就通过 Thread 来创建我们的线程。


    第一步 : 打开idea 创建类
    在这里插入图片描述


    第二步 : 创建 Thread 对象

    在这里插入图片描述


    问题 一: 为啥使用 Thread 类不需要通过 import 导入别的 包 ?

    问题二 :你还见过那些类也是类似的.


    答: 我们之前见过的String , StringBuilder ,StringBuffer 都是这样的,因为他们都是在java.lang底下的,因为java.lang 编译器会自动给我们导入.

    问题 看完了, 回到上面,其实这里的线程还是没有创建完成,因为我们创建线程是希望线程成为一个独立的执行流(执行一段代码),上面的程序并没有给他执行的任务。

    那么这里的问题就来到了如何指定他的任务呢 ?

    这里总共有 5种方法,下面就来看看


    方法一 : 实现一个类继承Thread 重写run方法 .

    在这里插入图片描述


    run方法里面就写我们需要执行的任务 .


    下面就来执行我们的程序看一下效果 :

    在这里插入图片描述


    注意 : 只有我们 通过 s.start() 去启动线程的时候才会创建线程, 而 new 是不会创建线程的 .

    之前说过我们的线程是用来解决并发编程的,上面并没有体现出来,下面我们来修改一下我们的代码,来看看如何并发的。

    在这里插入图片描述


    因为这里执行的非常快我们就可以通过 sleep 让线程休眠一秒来看执行效果

    在这里插入图片描述


    正因为 抢占式执行,才会出现我们的线程安全问题 . 随机调度导致多个线程竞争同一份资源.


    另外在来补充一下 t.runt.start 的区别

    在这里插入图片描述

    扩充 : 通过 jdk 自带的工具jconsole 查看当前的 java 进程中的所有进程

    在这里插入图片描述

    图二 : 观察线程

    在这里插入图片描述

    图三 :

    在这里插入图片描述

    此时第一种写法就完成 了 ,下面来看第二种 :


    方法二 : 实现Runnable 接口

    在这里插入图片描述

    这里的Runnable 作用 是描述 一个要执行的任务 , run 方法是任务的执行细节.

    这里我们不能直接通过 new Runnable (接口不能实例化) 传给Thread 来执行 , 所以需要去实现一个类 MyRunnable ,然后通过 new 传给 Thread类.

    可以看到, 我们的任务是 通过 Runnable 描述的,我们将任务传给了线程, 这里就 将线程 和 线程需要干的活进行了解耦合操作.

    这里的好处 就是未来的莫一天,我们要改代码,不用多线程了,使用多进程, 或者线程池或者协程等 此时代码的改动就比较小.


    方法三 : 匿名内部类 继承Thread 重写 run方法

    知识点 : 《内部类》

    在这里插入图片描述


    这里的方法三其实就是方法一,只不过这里使用了匿名内部类


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

    在这里插入图片描述


    这个写法与 写法2 等价 ,只不过实现 Runnable 的是一个匿名内部内 .


    方法五 : 使用Lambda 表达式

    知识点 : 反射 - 枚举 - Lambda表达式

    在这里插入图片描述

  • 相关阅读:
    vue+vite+ts添加eslint校验和代码提交校验
    基于AM335X开发板 (ARM Cortex-A8)——Linux系统使用手册 (中)
    vue2和vue3 的双向绑定原理
    7,vue利用axios调用后台api接口
    音视频开发:ffplay使用sonic实现倍速播放
    stm32 freeRTOS lwip TCP快速发送,内存泄露问题
    读周志华《机器学习》第四章--决策树
    Hadoop的基本框架
    Json 转sqlserver创建表脚本 JSONtoSQLGenerator
    Neo4j-Graph数据库技能树学习体验及一些建议
  • 原文地址:https://blog.csdn.net/mu_tong_/article/details/128049369