初识多线程

1. 前置知识——进程

在学习多线程前需要了解操作系统中的基本知识,这里简单回顾下。

1.1 进程控制块

一个进程对应着一个进程控制块PCB,PCB是一个用于管理和维护进程信息的数据结构,这个数据结构中大致包含下面内容(并不完整):

  1. 进程标识符PID:唯一标识进程的数值
  2. 进程分配的资源:如分配的内存(指向内存的指针)以及文件描述符表(该进程打开了什么文件)等
  3. 程序计数器:指向进程当前执行的指令的地址(由于进程需要经常被调度,因此需要寄存器记录该进程执行到哪了)

1.2 进程的五种状态

进程共有五种状态,它们分别为:

  1. 运行态:指进程上CPU运行的状态;
  2. 就绪态:指进程在就绪队列中等待CPU调度的状态;
  3. 阻塞态:指进程上CPU运行过程中,(由于某些原因)发现需要阻塞,知道满足条件后才能回到等待调度的状态;
  4. 创建态:操作系统为进程分配资源,创建PCB;
  5. 终止态:操作系统回收进程资源,撤销PCB;

状态间的关系如下:

Untitled Diagram.drawio.png

1.3 进程的调度

进程的调度是指一个进程由就绪态到运行态的过程,在引入多线程之前,调度的基本单元是进程,这里我们先了解一下进程的调度,以便后续了解多线程的调度。

我们可以把就绪队列简单的看作一个链式队列,就绪队列会根据PCB的优先级组织PCB,当CPU处于空闲状态的时候,调度器就会从就绪队列中取出PCB,此时进程就由就绪态转为运行态了。

image.png

在程序猿的视角中,调度器将进程由就绪态调度上CPU进行运行的过程是透明的,我们可以把这么一个过程看作调度器对就绪队列的进程的随机调度。

2. 线程的引入

引入线程后,调度的基本单位不再是进程,而是线程。也就是说前面我们讲到的进程的调度,此时基本单位是线程,也就是在

2.1 进程与线程的区别

  1. 一个进程可能包含一个或多个线程,而一个线程只能属于一个进程
  2. 每个进程都有独立的虚拟地址空间,也有自己独立的文件描述符,同一个进程的多个线程之间,则共用这一份虚拟地址空间和文件描述符表
  3. 进程是资源分配的基本单位,线程是调度的基本单位
  4. 一个进程挂了一般不会影响到其他进程,一个线程挂了很可能把整个进程带走,其他线程也就没了

2.2 为什么要使用多线程

为了提高CPU的资源利用率,可能会选择通过多进程以及多线程的方式来处理一段程序,然而在编程中为什么更加倾向于使用多线程呢,原因如下:

  1. 首先,由于进程的独立性,每个进程都有自己独立的虚拟地址空间,因此进程间进行通信的步骤较为麻烦;

  2. 更为重要的一点是创建进程需要涉及资源分配的工作,如分配内存空间以及创建文件描述符表,而同一个进程的多个线程共享资源,则省去了分配资源的步骤。


3. 第一个自定义线程

其实我们在程序开发的过程中早就涉及到多线程了:

public class Demo1 {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

即使是一个最简单的hello world程序,其实在运行的时候也设计到“线程”了,虽然我们没有手动在上面的代码中创建其他线程,JVM内部也会创建出多个线程,如:主线程,垃圾扫描线程等。

3.1 定义线程

通过继承Thread的方式自定义一个线程,run方法中描述的是线程执行的具体任务:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello thread!");
    }
}

run方法描述的是线程的工作

3.2 创建线程

前面只是定义了一个线程需要完成的工作,我们需要在程序中实例化线程,并且调用它的start()方法才算是创建了一个线程:创建出线程的TCB,并加入到就绪队列中,参与调度。

public class Demo1 {

    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        System.out.println("hello world!");
    }
}

运行结果:

hello world!
hello thread!

3.3 线程的随机调度

这里如果对调度器的随机调度理解不是很深,可能会提出一个疑问:明明我们是先调用了t.start()方法,为什么运行结果中先打印了hello world!呢?

线程中没有父线程和子线程的概念,多个线程涉及并发与并行,后续我们统称为并发

  1. 并发指的是当cpu忙碌的时候轮流调用线程,一般是一个时间片结束的时候,将cpu中运行的线程放入就绪队列,然后再随机调度就绪队列中的线程
  2. 并行指的是多核cpu能同时进行运行多个线程

原因:前面我们讲到,创建线程后会将TCB添加到就绪队列中等待调度器调度,然而调度的过程是随机的,不可预知的

我们在启动程序的时候就会有一个main线程,而当main进程在cpu中执行到t.start()语句后,会创建一个MyThread线程(在就绪态等待调度器调度),由于两个线程是并发的,因此打印的结果是随机的。


听了上面的解释,大家此时可能又有一个疑问:既然调度是随机的,为什么我执行了这么多次都是先打印的Hello world!

由于线程的创建是需要开销的,因此可能大家尝试了许多次都是先执行main线程中的语句,但谁也不能保证第n次运行程序的时候,顺序是否发生变化。

3.4 进程退出码

在console中不只打印了hello world!hello thread!,还打印了一句:

Process finished with exit code 0

操作系统中用进程的退出码来表示“进程的运行状态”,而上面的code 0就是进程的退出码,在C语言阶段的main函数有一个return值,都是写作了return 0

  • 使用0表示进程执行完毕,结果正确
  • 使用非0标识进程执行完毕,结果不正确
  • 如果还没有返回,表示进程此时正在运行
  • 进程崩溃,此时返回的值很可能是一个随机值

4. jconsole的使用

在jdk中,有一个叫jconsole的运行程序,通过该程序可以观察线程的基本信息以及调用方法栈,在多线程的开发中经常需要使用该工具来定位问题。

这里我们加上一个死循环观察线程的调度。

class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello thread!");
        }
    }
}

public class Demo1 {

    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
        while (true) {
            System.out.println("hello world!");
        }
    }
}

找到自己jdk的位置,并打开jconsole(如果是windows,需以管理员身份打开),我的系统是macos,位于jdk目录下的:jdk-1.8.jdk/Contents/Home/bin/jconsole:

在这里可以看到java的所有进程,由于我的启动类名为Demo1,因此直接连接thread.Demo1

image.png

直接点击不安全的连接:

image.png

重点关注这两个线程,分别为main线程,和我们刚才自定义的线程。

image.png

在列表的右边显示的就是当前线程的信息,上半部分为线程信息,下半部分为线程的函数调用栈,线程信息的内容如下:

  1. 线程名称Name(程序员可在创建时自定义)
  2. 状态State:此时状态为阻塞blocked,原因是main此时在使用打印机,因此被main线程阻塞
  3. 总阻塞次数

5. 创建线程的常见方式

  1. 创建一个类继承Thread,重写run,前面已经用过,不多赘述

  2. 创建一个类,实现runnable接口,重写run

    此时Runnable相当于定义了一个任务,还是需要实例化Thread实例,把任务交给Thread,这个写法,线程和任务是分开的,可以更好地解耦合。

//实现runnable接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello thread!");
        }
    }
}
public class Demo2 {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        //创建线程
        thread.start();
        while (true) {
            System.out.println("hello world!");
        }
    }
}
  1. 匿名内部类(Thread)
public class Demo3 {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread!");
                }
            }
        };
        thread.start();
        
        while (true) {
            System.out.println("hello world!");
        }
    }
}
  1. 匿名内部类(Runnable)
public class Demo4 {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello thread");
                }
            }
        });

        thread.start();

        while (true) {
            System.out.println("hello world");
        }
    }
}
  1. [推荐]lambda表达式为方式4的简化版
public class Demo5 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("hello thread!");
            }
        });
        thread.start();
        while (true) {
            System.out.println("hello world");
        }
    }
}

6. 性能对比

对比单线程和双线程的情况下,变量自增20亿次所耗费时间

单线程:

public class Demo6 {
    private static final long COUNT = 10_0000_0000;
    private static void serial() {
        long begin = System.currentTimeMillis();

        int a = 0;
        for (int i = 0; i < COUNT; i++) {
            a++;
        }
        a = 0;
        for (int i = 0; i < COUNT; i++) {
            a++;
        }
        long end = System.currentTimeMillis();
        System.out.println("共花费了" + (end - begin) + "ms");
    }

    public static void main(String[] args) {
        serial();
    }
}

多线程:这里需要用到join()方法来保证两个线程执行完毕才计时,并且该方法可能抛中断异常InterruptedException(当线程运行中断时会触发的异常)

private static void concurrency() {
    long begin = System.currentTimeMillis();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < COUNT; i++) {
            int a = 0;
            a++;
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < COUNT; i++) {
            int a = 0;
            a++;
        }
    });
    t1.start();
    t2.start();

    try {
        //使用join方法,保证等待t1,t2两个线程执行完毕
        t1.join();
        t2.join();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }
    long end = System.currentTimeMillis();
    System.out.println("共花费了" + (end - begin) + "ms");
}

public static void main(String[] args) {
    concurrency();
}

在我的机器上,单线程的耗费时间大概在1500~1600ms区间,双线程的耗费时间大概在900~1000ms区间。

由此可见线程虽说可以提高效率,但并不是预想中的双线程就将性能提高两倍左右,因为多线程的场景涉及创建线程以及频繁的调度线程的开销。

7. 多线程的使用场景

  1. CPU密集型场景:代码中大部分工作都是在使用CPU进行运算(比如反复++的操作),使用多线程可以更好的利用CPU多个核心并行计算资源,从而提高效率。
  2. I/O密集型场景:读写磁盘,读写网卡这些操作都属于I/O,当线程在运行时遇到I/O操作就会由运行态转为阻塞态,串性执行程序的话,此时CPU就会处于空闲的状态,引入多线程可以避免CPU过于闲置。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

头歌实践教学平台:CG1-v1.0-点和直线的绘制

第1关&#xff1a;OpenGL点的绘制 一. 任务描述 根据下面要求&#xff0c;在右侧修改代码&#xff0c;绘制出预期输出的图片。平台会对你编写的代码进行测试。 1.本关任务 熟悉编程环境&#xff1b; 了解光栅图形显示器的特点&#xff1b; 了解计算机绘图的特点&#xff1b…

Redis是单线程吗?为什么6.0之后引入了多线程?

Redis是单线程吗&#xff1f;为什么6.0之后引入了多线程&#xff1f; Redis 是单线程吗&#xff1f;Redis 单线程模式是怎样的&#xff1f;Redis 采用单线程为什么还这么快&#xff1f;Redis 6.0 之前为什么使用单线程&#xff1f;Redis 6.0 之后为什么引入了多线程&#xff1f…

geotrust dv通配符证书800

Geotrust是成立时间较久的正规CA认证机构&#xff0c;在过去的几十年间颁发了无数的SSL证书&#xff0c;这些SSL证书被各个开发者使用&#xff0c;受到大多数浏览器的信任。而Geotrust旗下的DV通配符证书因其广泛的应用范围受到了用户的青睐。今天就随SSL盾小编了解Geotrust旗下…

C语言(指针)2

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸各位能阅读我的文章&#xff0c;诚请评论指点&#xff0c;关注收藏&#xff0c;欢迎欢迎~~ &#x1f4a5;个人主页&#xff1a;小羊在奋斗 &#x1f4a5;所属专栏&#xff1a;C语言 本系列文章为个人学习笔记&#x…

XWiki 服务没有正确部署在tomcat中,如何尝试手动重新部署?

1. 停止 Tomcat 服务 首先&#xff0c;您需要停止正在运行的 Tomcat 服务器&#xff0c;以确保在操作文件时不会发生冲突或数据损坏&#xff1a; sudo systemctl stop tomcat2. 清空 webapps 下的 xwiki 目录和 work 目录中相关的缓存 删除 webapps 下的 xwiki 目录和 work …

游戏行业被攻击的原因、攻击种类及合适的服务器

很多游戏刚上线没多久就频繁遭到同行恶意攻击。在相关数据报告中&#xff0c;2023年上半年遭受DDoS攻击的行业中&#xff0c;游戏行业占到40%&#xff0c;而且攻击方式、攻击频率、攻击峰值呈明显上升趋势。很多充满创意的游戏开发公司刚才开发上线一个很有特色的产品&#xff…

Electron学习笔记(三)

文章目录 相关笔记笔记说明 五、界面1、获取 webContents 实例&#xff08;1&#xff09;通过窗口对象的 webContent 属性获取 webContent 实例&#xff1a;&#xff08;2&#xff09;获取当前激活窗口的 webContents 实例&#xff1a;&#xff08;3&#xff09;在渲染进程中获…

IDEA 好用的插件

图标插件&#xff1a;Atom Material Icons 此插件的作用就是更好的显示各种文件的类别&#xff0c;使之一目了然 汉化包 Chinese ​(Simplified)​ Language Pack / 中文语言包 作用就是 汉化 AI编码助手 GitHub Copilot AI编码助手&#xff1a;提示代码很好用 缺点&#xff1a…

H5 云商城 file.php 文件上传致RCE漏洞复现

0x01 产品简介 H5 云商城是一个基于 H5 技术的电子商务平台,旨在为用户提供方便快捷的在线购物体验。多平台适配:H5 云商城采用 H5 技术开发,具有良好的跨平台适配性。无论是在电脑、手机还是平板等设备上,用户都可以通过网页浏览器访问和使用云商城,无需安装额外的应用程…

CH340 RTS DTR引脚编程驱动OLED

运行结果 硬件连接&#xff08;在连接线上串接300R电阻&#xff09; 下面是c#实现代码 using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks;using uint8 System.Byt…

2024年数维杯B题完整代码和思路论文讲解与分析

2024数维杯数学建模完整代码和成品论文已更新&#xff0c;获取↓↓↓↓↓ https://www.yuque.com/u42168770/qv6z0d/bgic2nbxs2h41pvt?singleDoc# 2024数维杯数学建模B题45页论文和代码已完成&#xff0c;代码为全部问题的代码 论文包括摘要、问题重述、问题分析、模型假设、…

QT--2

Qt界面设计 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent) {//窗口相关设置this->resize(680,520);this->setFixedSize(680,520);this->setWindowTitle("Tim");this->setWindowFla…

【零基础】system generator①设置卡解析

1.在matlab中我们输入的是双精度浮点型数据&#xff0c;经过gateway后变成定点型。十六位十四个小数位&#xff0c;整个数据有十六位&#xff0c;其中十四位给了小数 2.fixed-point定点型&#xff1b;signed有符号&#xff1b;2’s comp补码 3.量化误差 truncate&#xff0c;舍…

Windows Server 2012 R2 新增D盘分区

我们经常搭建windows版本的游戏时会要在D盘上操作&#xff0c;今天就介绍下新的服务器如何新增一个D盘。 在"开始"图标右边有个”服务器管理器“&#xff0c;单击点开 点开服务器管理器后&#xff0c;点击“工具”打开“计算机管理” 打开计算机管理后点击“存储”-…

【c++】string深度刨析以及实现

#pragma once #include<iostream> using namespace std; #include<assert.h> namespace bite {class string{public://迭代器 //像指针 底层不一定是指针 typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str _size;}//const 版本…

RERCS系统-WDA+BOPF框架实战例子 PART 2-新建Root的子节点Node Element

1、通过事务码 BOBF进入Business Object Browser&#xff08;业务对象浏览&#xff09;页面&#xff1b; 2、输入debug 进入编辑模式&#xff1b; 3、双击对应的业务对象进入Business Object Detail Browser即业务对象数据浏览器 在Node Structure的Root中新建需要的SubNode子…

OpenCV使用 Kinect 和其他兼容 OpenNI 的深度传感器(75)

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇:使用 OpenCV 创建视频(74) 下一篇 :OpenCV使用 Orbbec Astra 3D 相机(76) 目的&#xff1a;​ 通过 VideoCapture 类支持与 OpenNI 兼容的深度传感器&#xff08;Kinect、XtionPRO 等&#xff09;。…

力扣HOT100 - 215. 数组中第K个最大元素

解题思路&#xff1a; 快速选择&#xff0c;目标是找出数组中第 k 小&#xff08;或第 k 大&#xff09;的元素&#xff0c;而不是对整个数组进行排序。 &#xff08;需要和快排进行区分&#xff0c;快排的目的是排序&#xff09; 注意&#xff1a; i l - 1, j r 1; 为什…

leetcode刷题指南

本文我将分享给大家一套我自己使用良久并觉得非常高效的 学习论&#xff0c;它可以运用到 Leetcode 上的刷题&#xff0c;也可以 generalize 到生活中涉及到学习以及记忆的方方面面。当然&#xff0c;本文将以 Leetcode 刷题为 case study 去进行讲解。 更具体一点, 我会教大家…

鸿蒙OpenHarmony开发板解析:【系统能力配置规则】

如何按需配置部件的系统能力 SysCap&#xff08;SystemCapability&#xff0c;系统能力&#xff09;是部件向开发者提供的接口的集合。 开发前请熟悉鸿蒙开发指导文档&#xff1a;gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。 部件配置系统…