计算机是如何工作的,Java多线程编程

计算机是如何工作的,Java多线程编程

一、冯诺依曼体系

现代的计算机,大多遵守 冯诺依曼体系结构 (Von Neumann Architecture)

在这里插入图片描述
CPU 中央处理器: 进行算术运算和逻辑判断.

在这里插入图片描述

AMD Ryzen 7 580OU with Radeon Graphics

CPU最重要的指标,就是叫做 GHz “主频”,3.2Ghz,描述了CPU运算的速度.
其实本质是3.2G个时钟周期.
可以近似的视为,每秒钟能执行32亿条指令.

这个数字越大,CPU 就算的越快,表示 1s 执行 32 亿条指令

存储器: 分为外存和内存, 用于存储数据(使用二进制方式存储)
输入设备: 用户给计算机发号施令的设备. (键盘,鼠标,麦克风,摄像头…)
输出设备:计算机个用户汇报结果的设备 (显示器,音箱…)

有的设备,既可以输入,也可以输出.触摸屏. 网卡

显卡(GPU)
显卡的定位和CPU类似.
CPU通用计算芯片——好比大学生,可以让他算1+1,也可以算微积分。GPU专用计算芯片——好比小学生,只会算1+1
很多图形相关运算(游戏,视频剪辑),不需要计算微积分,就只需要算1+1.但是算的量特别大.拿CPU算,也可以,但是大材小用了.
就专门搞了个GPU,专门负责算这些1+1的.(这里面包含很多很多的小学生)

针对存储空间

  • 硬盘 > 内存 >> CPU

针对数据访问速度

  • CPU >> 内存 > 硬盘

认识计算机的祖师爷 – 冯诺依曼

冯·诺依曼(John von Neumann,1903年12月28日-1957年2月8日), 美籍匈牙利数学家、计算机科学家、物理学家,是20世纪最重要的数学家之一。冯·诺依曼是布达佩斯大学数学博士,在现代计算机、博弈论、核武器和生化武器等领域内的科学全才之一,被后人称为 “现代计算机之父”, “博弈论之父”.


二、CPU 基本工作流程

1、逻辑门

1.1、电磁继电器

电子开关 —— 机械继电器 (Mechanical Relay):
电磁继电器:通过通电 / 不通电来切换开关状态,得到 1 或者 0 这样的数据

在这里插入图片描述


1.2、门电路 (Gate Circuit)

基于上述的 “电子开关” 就能构造出基本的门电路,可以实现 1 位(bit) 的基本逻辑运算
最基础的门电路,有三种:(借助于二极管等设备特殊物理特性)
非门:可以对一个 0/1 进行取反. 0-> 1
与门:可以针对两个 0/1 进行与运算. 1 0 -> 0
或门:可以针对两个 0/1 进行或运算. 1 0 -> 1
针对二进制数据来进行的.不是"逻辑与”,此处是按位与

借助上述的基础门电路,能构造出一个更复杂的门电路:异或门
相同为0,相异为1。1 0 -> 1


1.3、运算器

基于上述的门电路,还可以搭建出一些运算器

半加器:是针对两个比特位,进行加法运算

在这里插入图片描述
全加器:是针对三个比特位,进行加法运算

在这里插入图片描述


1.4、加法器

基于上述的半加器和全加器,就可以构造出一个针对多个 bit 位的数据进行加法运算的加法器了

在这里插入图片描述
A和B是两个8 bit的数字
A0这个数字的第0位(最右)A1
A2
A3

电子开关=>基础的门电路=>异或门电路=>半加器=>全加器=>8位加法器

有了加法器之后,就可以计算不只是加法,还能计算减法、乘法、除法都是通过这个加法器来进行


2、寄存器,控制单元(CU)

CPU里面除了运算器之外,还有控制单元和寄存器(Register)

门电路 (电子开关)
CPU芯片来说,上面就集成了非常非常多的这些电子开关,一个CPU上面的电子开关越多,就认为是计算能力就越强

CPU里面除了运算器之外,还有控制单元寄存器

  • 寄存器是CPU内部用来存储数据的组件
    访问速度:寄存器是内存的3-4个数量级
    存储空间:比内存小很多很多,现在的x64的cpu (64位的cpu),大概有几十个寄存器,每个寄存器是8个字节,几百个字节,
    成本:CPU上面的这个寄存器,还是非常贵
    持久化:掉电之后数据丢失

  • 控制单元 CU(Control Unit)
    协调CPU来去进行工作
    控制单元最主要的工作,能够去执行指令
    后面进行详细的论述


3、指令(Instruction)

指令和编程密切相关。

编程语言,大概分成三类:

  1. 机器语言
    通过二进制的数字,来表示的不同的操作

厂商在设计CPU的时候就会设计好"指令集"CPU都支持哪些指令,以及这些指令要怎么工作.(这就类似于CPU提供的API)

不同的CPU (哪怕是同一个厂商,但是不同型号的CPU),所支持的机器语言(指令)也可能存在差别

  1. 汇编语言

通过简单的单词作为"助记符"代替二进制的指令.

一个CPU到底支持哪些指令,生产厂商,会提供一个**"芯片手册”** 详细介绍CPU都支持哪些指令,每个指令都是干啥的
汇编语言和机器语言是一对一的关系 (完全等价)

不同的CPU有不同的体系架构.支持的指令集也是不一样的。即使是同类架构,不同的版本,指令集也是不一样的.不同的CPU上面跑的汇编也不一样
学校学的的大部分的汇编语言都是针对一款上古神 U,Intel 8086 CPU 16位的

  1. 高级语言

C, C++, C#(c sharp。C++相比于C做出改进,多俩加号C#,相对于C++做出改进,又多俩加号.)

Java, Python,PHP

Ruby,JS,Go

指令是如何执行的?
自己构造出一个最简单的芯片手册:

在这里插入图片描述

假设CPU上有两个寄存器
A 00
B 01

0010 1010
这个操作的意思,就是把 1010 内存地址上的数据给读取到A寄存器中
0001 1111
这个操作的意思,就是把 1111 内存地址上的数据读到寄存器 B
0100 1000
这个操作的意思,就是把 A 寄存器的值,存到 1000 这个内存地址中
1000 0100
这个操作的意思,就是把 00 寄存器和01寄存器的数值进行相加,结果放到 00 寄存器里

在这里插入图片描述

CPU的工作流程:(通过CU控制单元来实现的)

  1. 从内存中读取指令
  2. 解析指令
  3. 执行指令

咱们编写的程序,最终都会被编译器给翻译成 CPU 所能识别的机器语言指令,在运行程序的时候,操作系统把这样的可执行程序加载到内存中,cpu 就一条一条指令的去进行读取,解析,和执行,如果再搭配上条件跳转,此时,就能实现条件语句和循环语句


三、操作系统(Operating System)

1、操作系统的定位

操作系统是一组做计算机资源管理的软件的统称。目前常见的操作系统有:Windows系列、Unix系列、
Linux系列、OSX系列、Android系列、iOS系列、鸿蒙等

操作系统是一个搞 “管理的软件”,操作系统是软件硬件用户之间交互的媒介

  1. 对下,要管理好各种硬件设备
  2. 对上,要给各种软件提供稳定的运行环境

在这里插入图片描述


2、进程/任务(Process/Task)

exe 可执行文件,都是静静的躺在硬盘上的,在你双击之前,这些文件不会对你的系统有任何影响
但是,一旦你双击执行这些 exe 文件,操作系统就会把这个 exe 给加载到内存中,并且让 CPU 开始执行exe内部的一些指令 (exe里面就存了很多这个程序对应的二进制指令)
这个时候,就已经把 exe给执行起来,开始进行了一些具体的工作

这些运行起来的可执行文件,称为 “进程” (没跑起来的,叫做"程序"),进程 (process) 还有另一个名字任务 (task)

这些都是机器上运行的进程:Ctrl+Shift+Esc

在这里插入图片描述

——线程:
线程是进程内部的一个部分进程包含线程,如果把进程想象成是一个工厂,那么线程就是工厂里的生产线,一个工厂里面可以有一个生产线或者也可以有多个生产线
咱们写的代码,最终的目的都是要跑起来,最终都是要成为一些进程
对于 java 代码来说,最终都是通过 java 进程来跑起来的 (此处的这个 java 进程就是咱们平时常说的jvm)


3、操作系统是如何管理进程的?

进程是一个重要的 “软件资源”,是由操作系统内核负责管理(描述+组织) 的

  1. 先描述一个进程 ——明确出一个进程上面的一些相关属性
  2. 再组织若干个进程 ——使用一些数据结构,把很多描述进程的信息给放到一起,方便进行增删改查

描述进程:操作系统里面主要都是通过 C/C++来实现的,此处的描述其实就是用的C语言中的 “结构体” (也就和Java的类差不多),
操作系统中描述进程的这个结构体, "PCB" (process control block)进程控制块,这个东西不是硬件中的那个PCB板

(PCB 是一个数据结构,体现的是进程/线程是如何实现,如何被描述出来的)

组织进程:典型的实现,就是使用双向链表来把每个进程的PCB给串起来 (并不是一个单纯的双向链表,说PCB是使用链表来组织的,并不具体,实际的情况并不是一个简单的链表,其实这是一系列以链表为核心的数据结构,如就绪队列,阻塞队列…)

创建一个进程,本质上就是创建一个PCB这样的结构体对象,把它插入到链表中。销毁一个进程,本质上就是把链表上的PCB节点删除掉.
任务管理器查看到进程列表,本质上就是遍历这个PCB链表

操作系统的种类是很多的,内部的实现也是各有不同,咱们此处所讨论的情况,是以Linux这个系统为例,由于windows, mac 这样的系统,不是开源的,里面的情况我们并不知道


4、PCB中的一些属性:

1、pid (进程id)
进程的身份标识,进程的身份证号(唯一的数字)

在这里插入图片描述

2、内存指针
指明了这个进程要执行的代码 / 指令在内存的哪里,以及这个进程执行中依赖的数据都在哪里
当运行一个exe,此时操作系统就会把这个 exe 加载到内存中,变成进程
进程要执行的二进制指令 (通过编译器生成的), 除了指令之外还有一些重要的数据

并不是内存条上真实的地址,虚拟地址

3、文件描述符表:
程序运行过程中,经常要和文件打交道 (文件是在硬盘上的)
文件操作:打开文件,读/写文件,关闭文件
进程每次打开一个文件,就会在文件描述符表上多增加一项,(一个文件描述符表就可以视为是一个数组,里面的每个元素,又是一个结构体,就对应一个文件的相关信息)

一个进程只要一启动,不管你代码中是否写了打开 / 操作文件的代码,都会默认的打开三个文件 (系统自动打开的),标准输入(System.in),准输出(System.out) 标准错误(System.err)
要想能让一个进程正常工作,就需要给这个进程分配一些系统资源:内存,硬盘,CPU

这个文件描述符表的下标,就称为文件描述符

内存指针 和 文件描述附表 描述了进程持有了哪些硬件资源

硬件资源, 内存,硬盘,网卡都好分
CPU资源不好分!!!
进程有上百个.
CPU有几个呢?1个??其实不是.咱们现在的CPU都是多核CPU

在这里插入图片描述

这些进程,是希望能够"同时运行", “分时复用”

并行和并发:

  • 并行:微观上,两个CPU核心,同时执行两个任务的代码
  • 并发:微观上, 一个CPU核心,先执行一会任务1, 再执行一会任务,再执行一会任务…再执行一会任务
    只要切换的足够快, 宏观上看起来, 就好像这么多任务在同时执行一样

并行和并发这两件事, 只是在微观上有区分
宏观上咱们区分不了,微观上这里的区分都是操作系统自行调度的结果

例如6个核心,同时跑20个任务
这20个任务, 有些是并行的关系, 有些是并发的关系。可能任务A和任务B,一会是并行, 一会是并发….都是微观上操作系统在控制的,在宏观上感知不到
正因为在宏观上区分不了并行并发, 我们在写代码的时候也就不去具体区分这两个词实际上通常使用 “并发” 这个词, 来代指并行+并发
咱们只是在研究操作系统进程调度这个话题上的时候, 稍作区分但是其他场景上基本都是使用并发作为一个统称来代替的,并发编程


5、CPU 分配 —— 进程调度(Process Scheduling)

调度
所谓的调度就是 “时间管理”
并发就是规划时间表的过程,也就是“调度"的过程

4、进程调度相关的状态:

上面的属性是一些基础的属性,下面的一组属性,主要是为了能够实现进程调度
进程调度:是理解进程管理的重要话题,现在的操作系统,一般都是 “多任务操作系统”(前身就是 “单任务操作系统”,同一时间只能运行一个进程),一个系统同一时间,执行了很多的任务

4.1、状态
状态就描述了当前这个进程接下来应该怎么调度

  • 就绪状态:进程随时准备好了可以去 CPU 上执行
  • 运行状态
  • 阻塞状态 / 睡眠状态:暂时不可以去CPU上执行,比如进程在进行密集的IO操作,读写数据.

Linux中的进程状态还有很多其他的…

4.2、优先级
先给谁分配时间,后给谁分配时间,以及给谁分的多,给谁分的少
进程也是有优先级的。操作系统进行调度并不是一碗水端平

4.3、上下文
就表示了上次进程被调度出 CPU 的时候,当时程序的执行状态。下次进程上CPU的时候,就可以恢复之前的状态,然后继续往下执行

进程被调度出CPU之前,要先把CPU中的所有的寄存器中的数据都给保存到内存中 (PCB的上下文字段中) ,相当于存档了
下次进程再被调度上CPU的时候,就可以从刚才的内存中恢复这些数据到寄存器中,相当于读档

存档+读档,存档存储的游戏信息,就称为 “上下文”

进程的上下文,就是CPU中的各个寄存器(CPU内置的存储数据的模块,保存的就是程序运行过程中的中间结果) 的值

保存上下文,就是把这些CPU寄存器的值,记录保存到内存(pcb)中
回复上下文,就是把内存中的这些寄存器值恢复回去

4.4、记账信息
操作系统,统计了每个进程在 cpu 上,都分别被执行了多久,分别都执行了哪些指令,分别都排队等了多久了…
给进程调度提供指导依据的


6、内存分配 —— 内存管理(Memory Manage)

进程的调度,其实就是操作系统在考虑CPU资源如何给各个进程分配
那内存资源又是如何分配的呢?

虚拟地址空间: 程序中所获取到的内存地址,并非是真实的物理内存的地址,而是经过了一层抽象,虚拟出来的地址

目的:解决进程间相互影响的问题

内存(物理上是个内存条)可以存很多数据.内存就可以想象成是一个大走廊
走廊非常长,有很多房间.每个房间大小1 Byte每个房间还有个编号,从О开始依次累加
这个内存编号,就是"地址"
这个地址也就认为"物理地址"

内存有个了不起的特性,随机访问 (闪现)
访问内存上的任意地址的数据,速度都极快,时间上都差不多.
正是这个特点,造就了数组取下标操作是O(1)

——为什么访问的不是真实的物理内存地址:

早期的操作系统,里面的进程都是访问同一个内存的地址空间。如果某个进程出现 bug,把某个内存的数据给写错了,就可能引起其他进程的崩溃

在这里插入图片描述

由于操作系统上,同时运行着很多个进程,如果某个进程,出现了bug 进程崩溃了,是否会影响到其他进程呢?
现代的操作系统 (windows, linux, mac… ) ,能够做到这一点,就是 “进程的独立性” 来保证的,就依仗了 “虚拟地址空间”

针对进程使用的内存空间,进行"隔离"引入了虚拟地址空间!!!
代码里不再直接使用真实的物理地址了,而是使用虚拟的地址,由操作系统和专门的硬件设备负责进行虚拟地址到物理地址的转换

在这里插入图片描述

CE 是一个类似于"黑客工具"
功能大概就是改另一个进程里的内存数据
这个东西是属于操作系统,给程序猿留了个后门.
直接通过C中的指针操作,无法针对另一个进程的内存进行修改的!!!但是操作系统给我们提供了一些特殊的系统调用
通过这些系统调用,就可以手动的操作另一个进程中的内存数据了

例:如果某个居民核酸变成阳性了,是否会影响到其他的居民呢?
一旦发现有人阳性了,就需要立刻封楼封小区,否则就会导致其他人也被传染,这个情况就类似于早期的操作系统,

解决方案,就是把这个院子,给划分出很多的道路
这些道路之间彼此隔离开,每个人走各自的道理,这个时候就没事了,此时即使有人确诊,也影响不到别人了,

如果把进程按照虚拟地址空间的方式给划分出了很多份,这个时候不是每一份就只剩一点了嘛?? 虽然你的系统有百八十个进程,但是实际上从微观上看,同时执行的进程,就6个!!
每个进程能够捞着的内存还是挺多的,而且另一方面,也不是所有的进程都用那么多的内存,有的进程 (一个3A游戏,吃几个G),大多数的进程也就只占几M即可


7、进程间通信(Inter Process Communication)

进程间通信:

进程之间现在通过虚拟地址空间,已经各自隔离开了,但是在实际工作中,进程之间有的时候还是需要相互交互的。

例:某业主A问:兄弟们,谁家有土豆,借我两个
业主B回答:我有土豆,我给你
设定一个公共空间,这个空间是任何居民都可以来访问的,
让B先把土豆放到公共空间中,进行消毒,再让A来把这个公共空间的土豆给取走,彼此就不容易发生传染

类似的,咱们的两个进程之间,也是隔离开的,也是不能直接交互的,操作系统也是提供了类似的 "公共空间”,
进程 A 就可以把数据见放到公共空间上,进程B再取走

操作系统中,提供的 “公共空间” 有很多种,并且各有特点,有的存储空间大,有的小,有的速度快,有的慢.….
操作系统中提供了多种这样的进程间通信机制,(有些机制是属于历史遗留的,已经不适合于现代的程序开发)

现在最主要使用的进程间通信方式两种:

  1. 文件操作

  2. 网络操作 (socket)


四、多线程

1、线程(Thread)

为啥要有进程?因为我们的系统支持多任务了,程序猿也就需要 “并发编程” (这是因为CPU进入了多核心的时代,要想进一步提高程序的执行速度,就需要充分地利用CPU的多核资源),不是说CPU核心多了,程序一下就跑的快了,还需要你的程序代码能够把这些CPU核心给利用上

并发编程,是更广义的概念
多线程,是实现并发编程的一种具体方式 (同时也是Java中提供的默认的方式)
除了这种方式外,还有很多其他方式(其他的并发编程模型)

通过多进程,是完全可以实现并发编程的,但是有点小问题:
如果需要频繁的创建而 / 销毁进程,这个事情成本是比较高的,如果需要频繁的调度进程,这个事情成本也是比较高的:
对于资源的申请和放,本身就是一个比较低效的操作,

创建进程就得分配资源:
1)内存
2)文件
销毁进程也得释放资源
1)内存
2)文件

如何解决这个问题?思路有两个:

  1. 进程池: (如数据库连接池,字符串常量池)
    进程池虽然能解决上述问题,提高效率。同时也有问题:池子里的闲置进程,不使用的时候也在消耗系统资源,消耗的系统资源太多了
  2. 使用线程来实现并发编程:
    线程比进程更轻量,每个进程可以执行一个任务,每个线程也能执行一个任务 (执行一段代码),也能够并发编程,
    创建线程的成本比创建进程要低很多。销毁线程,的成本也比销毁进程低很多。调度线程,的成本也比调度进程低很多。
    Linux 上也把线程称为轻量级进程 (LWP light weight process)

2、为什么线程比进程更轻量?

  1. 进程重量是重在哪里:重在资源申请释放 (在仓库里找东西…)
  2. 线程是包含在进程中的,一个进程中的多个线程,共用同一份资源
  • (主要指的是内存+文件描述符表。内存:你线程1 new的对象在线程2,3,4里都可以直接使用。文件描述符表:线程1打开的文件在线程2,3,4里都可以直接使用)
  • 只是创建进程的第一个线程的时候 (由于要分配资源)成本是相对高的,后续这个进程中再创建其他线程,这个时候成本都是要更低一些,所以为什么更轻量?少了申请释放资源的过程

可以把进程比作一个工厂,假设这个工厂有一些生产任务,例如要生产 1w 部手机
要想提高生产效率:
1). 搞两个工厂,一个生产 5k (多创建了一个进程)
2). 还是一个工厂,在一个工厂里多加一个生产线,两个生产线并行生产,一个生产线生产5k,(多创建了一个线程)
最终生产1w个手机,花的时间差不多,但是这里的成本就不一样了

多加一些线程,是不是效率就会进一步提高呢?一般来说是会,但是也不一定
如果线程多了,这些线程可能要竞争同一个资源,线程太多,核心数目有限,不少的开销反而浪费在线程调度上了,这个时候,整体的速度就收到了限制,整体硬件资源是有限的


3、总结进程与线程的区别

  1. 进程包含线程,一个进程里可以有一个线程,也可以有多个线程
  2. 进程和线程都是为了处理 并发编程 这样的场景
    但是进程有问题,频繁创建和释放的时候效率低,相比之下,线程更轻量,创建和释放效率更高。为啥更轻量?少了申请释放资源的过程
  3. 操作系统创建进程,要给进程分配资源,进程是操作系统分配资源的基本单位
    操作系统创建的线程,是要在 CPU上调度执行,线程是操作系统调度执行的基本单位,(前面的例子相当于是每个进程里,只有一个线程了,可以视为是在调度进程,但是如果进程里有多个线程,每个线程是独立在 CPU 上调度的,更严谨的说法,还是操作系统以线程为单位进行调度
    • 一个线程也是通过一个PCB来描述的,一个线程对应一个PCB,一个进程对应多个PCB
      如果一个进程只有一个线程,就是一个进程对一个PCB 了
    • 之前介绍的, PCB里的状态,上下文,优先级,记账信息,都是每个线程有自己的,各自记录各自的。但是同一个进程里的若干个 PCB 中, pid是一样的,内存指针和文件描述符表也是一样的.
  4. 进程具有独立性每个进程有各自的虚拟地址空间,一个进程挂了,不会影响到其他进程。
    同一个进程中的多个线程,共用同一个内存空间,一个线程挂了,可能影响到其他线程的,甚至导致整个进程崩溃

Java这个生态中更常使用的并发编程方式,是多线程
其他的语言,主打的并发变成又不一样:
go,主要是通过多协程的方式实现并发.
erlang,这个是通过 actor 模型实现并发.
JS,是通过定时器+事件回调的方式实现并发.……
多线程仍然是最主流最常见的一种并发编程的方式


五、Java 多线程编程

Java是个跨平台的语言,很多操作系统的提供的功能,都被JVM给封装好了,我们不需要学习系统原生API(C语言),只需要学习Java提供的API就行了.

Java 的线程 和 操作系统线程 的关系:

  1. 线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些 API 供用户使用 (例如 Linux 的 pthread 库),
  2. 在 Java 标准库 中,就提供了一个 Thread 类,来表示 / 操作线程,Thread 类可以视为 Java 标准库提供的 API, 对操作系统提供的 API 进行了进一步的抽象和封装
  3. 创建好的 Thread实例,其实和操作系统中的线程是一 一对应的关系,操作系统提供了一组关于线程的API(C语言风格),Java对于这组API进一步封装了,就成了Thread

1、第一个多线程程序

Thread 类的基本用法
通过 Thread 类创建线程,写法有很多种
其中最简单的做法,创建子类,继承自Thread,并且重写 run 方法

Thread类位于 java.lang包 中,所以不需要 import 别的包。类似还有 String, StringBuilder, StringBuffer

Thread t = new MyThread();
t.run(); // 虽然t是父类引用,此处调用的run仍然是子类的方法.(t本质上还是指向的子类的对象)

创建线程,是希望线程成为一个独立的执行流(执行一段代码)
如何指定? 有很多办法

package thread;

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

public class demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start(); // 线程中的特殊方法,启动一个线程
    }
}

run 方法描述了,这个线程内部要执行哪些代码每个线程都是并发执行的 (各自执行各自的代码),因此就需要告知这个线程,你执行的代码是什么
run 方法中的逻辑,是在新创建出来的线程中,被执行的代码

并不是我一定义这个类,一写run方法,线程就创建出来,相当于我把活安排出来了,但是还没开始干呢
需要调用这里的 start 方法,才是真正的在系统中创建了线程,才是真正开始执行上面的 run 操作,在调用 start 之前,系统中是没有创建出线程的

start 这里的工作,就是创建了一个新的线程 (就是调用操作系统的API,通过操作系统内核创建新线程的PCB,并且把要执行的指令交给这个PCB,当PCB被调度到CPU上执行的时候,也就执行到了线程run方法中的代码了.)
,新的线程负责执行t.run()

如果只是直接打印 hello world,你的 java 进程主要就是有一个线程.(调用main方法的线程)主线程
通过t.start(),主线程调用t.start,创建出一个新的线程,新的线程调用t.run

jconsole

可以使用 jconsole观察线程的名字,JDK 是 Java 开发者工具,jconsolejdk自带的一个调试工具
在这里插入图片描述
jconsole 这里能够罗列出你系统上的java进程(其他进程不行)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述


2、线程之间是并发执行的

如果在一个循环中不加任何限制,这个循环转的速度非常非常快,导致打印的东西太多了,根本看不过来了,就可以加上一个 sleep 操作,来强制让这个线程休眠一段时间
这个休眠操作,就是强制地让线程进入阻塞状态,单位是 ms,就是1s 之内这个线程不会到 cpu 上执行

public void run() {
   while (true) {
        System.out.println("hello thread!");
        Thread.sleep(1000);
    }
}

这是多线程编程中最常见的一个异常,线程被强制的中断了,用 try catch 处理

在这里插入图片描述

在一个进程中,至少会有一个线程,
在一个 java进程中,也是至少会有一个调用 main 方法的线程 (这个线程不是你手动搞出来的)
自己创建的 t 线程 和 自动创建的 main 线程,就是并发执行的关系 (宏观上看起来是同时执行)
此处的并发 = 并行 + 并发
宏观上是区分不了并行和并发的,都取决于系统内部的调度

package thread;

class MyThread2 extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello thread!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class demo2 {
    public static void main(String[] args) {
        Thread t = new MyThread2();
        t.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行打印:

/* hello main
hello thread!
hello thread!
hello main
hello main
hello thread!
hello thread!
hello main */

现在两个线程,都是打印一条,就休眠 1s
当1s 时间到了之后,系统先唤醒谁呢?
看起来这个顺序不是完全确定 (随机的)
每一轮,1s 时间到了之后,到底是先唤醒 main 还是 thread,这是不确定的 (随机的)
操作系统来说,内部对于线程之间的调度顺序,在宏观上可以认为是随机的 (抢占式执行)
这个随机性,会给多线程编程带来很多其他的麻烦


3、Thread 类创建线程的写法

这些方式是离不开 Thread 的,使用了不同的方式来描述, Thread 里的任务是啥

写法一:继承 Thread ,重写 run

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

public class demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
    }
}

写法二:实现 Runnable,重写 run

创建一个类,实现 Runnable 接口,再创建 Runnable 实例传给Thread 实例

通过 Runnable 来描述任务的内容
进—步的再把描述好的任务交给Thread 实例

将任务与线程分离开了

package thread;

// Runnable 作用,是描述一个“要执行的任务",run方法就是任务的执行细节
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("hello");
    }
}

public class demo3 {
    public static void main(String[] args) {
        // 这只是描述了个任务
		Runnable runnable = new MyRunnable();
        // 把任务交给线程来执行
		Thread t = new Thread(runnable);
        // 调用 start() 创建线程
        t.start();
    }
}

写法三 / 写法四: 就是上面两个写法的翻版,使用了匿名内部类

写法三:使用匿名内部类,继承 Thread

创建了一个匿名内部类,继承自 Thread 类,同时重写run方法,同时再new出这个匿名内部类的实例,并且让 t 引用指向该实例

package thread;

public class demo4 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("hello thread!");
            }
        };
        t.start();
    }
}

写法四:使用匿名内部类,实现 Runnable

new 的是 Runnable,针对这个创建的匿名内部类,同时 new 出的 Runnable 实例传给 Thread 的构造方法

这个写法和2 本质相同
只不过是把实现 Runnable 任务交给匿名内部类的语法
此处是创建了一个类, 实现 Runnable, 同时创建了类的实例, 并且传给 Thread 的构造方法

package thread;

public class demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello thread!");
            }
        });
        t.start();
    }
}

通常认为 Runnable 这种写法更好一点,能够做到让 线程 和 线程执行的任务,更好的进行解耦,与方法二相同
写代码一般希望,高内聚,低耦合。未来如果要改代码,不用多线程,使用多进程,或者线程池,或者协程…此时代码改动比较小.
Runnable 单纯的只是描述了一个任务,至于这个任务是要通过一个进程来执行,还是线程来执行,还是线程池来执行,还是协程来执行,Runnable 本身并不关心,Runnable 里面的代码也不关心

第五种写法:使用 Lambda 表达式

相当于是第四种写法的延伸,使用 lambda 表达式,是使用lambda 代替了 Runnable 而已

把任务用 lambda 表达式来描述,直接把 lambda 传给 Thread 构造方法

lambda 就是个匿名函数 (没有名字的函数) 用一次就拉到
java 里面函数没法脱离类存在
java为了能和别的语言对齐,搞了个蹩脚的函数式接口,通过这个来实现lambda

package thread;

public class demo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("hello thread!");
        });
        t.start();
    }
}

4、多线程的优势-增加运行速度

多线程能够提高任务完成的效率

程序,分成:
IO密集,涉及到读写文件,读写控制台,读写网络。CPU密集,包含了大量的加减乘除等算术运算:比如有一个运算量很大的任务,观察多线程和单线程的差异

——测试:有两个整数变量,分别要对这俩变量自增10亿次,分别使用一个线程,和两个线程 (典型的 CPU 密集场景) 就像这种衡量执行时间的代码,让他跑的久一点,不是坏事,跑的久,误差就小 (线程调度自身也是有时间开销的),运算的任务量越大,线程调度的开销相比之下就特别不明显了,从而就可以忽略不计

此处不能直接这么记录结束时间,别忘了,现在这个求时间戳的代码是在 main 线程中
main t1t2 之间是并发执行的关系,此处t1 t2 还没执行完呢,这里就开始记录结束时间了,这显然是不准确的
正确做法应该是让 main 线程等待 t1 和 t2 跑完了,再来记录结束时间
join 效果就是等待线程结束t1.join 就是让 main 线程等待 t1 结束,t2.join 让 main 线程等待 t2结束

package thread;

public class demo7 {
    // 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
    private static final long count = 10_0000_0000;
    
    public static void main(String[] args) throws InterruptedException {
	    serial();  // 使用串行方式,一个线程完成
    	concurrency(); // 使用并行方式,两个线程完成
    }

    public static void serial() {
        long begin = System.currentTimeMillis(); // 获取到当前系统的 ms 级时间戳
        long a = 0;
        for (int i = 0; i < count; i++) {
            a++;
        }
        long b = 0;
        for (int i = 0; i < count; i++) {
            b++;
        }
        long end = System.currentTimeMillis();
        System.out.println("消耗时间: " + (end- begin) + "ms");
    }

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

        t1.join(); // 让 main 线程等待 t1 结束
        t2.join(); // 让 main 线程等待 t2 结束

        long end = System.currentTimeMillis();
        System.out.println("消耗时间: " + (end- begin) + "ms");
    }
}

——main 线程先调用 t1.start,
启动 t1,开始计算 t1 的同时,main 再调用 t2.start
启动 t2 的同时,t1 仍然再继续计算,同时 main 线程进入就 t1.join
此时 main 阻塞等待了,t1和t2 还是再继续执行的
等到 t1 执行完了,main线程从 t1.join 返回,再进入 t2.join,再来等待 (如果 t2 已经执行完,直接返回)
等到 t2 执行完了,main 从 t2.join 返回,继续执行计时操作

——结果:串行执行的时候,时间大概是600多ms (平均650左右)
两个线程并发执行,时间大概是400多ms (平均450左右)
提升了接近50%

——为什么不是,一个线程600多ms,两个线程就是300多ms?
为啥使用多线程能快?
多线程可以更充分的利用到多核心 cpu 的资源,但是 t1 和 t2,不一定是分布在两个CPU上执行的
这俩线程在底层到底是并行执行,还是并发执行,不确定,真正并行执行的时候,效率才会有显著提升

1). 实际上,t1和t2 在执行过程中,会经历很多次的调度
这些次调度,有些是并发执行的 (在一个核心上),有些是并行执行的 (正好在两个核心上)

到底多少次是并发,多少次是并行? 不好预估,取决于你的系统的配置,也取决于,当前程序的运行环境 (系统同一时刻跑了很多程序,并行的概率就更小,有更多的人来抢CPU)

2). 另一方面,线程调度自身也是有时间消耗的

虽然缩短的不是50%,但是仍然很明显,仍然很有意义!!

多线程特别适合于那种CPU密集型的程序,程序要进行大量的计算,使用多线程就可以更充分的利用CPU的多核资源,提高程序运行效率

不是说使用多线程,就能一定提高效率!!!

  1. 是否是多核 (现在的CPU基本都是多核了)
  2. 当前核心是否空闲 (如果CPU这些核心已经都满载了,这个时候启动更多的线程也没啥用)

六、Thread类的其他的属性和方法

1、Thread 的常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
【了解】Thread(ThreadGroup group, Runnable target)线程可以被用来分组管理,分好的组即为线程组,这 个目前我们了解即可

Thread(String name)Thread(Runnable target, String name):多了一个参数 name,这个东西是给线程 (thread对象) 起一个名字,起一个啥样的名字,不影响线程本身的执行
仅仅只是影响到程序猿调试,可以借助一些工具看到每个线程以及名字,很容易在调试中对线程做出区分

线程默认的名字,叫做 thread-0 之类的… thread-1,2,3

示例:

public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("thread");
        });
        t.start();
    }
}

在这里插入图片描述


2、Thread 的几个常见属性

属性获取方法
IDgetId()
名称getName() 构造方法里起的名字
状态getState() 线程状态. (Java里线程的状态要比操作系统原生的状态更丰富一些)
优先级getPriority() 这个可以获取,也可以设置.但是设置了也没啥用
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

——是否后台线程(守护线程)isDaemon()
如果线程是后台线程,不影响进程退出
如果线程不是后台线程,就会影响到进程退出
创建的 t1t2 默认都是前台的线程,即使 main 方法执行完毕,进程也不能退出,得等 t1t2 都执行完,整个进程才能退出!
如果 t1t2 是后台线程,此时如果 main 执行完毕,整个进程就直接退出,t1t2 就被强行终止了

上面示例中,其他 JVM 自带的线程都是后台线程

也可以手动的使用 setDaemon 设置成后台线程,把 t 设置成守护线程 / 后台线程,此时进程的结束与否就和 t 无关了,此时前台线程只有 main 线程,什么时候结束,就看 main 线程什么时间结束

在这里插入图片描述

——是否存活isAlive()
操作系统中对应的线程是否正在运行 (t 的 run 还没跑-false;run 正在跑-true,跑完了-false)
Thread t 对象的生命周期和内核中对应的线程,生命周期并不完全一致
创建出t对象之后,在调用 start 之前,系统中是没有对应线程的

在这里插入图片描述

run方法执行完了之后,系统中的线程就销毁了,此时线程销毁,但是t这个对象可能还存在,通过 isAlive 就能判定当前系统的线程的运行情况
如果调用 start 之后,run 执行完之前,isAlive 就是返回 true 。如果调用 start 之前,run 执行完之后,isAlive 就返回 false

在这里插入图片描述

在这里插入图片描述

ID 是线程的唯一标识,不同线程不会重复
名称是各种调试工具用到
优先级高的线程理论上来说更容易被调度到
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
是否存活,即简单的理解,为 run 方法是否运行结束了

public class ThreadDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 我还活着");
                            Thread.sleep(1 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
        });
        System.out.println(Thread.currentThread().getName()
                + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName()
                + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName()
                + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName()
                + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName()
                + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName()
                + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {}
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
    }
}

3、Thread中的一些重要方法

3.1、启动一个线程-start()

start() 决定了系统中是不是真的创建出线程

start 和 run 的区别:

  • run() 单纯的只是一个普通的方法,描述了任务的内容
  • start() 则是一个特殊的方法,内部会在系统中创建线程
package thread;

public class demo1 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        // t.run();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

start() 是并发执行,而 run() 循环打印 hello thread
run方法只是一个普通的方法,你在main线程里调用 run,其实并没有创建新的线程,这个循环仍然是在 main 线程中执行的
既然是在一个线程中执行,代码就得从前到后的按顺序运行,运行第一个循环,再运行第二个循环

for (int i = 0; i < 5; i++) {
	System.out.println("hello thread");
		try {
			Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
}

如果改成循环五次,打印五个 hello thread,让后打印 hello main


3.2、中断一个线程

中断的意思是,不是让线程立即就停止,而是通知线程,你应该要停止了,是否真的停止,取决于线程这里具体的代码写法

例如我在打游戏,我妈叫我去买酱油,可以立即就去,也可以打完再去,或者不去

线程停下来的关键,是要让线程对应的 run 方法执行完
还有一个特殊的是 main 这个线程,对于 main 来说,得是main方法执行完,线程就完了

如何中断线程:

① 标志位

可以手动的设置一个标志位 (自己创建的变量,boolean),来控制线程是否要执行结束

这个代码之所以能够起到,修改 flag,t 线程就结束
完全取决于 t 线程内部的代码,代码里通过 flag 控制循环
因此,这里只是告诉让这个线程结束,这个线程是否要结束,啥时候结束都是线程内部自己代码来决定的

package thread;

public class demo2 {
    private static boolean flag = true;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (flag) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        // 在主线程里就可以随时通过 flag 变量的取值,来操作 t 线程是否结束
        // 只要把这个 flag 设为true,这个循环就退出了,进一步的 run 就执行完了,再进一步就是线程执行结束了
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        flag = true;
        System.out.println("终止 t 线程!");
    }
}

运行输出:

hello thread
hello thread
hello thread
hello thread
hello thread
终止 t 线程!

在其他线程中控制这个标志位,就能影响到这个线程的结束
此处因为,多个线程共用同一个虚拟地址空间,因此,main 线程修改的 flagt 线程判定的 flag,是同一个值


② interrupted()

但是,flag 并不严谨,更好的做法,使用 Thread 中内置的一个标志位来进行判定,Thread提供了内置的标志位,
可以使用 isInterruptted 方法判定标志位,
使用 interrupt 方法来设置标志位 (还能够把线程从休眠中唤醒:通过异常,但是会把标志位清除)

Thread.currentThread() 这是一个静态的方法,可以获取到当前线程的实例,哪个线程调用的这个方法,就是得到哪个线程的对象引用,很类似于 this,下面例子是在 t.run 中被调用的,此处获取的线程就是 t 线程

Thread.currentThread().isInterrupted() 这是实例方法,为true表示被终止,为false表示未被终止.(应该要继续走)
t.interrupt(); 就是终止线程

上个方法里主要就是直接操作一个boolean变量,这个代码就是把boolean操作封装到,Thread 的方法里了

package thread;

public class demo3 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000); // 如果线程在sleep中休眠,此时调用interrupt会把t线程唤醒.从sleep中提前返回
                } catch (InterruptedException e) { // interrupt会触发sleep内部的异常,导致sleep提前返回
                    e.printStackTrace();
                }
            }
        });
        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 在主线程中,调用 interrupt 方法,中断这个线程
        // 让 t 线程被中断
        t.interrupt(); // 将标志位设置为 true
    }
}

运行此代码,打印五次 hello thread 后,出现异常,然后继续打印 hello thread

调用 t.interrupt() 这个方法,会做两件事:

  1. 如果 t 线程是处在就绪状态,就是设置线程内部的标志位为 true
  1. 如果 t 线程处在阻塞状态 (sleep 休眠了),就会触发一个 InterruptException,把 sleep唤醒
    但是 sleep 在唤醒的时,还会做一件事,把刚才设置的这个标志位再,设置回 false (清空了标志位),这就导致,当sleep 的异常被catch完了之后,循环还要继续执行!!!

为什么 sleep 要清除标志位?
唤醒之后,线程到底要终止,还是不要,到底是立即终止,还是稍后,就把选择权交给程序猿自己

大前提:调用interrupt只是告诉线程,你应该终止了,但是他是不是真的终止,这是他自己的事情

线程A通知B是否要终止,B是不是真的终止了,看B自己的心情
为啥不设计成 A 让 B 终止,B 就立即终止呢?
A B 之间是并发执行的,随机调度的,导致 B 这个线程执行到哪里了,A 是不清楚的

在这里插入图片描述

这个代码绝大部分情况,都是在休眠状态阻塞
此处的中断,是希望能够立即产生效果的
如果线程已经是阻塞状态下,此时设置标志位就不能起到及时唤醒的效果

调用这个 interrupt 方法,就会让 sleep 触发一个异常,从而导致线程从阻塞状态被唤醒
当下的代码,一旦触发了异常之后,就进入了 catch 语句,在 catch 中,就单纯的只是打了一个日志
printStackTrace 是打印当前出现异常位置的代码调用栈,打完日志之后,就直接继续运行

解决方法:

package thread;

public class demo3 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    // 当前触发异常后,立即退出循环
                    System.out.println("这个是收尾工作");
                    break;
                }
            }
        });
        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 在主线程中,调用 interrupt 方法,中断这个线程
        // 让 t 线程被中断
        t.interrupt();
    }
}

运行结果:

hello thread
hello thread
hello thread
hello thread
hello thread
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at thread.demo3.lambda$main$0(demo3.java:9)
	at java.lang.Thread.run(Thread.java:748)
这个是收尾工作

推荐的做法:
咱们一个代码中的线程有很多个,随时哪个线程都可能会终止
Thread.interrupted() 这个方法判定的标志位是Threadstatic成员,一个程序中只有一个标志位
Thread.currentThread().isInterrupted() 这个方法判定的标志位是 Thread 的普通成员,每个示例都有自己的标志位,一般就无脑使用这个方法即可

方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位

3.3、线程等待-join()

线程是一个随机调度的过程, 线程之间的执行是按照调度器来安排的,这个过程可以视为是 “无序,随机”,这样不太好,有些时候,我们需要能够控制线程之间的顺序

线程等待,就是其中一种控制线程执行顺序的手段,线程等待做的事,主要是控制线程结束的先后顺序

join():调用 join 的时候,哪个线程调用的 join ,哪个线程就会阻塞等待,要等到对应的线程执行完毕为止 (对应线程的 run 执行完)

package thread;

public class demo4 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        System.out.println("join 前");

        // 在主线程中,使用等待操作,等 t线程执行结束
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("join 后");
    }
}

——执行结果:

join 前
hello thread
hello thread
hello thread
hello thread
hello thread
join 后

本身执行完 start 之后,t 线程和 main 线程就并发执行,分头行动。main 继续往下执行,t 也会继续往下执行

首先,调用这个方法的线程是 main 线程,针对t这个线程对象调用的,此时就是让 main 等待 t
调用 join 之后,main 线程就会进入阻塞状态 (暂时无法在cpu上执行)
代码执行到 join 这一行,就暂时停下了,不继续往下执行了

那么join什么时候能继续往下走,恢复成就绪状态呢?
就是等到 t 线程执行完毕 ( trun 方法跑完了)
通过线程等待,就是在控制让 t 先结束,main后结束,一定程度上的干预了这两个线程的执行顺序
这是代码中控制的先后顺序,就像刚才写的自增 100 亿次这个代码,计时操作就是要在计算线程执行完之后再执行

方法说明
public void join() 死等等待线程结束
public void join(long millis) 更常见的方式等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)同理,但可以更高精度

但是 join 操作默认情况下(无参版),是死等,不见不散,这不合理
join 提供了另外一个版本,就是可以设置等待时间最长等待多久,如果等不到,就不等了

try {
	t.join(10000);
} catch (InterruptedException e) {
	e.printStackTrace();
}

进入 join 也会产生阻塞,这个阻塞不会一直持续下去,如果 10s 之内,t 线程结束了,此时 join 直接返回
如果10s之后,t 仍然不结束, 此时 join 也就直接返回
日常开发中涉及到的一些 "等待” 相关的操作,一般都不会是死等,而是会有这样的 “超时时间”

如果是执行 join 的时候,t 已经结束了,join 不会阻塞,就会立即返回
t.start(); 后 t 开始打印,main 也开始 sleep,t 打印完,main sleep 完,此时 join t ,就无需等待,立即返回

在这里插入图片描述

多线程是实现并发编程的基础方式,比较接近系统底层,使用起来不太友好
为了简化并发编程,引入了很多方便的写法,比如,
erlang(编程语言),引入actor模型.
go,引入了csp模型
js,使用回调的方式=> async await
python => async await


3.4、获取当前线程的引用

方法说明
public static Thread currentThread();返回当前线程对象的引用

为什么用 static:
字面上看,static这个词,和类方法/类属性之间,没有任何联系 (历史遗留问题)

历史遗留问题,Java这么搞,原因是C++就是这么搞,C++ 又是从C来的,

追溯到 C 语言,static可以修饰全局/局部变量,修饰函数。

即使是单例模式中,static 的效果也是不和字面意思—样了
早期的操作系统中,内存区域划分,确实有一个"静态内存区",static 初心就是表示把变量放到 “静态内存区”,于是就引入了关键字 static

随着计算机的发展,静态内存区这个东西就逐渐没了,但是 static 关键字仍然在,并且被赋予了其他的功能

C++中 static 除了上述 C 中的功能之外,又有了新的用法:修饰一个类的成员变量和成员函数,此处 static 修饰的成员就表示类成员
Java 就把 C++的 static 给继承过来了

为啥这里是使用 static 表示类属性,不新造一个关键字,用新的关键字来表示"类属性,类方法"(classmethod)??
这种操作冲击巨大,要考虑 “代码的兼容性”

—个编程语言,要想新增关键字,是一件非常有风险的事情

关键字不能作为变量名,不能作为函数名,不能作为类名…
一旦引入了新的关键字,此时现有的千千万万的代码,里面一旦使用了这个关键字的单词作为变量名,代码就突然无法编译了

=> 类方法(更好的说法):调用这个方法,不需要实例,直接通过类名来调用
Thread t = new Thread();
直接这样通过类名 Thread.currentThread()
不一定非要 t.currentThread()

Thread.currentThread() 就能够获取到当前线程的实例 (Thread 实例的引用),哪个线程调用的这个 currentThread,就获取到的是哪个线程的实例

package thread;

public class demo5 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()); // Thread-0
            }
        };
        t.start();

        // 在 main 线程中调用的,拿到的就是 main 这个线程的实例
        System.out.println(Thread.currentThread().getName()); // main
    }
}

this.getName() :对于这个代码,是通过继承 Thread 的方式来创建线程
此时在 run 方法中,直接通过 this,拿到的就是当前 Thread 的实例

		Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println(this.getName());
            }
        };
        t.start();

此处的 this 不是指向 Thread 类型了,而是指向 Runnable,而 Runnable 只是一个单纯的任务,没有 name 属性的

		Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(this.getName()); // err
            }
        });
        t.start();

要想拿到线程的名字,只能通过 Thread.currentThread()
lambda 表达式效果同 Runnable

		Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });
        t.start();

3.5、休眠当前线程

因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis 毫秒
public static void sleep(long millis, int nanos) throws InterruptedException可以更高精度的休眠

sleep 所谓的休眠到底是在干啥?让线程休眠,本质上就是让这个线程不参与调度了 (不去CPU上执行了)

进程:PCB+双向链表,这个说法是针对只有一个线程的进程是如此的
如果是一个进程有多个线程,此时每个线程都有一个PCB一个进程对应的就是一组PCB了
PCB 上有一个字段 tgroupld,这个id其实就相当于进程的 id同一个进程中的若干个线程的 tgroupld 是相同的

process control block
进程控制块 和 线程有啥关系?其实 Linux 内核不区分进程和线程
进程线程是程序猿写应用程序代码,搞出来的词,实际上 Linux 内核只认PCB
在内核里 Linux 把线程称为轻量级进程

在这里插入图片描述
如果某个线程调用了 sleep 方法,这个 PCB 就会进入到阻塞队列,暂时不参与 CPU的调度。比如调用sleep(1000),对应的线程PCB就要再阻塞队列中待1000ms 这么久,到但是当这个PCB回到了就绪队列,会被立即调度嘛? 虽然是sleep (1000),但是实际上考虑到调度的开销,对应的线程是无法在唤醒之后立即就执行的,所以实际上的时间间隔大概率要大于1000ms

操作系统调度线程的时候,就只是从就绪队列中挑选合适的 PCB 到 CPU 上运行,阻塞队列里的 PCB 就只能干等着,当睡眠时间到了,系统就会把刚才这个 PCB 从阻塞队列挪回到就绪队列,等待参与 CPU 的调度,以上情况都是在 Linux 系统

内核中的很多工作都依赖大量的数据结构,但凡是需要管理很多数据的程序,都大量的依赖数据结构


4、线程的状态

进程有状态:就绪,阻塞… 这里的状态就决定了系统按照啥样的态度来调度这个进程,这里相当于是针对一个进程中只有一个线程的情况
咱们现在认为,线程是调度的基本单位了,所以所谓的状态,其实是绑定在线程上,状态是针对当前的线程调度的情况来描述的
Linux 中,PCB 其实是和线程对应的,一个进程对应着一组 PCB

上面说的 “就绪" 和 “阻塞” 都是针对系统层面上的线程的状态 (PCB),比较简单
在 Java Thread 类中,对于线程的状态,又进—步的细化了

线程的状态是一个枚举类型 Thread.State:

public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
        }
    }
}

结果:

NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED

1、NEW:安排了工作, 还未开始行动

创建了 Thread 对象,但是还没有调用 start (内核里还没创建对应PCB)

public class demo6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            
        });
        System.out.println(t.getState()); // NEW
        t.start();
    }
}

2、 TERMINATED:工作完成了

操作系统中的 pcb 已经执行完毕,销毁了,但是 Thread 对象还在,获取到的状态

public class demo6 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {

        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState()); // TERMINATED
    }
}

以上两个状态是 Java 内部搞出来的状态,就和操作系统中的 PCB 里的状态就没啥关系

3、 RUNNABLE:可工作的。又可以分成 正在CPU上执行的在就绪队列里,随时可以去CPU上执行

就绪状态,处于这个状态的线程,就是在就绪队列中,随时可以被调度到 CPU 上
如果代码中没有进行 sleep,也没有进行其他的可能导致阻塞的操作,代码大概率是处在 Runnable 状态的

public class demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                // 这里什么都不能有
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState()); // RUNNABLE
    }
}

一直持续不断的执行这里的循环,随时系统想调度它上cpu都是随时可以的

下面三个都是阻塞 (都是表示线程PCB 正在阻塞队列中),这几个状态是不同原因的阻塞

4、TIMED_WAITING:这几个都表示排队等着其他事情

代码中调用了 sleep,就会进入到 TIMED_WAITIN,意思就是当前的线程在一定时间之内,是阻塞的状态

public class demo7 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState()); // TIMED_WAITING
    }
}

一定时间到了之后,阻塞状态解除这种情况就是 TIMED_WAITING,也是属于阻塞的状态之一

5、BLOCKED:这几个都表示排队等着其他事情

当前线程在等待锁,导致了阻塞 (阻塞状态之一) --synchronized

6、WAITING:这几个都表示排队等着其他事情

当前线程在等待唤醒,导致了阻塞 (阻塞状态之一) --wait

例:刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;

当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;

当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入 BLOCKED 、 WATING 、 TIMED_WAITING 状态,至于这些状态的细分,我们以后再详解;

如果李四、王五已经忙完,为 TERMINATED 状态。

所以,之前我们学过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。

为什么要这么细分?这是非常有好处的:

开发过程中经常会遇到一种情况,程序 "卡死” 了
一些关键的线程,阻塞了
在分析卡死原因的时候,第一步就可以先来看看当前程序里的各种关键线程所处的状态


5、观察线程的状态和转移

线程状态转换简图:

在这里插入图片描述

public class Demo {
    public static void main(String[ ] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for(int i = 0; i < 1000000; i++) {
                // 这个循环体,也不 sleep
            }
        });

        // 启动之前,获取 t 的状态,就是 NEW 状态.
        System.out.println( "start 之前: " + t.getState());

        t.start();
        // 运行中的状态:RUNNABLE
        System.out.println("t 执行中的状态:" + t.getState());
        t.join();

        // 线程执行完毕之后,就是 TERMINATED 状态
        System.out.println("t 结束之后: " +t.getState());
    }
}

运行结果:

start 之前: NEW
运行中的状态:RUNNABLE
t 结束之后: TERMINATED

——TERMINATED:

一旦内核里的线程PCB消亡了,此时代码中 t 对象也就没啥用了,之所以存在,是迫不得已

Java 中的对象的生命周期,自有其规则 (new 的对象,GC 回收)
这个生命周期和系统内核里的线程并非完全一致
内核的线程释放的时候,无法保证 Java 代码中 t 对象也立即释放

因此,势必就会存在,内核的PCB没了,但是代码中的 t 还在这样的情况,
此时就需要通过特定的状态,把 t 对象标识成 “无效” => TERMINATED

也是不能重新 start 的,一个线程,只能 start 一次!!
t 线程对象,如果TERMINATED之后,还有重新启用的机会,程序猿就不好判定当前这里的 t 到底是一个有效的 (后面还要用,还是就无效了,后面肯定不用了)
如果明确TERMINATED就是终结,没有重新 start 的机会了,此时程序猿就可以心安理得的放弃 t,同时后续任何代码中使用了 t 都可以视为,是不太科学的操作了

——RUNNABLE:

之所以,此处能看到 RUNNABLE,主要就是因为当前线程 run 里面,没写任何 sleep 之类的方法

在这里插入图片描述

在这里插入图片描述

——yield() 大公无私,让出 CPU:

Thread t1 = new Thread(new Runnable() {
	@Override
	public void run() {
		while (true) {
        	System.out.println("张三");
			// 先注释掉, 再放开
			// Thread.yield();
	    }
	}
}, "t1");
t1.start();

Thread t2 = new Thread(new Runnable() {
	@Override
	public void run() {
        while (true) {
    	    System.out.println("李四");
        }
	}
}, "t2");
t2.start();
  1. 不使用 yield 的时候, 张三李四大概五五开

  2. 使用 yield 时, 张三的数量远远少于李四

结论:
yield 不改变线程的状态,,但是会重新去排队


  • 50
    点赞
  • 156
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 49
    评论
评论 49
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三春去后诸芳尽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值