前言 : 上文主要了解到了进程, 那么为啥需要引入进程呢?
或者说为啥要有进程呢?
其实这里最主要的目的是为了解决 并发编程 这样的问题。
了解 :
这里 cpu 进入了多核心的时代,想要进一步提高程序的执行速度,就需要充分的利用CPU的多核心资源.
cpu 再往小了做 就比较困难 ,此时先要通过单核心提高执行速度就遇到了困难, 既然一个核心不行那么多个核心呢?显然是可以的,要不一句话人多力量大吗。
但是不是说 cpu的核心多了,程序一下就跑的快了 , 还需要你的程序代码能供把这些 cpu 核心给用上 。
回到进程这里, 关于我们的多进程编程,已经是可以解决并发编程的问题(可以利用cpu的多核资源) ,但是因为频繁的创建和销毁进程对cpu的资源开销是比较大的 ,调度进程同样也是 ,另外 使用进程会拖慢了我们的运行速度 。
正因为 使用 进程, 开销比较大速度比较慢 , 为了解决这个问题 我们的线程
就应运而生 , 这里线程也称为 轻量级进程
那么为啥说进程的开销比较大呢 ?
这里主要就在 资源分配 / 回收上
, 之前说过 进程是分配资源的最小单位, 我们想要创建进程就需要给它分配资源 (内存 资源, 硬盘资源等), 此时分配资源就需要花费一定的时间.
那么为啥说线程的开销小速度快呢 ?
其实也就是线程将分配资源/ 回收上 / (申请资源和回收资源)
的这一部分开销给省略了
为了好理解 这里举一个例子 :
那种流水线的厂子不知道大家进没进过,鄙人有幸在里面做过一天的事情,可以说是非常的累的 。
假设 有一个厂是生产手机的 ,一天能生产 1w部手机, 但是市场上需求量非常高,导致一天生产 1w部手机可能不够,此时厂长就想要提高产量,让每天有 2 w 部手机产出 ,这里就有两种解决方式
1.在其他的方法重新开一个厂,然后两个厂同时生产那么此时是不是一天就可以达到2w部,
2.既然是流水线,那么是不是可以多增加一条流水线,此时是不是同样可以使 一天 2 w 部 (假设之前的厂只有一条流水线,但大部分不可能是这个情况)
对比一下这两种方法,你觉的是 1 好 还是 2 好 显然是 2 好,因为重新开一个厂 ,需要挑选场地, 建厂, 装流水线等都是需要花钱的,而 方法二 新增一条流水线 ,就不需要花额外的钱去 挑选场地,建厂 。
这里是不是就可以认为 方法一 是使用进程 ,而方法二是线程呢?
既然将 进程 想象成 厂 , 线程想象程流水线, 这里就可以得出 进程和线程之前的关系 。
一个进程可以包含一个线程,也可以包含多个线程(注意 : 这里不能没有)
好理解 : 厂吗不可能没有流水线的,要不然咋生产商品赚钱呢 做慈善吗, 同样厂可以拥有一条,也可以拥有多条.
另外 : 正因为我们的厂内可以拥有多条流水线,那么是不是只有创建厂和新建第一条流水线的时候开销比较大,之后添加的流水线,就直接在厂内添加即可? 放到进程和线程上来说,也就是我们启动第一个线程的时候开销是比较大的 去申请 资源 (没有资源咋办事吗),后面的进程就省事了, 因为是不会去申请和释放资源的直接用即可.
通过上面总结 : 一个进程可以包含多个线程程 , 这些线程之间共用同一份资源
补充: 这里的资源指的是 内存 和 文件描述符表
内存 : 线程1 new 的对象 在 线程 2 , 3 , 4 等 都可以直接使用
文件描述符表 : 线程 1 打开的文件 , 在线程 2 , 3 , 4 等轴都可以直接使用 .
另外 : 操作实际调度的时候,是以线程为单位进行调度的.
回到上文 所说的进程调度就不够准确, 准确来说是以线程为单位来调度的 , 如果站在上文的角度 说进程调度就相当于每一个进程只有一个线程的情况.
补充 : 这里每个进程中的线程都是独立在CPU上进行调度的 , 换句话来说 线程是操作系统调度执行的基本单位
另外 : 一个线程就是一个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
看完上面, 有没有想过 我们要如何去描述一个线程呢 ?
在上文 介绍的 进程 通过 PCB来描述的,那么线程是不是也是呢 ?
答案 : 我们的线程同样也是通过PCB来描述的.
注意 :这里别晕,其实PCB对应的就是线程,为啥上文说是对应进程呢?因为是站在一个进程只有一个线程的角度,所以才会所对应PCB
此时就有趣了,既然 线程也是通过 PCB 来描述的,那么进程又可以拥有多个线程,那么进程是不是会对应多个PCB呢 ?
没错 我们的进程 是可能对应多个PCB的,正因线程同样也是被PCB描述的,进程中又含有多个线程所以,进程就会对应多个PCB了,
那么还有一个问题 :上文介绍 过一对PCB的属性 状态 ,上下文, 优先级 ,记账信息,是每个线程都有自己的还是共同使用一个呢 ?
答案 : 每个线程都会拥有自己的各自记录各自的,但是 同一进程里的PCB之间, pid是一样的,内存指针和文件描述符表也是一样的.
描述 看完 , 再来像一个问题 , 既然我们使用多线程 比较轻量 ,能提高效率,那么我们能不能一直创建线程呢?
答案 不是的 ,虽然创建线程是能提高我们的 效率但是,线程一多,会导致多个线程进行资源的竞争,此时肯定会有没有抢到的线程(资源就那么一份),此时就会拖慢我们的效率。
举例 :
图一 :
图二 :
总结 : 进程和线程的区别 (注意 :这里是非常经典的面试题)
1.进程是包含线程的 , 一个进程可以含有多个线程
2.进程和线程都是处理并发编程的场景
3.进程比较重 , 线程比较轻 .
为啥进程重 因为频繁的创建和消费进程的时候效率是非常低的(消耗的资源多) , 而线程就不会 ,
为啥线程轻 因为只有启动第一个线程的时候才会去创建和消耗资源,其他的线程是公用同一份资源的。
4.进程是具有独立性的, 每一个进程之间都拥有各自的虚拟地址空间,一个进程挂了,其他进程是不受影响的,而线程则不会,因为线程之间公用同一块资源,所以当其中一个线程出现了异常就可能导致整个进程崩溃
5.进程是操作系统分配资源的基本单位
6.线程是操作系统调度的执行的基本单位
理论知识 看完,下面来看我们java如何进行多线程操作
引用 :
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
下面我们就通过 Thread 来创建我们的线程。
第一步 : 打开idea 创建类
第二步 : 创建 Thread 对象
问题 一: 为啥使用 Thread 类不需要通过 import
导入别的 包 ?
问题二 :你还见过那些类也是类似的.
答: 我们之前见过的String , StringBuilder ,StringBuffer
都是这样的,因为他们都是在java.lang
底下的,因为java.lang
编译器会自动给我们导入.
问题 看完了, 回到上面,其实这里的线程还是没有创建完成,因为我们创建线程是希望线程成为一个独立的执行流(执行一段代码),上面的程序并没有给他执行的任务。
那么这里的问题就来到了如何指定他的任务呢 ?
这里总共有 5种方法,下面就来看看
方法一 : 实现一个类继承Thread 重写run方法 .
run方法里面就写我们需要执行的任务 .
下面就来执行我们的程序看一下效果 :
注意 : 只有我们 通过 s.start() 去启动线程的时候才会创建线程, 而 new 是不会创建线程的 .
之前说过我们的线程是用来解决并发编程的,上面并没有体现出来,下面我们来修改一下我们的代码,来看看如何并发的。
因为这里执行的非常快我们就可以通过 sleep
让线程休眠一秒来看执行效果
正因为 抢占式执行,才会出现我们的线程安全问题 . 随机调度导致多个线程竞争同一份资源.
另外在来补充一下 t.run
与 t.start
的区别
扩充 : 通过 jdk
自带的工具jconsole
查看当前的 java
进程中的所有进程
图二 : 观察线程
图三 :
此时第一种写法就完成 了 ,下面来看第二种 :
方法二 : 实现Runnable 接口
这里的Runnable 作用 是描述 一个要执行的任务 , run 方法是任务的执行细节.
这里我们不能直接通过 new Runnable
(接口不能实例化) 传给Thread 来执行 , 所以需要去实现一个类 MyRunnable
,然后通过 new 传给 Thread类.
可以看到, 我们的任务是 通过 Runnable 描述的,我们将任务传给了线程, 这里就 将线程 和 线程需要干的活进行了解耦合操作.
这里的好处 就是未来的莫一天,我们要改代码,不用多线程了,使用多进程, 或者线程池或者协程等 此时代码的改动就比较小.
方法三 : 匿名内部类 继承Thread 重写 run方法
知识点 : 《内部类》
这里的方法三其实就是方法一,只不过这里使用了匿名内部类
方法四 : 匿名内部类 实现 Runnable接口 ,重写 run方法
这个写法与 写法2 等价 ,只不过实现 Runnable 的是一个匿名内部内 .
方法五 : 使用Lambda 表达式
知识点 : 反射 - 枚举 - Lambda表达式