初识多线程

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.hqwc.cn/news/689003.html

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

相关文章

Centos 中如何汉化man命令

刚学Linux&#xff0c;记不住命令和选项&#xff0c;很依赖里面的 man 查看命令&#xff0c;但因为着实看不懂&#xff0c;有没有什么办法把man查看命令的信息改成中文 在CentOS 7中&#xff0c;你可以通过安装man-pages-zh包来获取中文的man手册。以下是具体的步骤&#xff1a…

学生党性价比蓝牙耳机哪款好用?五款性价比机型盘点分享

在众多的蓝牙耳机里&#xff0c;对于许多预算不到的学生党来说&#xff0c;想要在有限的预算内挑选到一款性价比高、性能出色的蓝牙耳机&#xff0c;确实是一个不小的挑战&#xff0c;作为蓝牙耳机大户的我今天就来为大家盘点五款性价比极高的蓝牙耳机&#xff0c;帮助大家在有…

Sqlite在Mybatis Plus中关于时间字段的处理

我的个人项目中&#xff0c;使用Mybatis-Plus 和 Sqlite数据库&#xff0c; 但是在存储和查询时间字段的时候&#xff0c;总是出现问题&#xff0c;记录下我解决问题的过程。 Sqlite会默认把时间字段转成时间戳存储到数据库的字段中&#xff0c;看起来不直观&#xff0c;所以我…

永倍达 最新消息!发放消费券! 重新开网?

大家好 我是一家软件开发公司的产品经理 吴军 我又又又又又叕来蹭热度了&#xff0c;最近永倍达有新动作&#xff01;发放消费券&#xff1f; 店长群最新通知 4.15号开始发放消费券 一个月之后才可以重新提现 今天是5.10号离5.18也不远了 大家拭目以待看看到底能不能提现&a…

品鉴中的个人风格:如何形成自己与众不同的红酒品鉴体验

品鉴云仓酒庄雷盛红酒不仅是一种感官体验&#xff0c;更是一种个人风格的展现。每个人都有自己与众不同的品味和偏好&#xff0c;通过品鉴红酒&#xff0c;我们可以形成自己与众不同的红酒品鉴体验。 要形成自己与众不同的红酒品鉴体验&#xff0c;首先需要勇于尝试不同类型的红…

记一次favicon.ico的折腾

某项目需要将前端和后台整合在一起 我也不知道为啥要整合 上面有要求就整呗 正常前端npm run build打包后 dist内会根据设置自动生成favicon.ico文件在根目录下 但由于前后端整合 需要打包后将图标放在dist下的static文件夹里 需要的效果 打包后 index.html里 <link rel&…

LVS的三种工作模式---(DR/TUN/NAT)

目录 一、NAT模式&#xff08;LVS-NAT&#xff09; 二、IP隧道模式&#xff08;LVS-TUN&#xff09; 三、DR模型--直接路由模式&#xff08;LVS-DR&#xff09; LVS/DR模式ARP抑制 原因&#xff1a; LVS的DR工作模式及配置&#xff1a; LVS的NAT工作模式及配置&#xff1…

基于springboot实现贸易行业crm系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现贸易行业crm系统演示 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了基于springboot的贸易行业crm系统的开发全过程。通过分析基于springboot的贸易行业crm系统管理的不足&#xff0c;创建…

Redis如何避免数据丢失?——RDB

目录 1. RDB机制的配置 2. fork()函数和写时复制(Copy On Write&#xff09; 什么是Copy On Write 系统fork中使用Copy On Write机制 3. RDB文件结构 RDB文件内容和内容顺序 InfoAuxFields是rdb信息数据 数据库数据 数据 数据存储格式 字符串编码 操作码 4. RDB的2…

物联网SCI期刊,潜力新刊,审稿速度快,收稿范围广泛!

一、期刊名称 Internet of Things 二、期刊简介概况 期刊类型&#xff1a;SCI 学科领域&#xff1a;物联网 影响因子&#xff1a;5.9 中科院分区&#xff1a;3区 出版方式&#xff1a;订阅模式/开放出版 版面费&#xff1a;选择开放出版需支付$2310 三、期刊征稿范围 I…

WorkPlus im(即时通讯)集成平台助力政企数字化转型升级

随着互联网技术的不断发展&#xff0c;企业内部通讯软件已经成为企业日常运营中不可或缺的一部分。企业IM&#xff08;即时通讯&#xff09;和移动门户作为企业内部通讯软件的关键组成部分&#xff0c;为企业提供更加高效、便捷的通讯方式&#xff0c;提高了企业的运营效率。 针…

设计软件有哪些?渲染软件篇(4),渲染100邀请码1a12

除了之前介绍的一些渲染软件&#xff0c;这次我们继续介绍。 1、渲染100(http://www.xuanran100.com/?ycode1a12) 渲染100是网渲平台&#xff0c;为设计师提供高性能的渲染服务。通过它设计师可以把本地渲染移到云端进行&#xff0c;速度快价格便宜&#xff0c;支持3dmax、v…