多线程必知必记的

简介: 《基础系列》

进程与线程的区别是什么?

批处理操作系统

批处理操作系统就是把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。

批处理操作系统在一定程度上提高了计算机的效率,但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,导致CPU闲置所以批处理操作效率也不高

进程的提出

批处理操作系统的瓶颈在于内存中只存在一个程序,进程的提出,可以让内存中存在多个程序,每个程序对应一个进程,进程是操作系统资源分配的最小单位。CPU采用时间片轮转的方式运行进程:CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。多进程的好处在于一个在进行IO操作时可以让出CPU时间片,让CPU执行其他进程的任务。

线程的提出

随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。

进程和线程的区别

进程是计算机中已运行程序的实体,进程是操作系统资源分配的最小单位。而线程是在进程中执行的一个任务,是CPU调度和执行的最小单位。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O)

  • 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
  • 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
  • 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。

另外一个重要区别是,进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。

独立性

Linux系统会给每个进程分配4G的虚拟地址空间(0到3G是User地址空间,3到4G部分是kernel地址空间),进程具备私有的地址空间,未经允许,一个用户进程不能访问其他进程的地址空间。

动态性

程序是一个静态的指令集合,而进程是正在操作系统中运行的指令集合,进程有自己的生命周期和各种不同的状态。

五态模型一般指的是:

新建态(创建一个进程)

就绪态(已经获取到资源,准备好了,进入运行队列,一旦获得时间片可以立即执行)

运行态(获取到了时间片,执行程序)

阻塞态(运行过程中等待获取其他资源,I/O请求等)

终止态(进程被杀死了)

并发性

多个进程可以在CPU上并发执行。 线程是独立运行和调度的最小单位,线程会共享进程的虚拟空间,一个进程会对应多个线程。在Java中,线程拥有自己私有的程序计数器,虚拟机栈,本地方法栈。

PS:虚拟内存

虚拟内存是一种逻辑上扩充物理内存的技术。基本思想是用软、硬件技术把内存与外存这两级存储器当做一级存储器来用。虚拟内存技术的实现利用了自动覆盖和交换技术。简单的说就是将硬盘的一部分作为内存来使用。

PS:虚拟地址空间

每个进程有4G的地址空间,在运行程序时,只有一部分数据是真正加载到内存中的,内存管理单元将虚拟地址转换为物理地址,如果内存中不存在这部分数据,那么会使用页面置换方法,将内存页置换出来,然后将外存中的数据加入到内存中,使得程序正常运行。

进程间如何通信?

进程间通信的方式主要有管道,

管道

调用pipe函数在内存中开辟一块缓冲区,管道半双工的(即数据只能在一个方向上流动),具有固定的读端和写端,调用

#include <unistd.h>
int pipe(int pipefd[2]);点击复制代码复制出错复制成功

Java中创建线程有哪些方式?

第一种 继承Thread类,重写Run方法

这种方法就是通过自定义CustomThread类继承Thread类,重写run()方法,然后创建CustomThread的对象,然后调用start()方法,JVM会创建出一个新线程,并且为线程创建方法调用栈和程序计数器,此时线程处于就绪状态,当线程获取CPU时间片后,线程会进入到运行状态,会去调用run()方法。并且创建CustomThread类的对象的线程(这里的例子中是主线程)与调用run()方法的线程之间是并发的,也就是在执行run()方法时,主线程可以去执行其他操作。

class CustomThread extends Thread {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"线程调用了main方法");
        for (int i = 0; i < 10; i++) {
            if (i == 1) {
                CustomThread customThread = new CustomThread();
                customThread.start();
                System.out.println(Thread.currentThread().getName()+"线程--i是"+i);
            }
        }
        System.out.println("main()方法执行完毕!");
    }
    void run() {
        System.out.println(Thread.currentThread().getName()+"线程调用了run()方法");
        for (int j = 0; j < 5; j++) {
            System.out.println(Thread.currentThread().getName()+"线程--j是"+j);
        }
        System.out.println("run()方法执行完毕!");
    }
}点击复制代码复制出错复制成功

输出结果如下:

main线程调用了main方法
Thread-0线程调用了run()方法
Thread-0线程--j是0
main线程--i是1
Thread-0线程--j是2
Thread-0线程--j是3
Thread-0线程--j是4
run()方法执行完毕!
main()方法执行完毕!点击复制代码复制出错复制成功

可以看到在创建一个CustomThread对象,调用start()方法后,Thread-0调用了run方法,进行for循环,对j进行打印,与此同时,main线程并没有被阻塞,而是继续执行for循环,对i进行打印。

执行原理

首先我们可以来看看start的源码,首先会判断threadStatus是否为0,如果不为0会抛出异常。然后会将当前对象添加到线程组,最后调用start0方法,因为是native方法,看不到源码,根据上面的执行结果来看,JVM新建了一个线程调用了run方法。

private native void start0();
public synchronized void start() {
       //判断当前Thread对象是否是新建态,否则抛出异常
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    //将当前对象添加到线程组
    group.add(this);
    boolean started = false;
    try {
        start0();//这是一个native方法,调用后JVM会新建一个线程来调用run方法
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}点击复制代码复制出错复制成功

扩展问题:多次调用Thread对象的start()方法会怎么样?

会抛出IllegalThreadStateException异常。其实在Thread#start()方法里面的的注释中有提到,多次调用start()方法是非法的,所以在上面的start()方法源码中一开始就是对threadStatus进行判断,不为0就会抛出IllegalThreadStateException异常。

7.png

注意事项:

start()方法中判断threadStatus是否为0,是判断当前线程是否新建态,0是代表新建态(上图中的源码注释里面有提到),而不是就绪态,因为Java的Thread类中,Thread的Runnable状态包括了线程的就绪态和运行态,(Thread的state为RUNNABLE时(也就是threadStatus为4时),代表线程为就绪态或运行态)。执行start()方法的线程还不是JVM新建的线程,所以不是就绪态。有一些技术文章把这里弄错了,例如这一篇《深入浅出线程Thread类的start()方法和run()方法》

6.png

总结

这种方式的缺点很明显,就是需要继承Thread类,而且实际上我们的需求可能仅仅是希望某些操作被一个其他的线程来执行,所以有了第二种方法。

相关文章
|
12月前
|
前端开发 JavaScript 定位技术
《从Web到原生:Cordova框架如何搭建功能互通的桥梁》
Cordova是一个开源移动开发框架,通过WebView组件运行HTML、CSS和JavaScript编写的Web应用,并借助插件机制实现与设备原生功能的交互。开发者可轻松调用相机、地理位置等原生功能,无需深入原生代码细节。Cordova拥有丰富的官方及第三方插件生态,支持从硬件访问到网络通信等多种功能,助力快速开发跨平台应用。然而,在使用过程中需关注插件兼容性、性能优化及数据安全等问题。Cordova打破了Web与原生间的壁垒,让开发者用熟悉的技术构建功能丰富、体验流畅的应用。
417 17
|
Kubernetes Cloud Native 调度
云原生批量任务编排引擎Argo Workflows发布3.6,一文解析关键新特性
Argo Workflows是CNCF毕业项目,最受欢迎的云原生工作流引擎,专为Kubernetes上编排批量任务而设计,本文主要对最新发布的Argo Workflows 3.6版本的关键新特性做一个深入的解析。
|
Java 编译器 Maven
@Data@NoArgsConstructor@AllArgsConstructor 这几个常用注解什么意思?
@Data@NoArgsConstructor@AllArgsConstructor 这几个常用注解什么意思?
1698 1
|
算法 计算机视觉
OpenCV(四十五):ORB特征点
OpenCV(四十五):ORB特征点
405 0
|
Cloud Native 安全 Anolis
免费、安全、可靠!一站式构建平台 ABS 介绍及实例演示 | 龙蜥技术
一文了解软件包构建、镜像构建、内核源码构建、云原生构建 4 大构建服务。
|
网络协议 算法 数据库
【网络层】RIP协议详解(应用层)、慢收敛、OSPF协议(适合大网络)
【网络层】RIP协议详解(应用层)、慢收敛、OSPF协议(适合大网络)
386 0
|
JavaScript 前端开发
ESLint 和 Prettier 配置冲突解决方案
ESLint 和 Prettier 配置冲突解决方案
大尺度信道建模 | 带你读《大规模天线波束赋形技术原理与设计 》之二十四
本节将介绍 3D 信道模型中 不同传输场景的大尺度衰落模型,包括路损计算、穿透损耗、直射径概率、阴 影衰落等。
大尺度信道建模  | 带你读《大规模天线波束赋形技术原理与设计 》之二十四
|
资源调度 分布式计算 Hadoop
YARN(Hadoop操作系统)的架构
本文详细解释了YARN(Hadoop操作系统)的架构,包括其主要组件如ResourceManager、NodeManager和ApplicationMaster的作用以及它们如何协同工作来管理Hadoop集群中的资源和调度作业。
865 3
YARN(Hadoop操作系统)的架构