初识多线程

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 {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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过于闲置。

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

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

相关文章

金仓面对面 | 人大金仓×安硕信息共话金融信用风险管理数字化转型之道

金仓面对面 在数字化浪潮的推动下&#xff0c;人大金仓携手行业先锋&#xff0c;共同开启一场关于创新与转型的思想盛宴——金仓面对面。这不仅是一场对话&#xff0c;更是一次智慧的火花碰撞&#xff0c;一次行业数字化转型洞察的深度挖掘。 行业精英汇聚&#xff1a;我们荣幸…

云南区块链商户平台:抓包技术自制开票工具(二)

前言 上节我们分析了云南区块链商户平台的登录接口以及数据加密、解密&#xff0c;本节我们将构建一个项目框架&#xff0c;将大致的雏形制作出来 说明 由于我们使用开票软件都是在 云南区块链商户平台上操作&#xff0c;如果再开发电脑端就显得没必要&#xff0c;思考良久&…

在线旅游网站,基于 SpringBoot+Vue+MySQL 开发的前后端分离的在线旅游网站设计实现

目录 一. 前言 二. 功能模块 2.1. 登录界面 2.2. 管理员功能模块 2.3. 用户功能模块 三. 部分代码实现 四. 源码下载 一. 前言 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff0c;旅游网站当然…

分布式与集群的区别

先说区别&#xff1a; 分布式是并联工作的&#xff0c;集群是串联工作的。 分布式中的每一个节点都可以做集群。而集群并不一定就是分布式的。 集群举例&#xff1a;比如新浪网&#xff0c;访问的人很多&#xff0c;他可以做一个集群&#xff0c;前面放一个相应的服务器&…

Covalent Network(CQT)通过 “新曙光” 计划实现重要里程碑,增强以太坊时光机,提供 30% 的年化质押收益率

Covalent Network&#xff08;CQT&#xff09;作为集成超过 280 条区块链&#xff0c;并服务于超过 2.8 亿个钱包的领先结构化数据基础设施层&#xff0c;宣布了其战略计划 “新曙光” 中的一个重要进展。随着网络升级并完成了准备工作的 75%&#xff0c;这将为即将部署的以太坊…

JUC下CountDownLatch详解

详细介绍 CountDownLatch是Java并发包java.util.concurrent中提供的一个同步工具类&#xff0c;它允许一个或多个线程等待其他线程完成操作后再继续执行。这个工具类基于一个计数器&#xff0c;计数器的初始值可以由构造函数设定。线程调用countDown()方法会将计数器减1&#x…

uniapp——弹出键盘遮挡住输入框 textarea,处理方法

案例 在写输入框的时候会遇见 键盘遮挡住部分textarea框的一部分&#xff0c;使用cursor-spacing处理即可 修改后&#xff1a; 其他问题&#xff1a; 调起键盘输入时&#xff0c;不希望上方的内容被顶上去 代码 <view class"commentBox" :style"botto…

爬虫学习(4)每日一笑

代码 import requests import re import osif __name__ "__main__":if not os.path.exists("./haha"):os.makedirs(./haha)url https://mlol.qt.qq.com/go/mlol_news/varcache_article?docid6321992422382570537&gameid3&zoneplat&webview…

Linux 认识与学习Bash——3

在Linux bash中&#xff0c;数据流重定向是指将命令的输出从默认的标准输出&#xff08;通常是终端&#xff09;重定向到其他位置&#xff0c;如文件或另一个命令的输入。这是通过使用特定的符号来实现的。例如&#xff0c;>用于将输出重定向到文件&#xff0c;而<用于将…

Proxmox VE 8 用SDN隔离用户网络

作者&#xff1a;田逸&#xff08;formyz&#xff09; 最新发布的Proxmox VE&#xff08;以下简称PVE&#xff09; 8在Web管理后台集成了易于操作的SDN&#xff08;软件定义网络&#xff09;功能插件&#xff0c;其实质是对不同的PVE用户指定不同的网络&#xff0c;进行逻辑隔离…

[移动通讯]【无线感知-P1】[从菲涅尔区模型到CSI模型-3][Mobius transformations-7】【Inversion】

前言&#xff1a; mobius map 里面比较难的是inversion &#xff0c;林菲尔德学院&#xff08;Linfield College&#xff09; Michael P. Hitchman. 有本书详细介绍一下该方向的一些原理&#xff0c;例子. Whitman College Book: 《Geometry with an Introduction to Cosmic T…

el-select选项框内容过长

利用popper-class实现选项框内容过长&#xff0c;截取显示功能&#xff1a; <el-select popper-class"popper-class" :popper-append-to-body"false" v-model"value" placeholder"请选择"><el-optionv-for"item in opt…

初识 Linux线程

再学习完Linux进程后,本期,我们来讲解Linux线程 1.为什么需要线程 在之前学习进程前,我们写的所有代码几乎都是单个执行流的,也就是说我们的代码只有一条路走. 在学习进程后,我们可以通过fork进行进程创建,给进程分配任务进行多执行流执行任务,问题来了 那我们为什么还需要…

清空回收站是彻底删除吗?一文解答你的疑问!

“我刚刚本来想在回收站中恢复一个文件的&#xff0c;但是一不小心就清空了回收站&#xff0c;想问问清空回收站是彻底删除吗&#xff1f;清空了回收站文件还有机会找回吗&#xff1f;” 在使用电脑的过程中&#xff0c;我们经常会将不再需要的文件或文件夹移动到回收站&#x…

数据结构与算法学习笔记六-二叉树的顺序存储表示法和实现(C语言)

目录 前言 1.数组和结构体相关的一些知识 1.数组 2.结构体数组 3.递归遍历数组 2.二叉树的顺序存储表示法和实现 1.定义 2.初始化 3.先序遍历二叉树 4.中序遍历二叉树 5.后序遍历二叉树 6.完整代码 前言 二叉树的非递归的表示和实现。 1.数组和结构体相关的一些知…

AUTOSAR OS调度表讲解

调度表 AUTOSAR OS通过调度表(Schedule Table)来解决一个alarm只能激活一个任务的限制。调度表是预定义的行为序列,通过到期点实现。AUTOSAR OS遍历调度表并依次处理每个到期点,遍历由底层的counter来实现驱动。 到期点发生在从概念零开始的静态配置偏移量上。偏移量在静…

【程序设计和c语言-谭浩强配套】(适合专升本、考研)

一晃大半年没更新了&#xff0c;这一年一直在备考&#xff0c;想着这几天把前段时间学的c语言给大家分享一下&#xff0c;在此做了一个专栏&#xff0c;有需要的小伙伴可私信获取o。 简介&#xff1a;本专栏所有内容皆适合专升本、考研的复习资料&#xff0c;本人手上也有日常…

【2024亚马逊云科技峰会】Amazon Bedrock + Llama3 生成式AI实践

在 4 月 18 日&#xff0c;Meta在官网上公布了旗下最新大模型Llama 3。目前&#xff0c;Llama 3已经开放了80亿&#xff08;8B&#xff09;和700亿&#xff08;70B&#xff09;两个小参数版本&#xff0c;上下文窗口为8k&#xff0c;据称&#xff0c;通过使用更高质量的训练数据…

【基础算法总结】二分查找一

二分查找一 1. 二分查找2.在排序数组中查找元素的第一个和最后一个位置3.x 的平方根4.搜索插入位置 点赞&#x1f44d;&#x1f44d;收藏&#x1f31f;&#x1f31f;关注&#x1f496;&#x1f496; 你的支持是对我最大的鼓励&#xff0c;我们一起努力吧!&#x1f603;&#x1…

制造业数字化转型解决方案及应用(125页PPT)

一、资料介绍 《制造业数字化转型解决方案及应用》是一份内容丰富、深入剖析制造业数字化转型的125页PPT资料。这份资料以“智能制造、制造业数字化转型、制造业数字化转型案例”为关键词&#xff0c;全面展现了制造业数字化转型的核心理念、解决方案以及实际应用案例。 关注…