操作系统,进程与线程(一)(上)+https://developer.aliyun.com/article/1413561
就像送外卖,外卖小哥和你是互不接触的,但是你们可以通过快递柜实现信息的传递
三.线程 Thread
前言:多进程
多进程出现的初心是为了实现"并发编程",就是为了提高程序运行的效率,如今多核cpu的时代,更加突出"并发编程"的重要性,但是多进程编程也有一个致命的缺点,进程的创建,销毁,调度既缓慢又需要开辟响应的内存空间(进程是资源分配的基本单位),即使进程的管理是通过双向链表这样的数据结构实现的,但是随着进程的增多,进程管理所消耗的时间和内存就不容忽视,为了进一步的提高效率,就引入了"线程"
1.线程的概念
线程是轻量级进程,线程必须依赖于进程,进程包含线程。
可以理解为线程是对进程内部的进一步优化,将一个完整的进程划分为多个"小线程",从而提高效率
一个进程至少要有一个线程,这个线程可以执行一些代码,进程也可以包含多个线程,让这多个线程各自执行一些代码,从而实现并发编程的效果。
我们之前所说的进程的调度,是基于"一个进程只有一个线程"的前提。实际上"进程的调度"是不准确的,应该是"线程的调度",线程是调度执行的基本单位,也就是说每个PCB对应一个线程,每个线程都有其独自的状态,优先级,上下文,记账信息,但是对于同一个进程内部的线程来说,他们公用同一份pid,内存指针,文件描述符表。
那为什么多线程就比多进程效率更高呢?多进程效率低的原因在于进程的调度需要频繁的进行资源分配,内存开辟,而线程的创建,销毁都不需要进行资源分配或者内存开辟,因为线程使用的是进程的资源和进程的内存,而不需要再去硬盘上获取额外的资源(硬盘的读写是一个非常耗时的操作),从而提高了并发编程的效率
逻辑链:
进程包含线程 => 一个进程包含多个PCB =>每个PCB就用来表示一个线程 =>每个线程都有其专属的状态,优先级,上下文,记账信息 => 每个PCB共用同一个进程的内存空间和文件描述符表 =>每个线程都能独自去cpu上进行调度 =>省去了创建,分配内存的步骤 =>提高效率
进程是资源分配的基本单位
线程是调度执行的基本单位
如果再从宏观的角度来看,一个系统含有多个进程,每个进程都有自己的资源
一个进程包含多个线程,每个线程都能独自进行调度,共用内存指针/文件描述符表
2.线程越多,效率越高么?
既然线程能够提升效率,那么我们只要在一个进程内部尽可能多的创建新的线程,那效率不就会越来越高么?实际上,任何事情都不能过度,线程数目过多,效率会不增反降。进程也是需要去管理的线程的,当线程数量较少时,管理起来很容易,也能够提高效率,但如果线程的数量越来越多,就变得难以管理了,进程管理的难度就会增加,且更加容易出错(管理3人团队远没有管理100人难),线程与线程之间可能会发生矛盾
如果出现同一进程内的两个线程发生冲突,称为"线程不安全问题",这是多线程的重点,同时,如果某个线程出现问题(抛出异常),如果没有妥善处理,可能会导致整个进程都崩溃(比如某个员工一怒之下删除了所有数据库),其他线程也就随之崩溃(也就是说线程之间不是独立的)
3,经典面试题:进程与线程的区别
- 进程包含线程,一个进程可以有一个线程,也可以有多个线程(结构关系)
- 进程和线程都是为了实现"并发编程",提高程序的运行效率,但是线程更加轻量化,速度更快(作用)
- 多个线程位于同一个进程内,共用同一份资源(硬件+内存),省去了资源的开辟与销毁(为什么线程更快呢) 同时在写代码的时候就可以使用同一个变量(减少了额外为变量开辟的内存)
- 进程是资源分配的基本单位,线程是调度执行的基本单位
- 进程与进程之间通过"虚拟地址空间"实现了隔离,也就是进程具有独立性,与其他进程互不干扰;而线程不具有这样的性质,位于同一个进程之内的线程来说,一个线程的异常可能影响到其他线程
四.Java中如何实现多线程编程
我们要知道,不同的操作系统的进程的调度方式是不同的,他们的cpu存储的指令也是不同的,按理说不同的操作系统对应的多线程的实现方式是不同的,但是Java提供了一套规范"api"将不同的操作系统的线程实现方式统一起来,最后再将其封装,从而实现了"跨平台性"
在Java中通过Thread类实现多线程编程
1.创建Thread类 -- 继承Thread 重写run方法
// 创建一个类 继承于Thread类 class MyThread extends Thread { @Override public void run() { // 线程的入口 告诉线程要执行哪些逻辑 System.out.println("hello thread"); } }
2.通过start方法进行调用
public class Test { public static void main(String[] args) { // 首先要实例化出一个Thread类 Thread thread = new MyThread(); // start和run都是Thread类的成员 // run只是告诉线程要去执行那些逻辑 // start是真正的调用系统的api,创建出一个线程,再让线程去执行run thread.start(); System.out.println("hello main"); } }
3.输出结果
注意:
1.run方法的作用是告诉你线程的执行入口 进入到run方法内部执行代码 就和一个Java程序的入口是main一样
2.start方法的作用的调用系统的api,创建出一个线程,再让线程去执行run
3.start和run的区别就是有没有创建出一个新的线程
下面修改一下代码,将打印的逻辑都放在while(true)循环之中
// 线程的入口 告诉线程要执行哪些逻辑 while(true) { System.out.println("hello thread"); } while (true) { System.out.println("hello main"); }
执行结果:
从这张图中我们可以看出, 两个while循环在"同时执行" =>实现了并发编程,每一个线程都是一个独立的执行流,独立的执行逻辑;还可以看出,具体执行哪一个线程或者线程执行的次数是不确定的,一是因为线程的调度是具有优先级的,但本质上线程的调度取决于cpu上的调度器的实现
如果将main方法中的start方法改为run方法
此时只会执行run方法内部的代码,原因在于start方法调用了系统的api,开辟了一个新的线程,这个新的线程用于执行"hello main"这个代码;而run方法并不会创建出一个新的线程,他只是"主线程"的入口,他只会执行run方法内部的代码。使用start方法时,相当于开辟了两个线程,一个是run方法,另一个是"hello main"对应的方法
如何查看线程的具体执行情况呢?可以使用jdk中的"jconsole"程序
注意:
若jconsole打开之后什么也不显示,可以使用"以管理员身份"打开
未来写一些多线程的程序时,可以通过jconsole来时刻观察线程的运行状态
让线程的while循环转的慢一点-->在循环体内部使用sleep
// 创建一个类 继承于Thread类 class MyThread extends Thread { @Override public void run() { // 线程的入口 告诉线程要执行哪些逻辑 while(true) { System.out.println("hello thread"); try { // 休眠1s Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Test { public static void main(String[] args) throws InterruptedException { // 首先要实例化出一个Thread类 Thread thread = new MyThread(); // start和run都是Thread类的成员 // run只是告诉线程要去执行那些逻辑 // start是真正的调用系统的api,创建出一个线程,再让线程去执行run thread.start(); // thread.run(); while (true) { System.out.println("hello main"); // 休眠1s Thread.sleep(1000); } } }
注意:
1.sleep方法是Thread类的一个类方法,可以直接通过类名来访问
2.sleep方法在使用的过程中会抛出异常,不同的位置处理异常的方式会有所差异
4.再谈run和start的区别
在Java中线程的执行和run方法是紧密相连的,run方法告诉线程需要去执行哪些代码 是线程的入口
对于main方法来说,他自己本身就是一个线程,被称为主线程,也就是说每个Java程序必然含有一个主线程
代码中两个都写了while(true)的循环,如果使用start则两个代码都会被执行 如果使用run则只会执行run方法内部的 另一个代码就不会被执行
我们使用t.start只是为了创建出一个新的线程,在这个新的线程里面去执行我们新的任务
如果两个都是while(true)循环,使用t.start两个代码都会执行,对于自定义的线程来说,因为是while(true)循环,run方法永远没有结束,所以会一直执行,同时主线程中的代码也会执行,系统把"hello tThread"看作一个新的线程
如果都是用while(true)循环,而不使用start方法,打印结果是什么呢?只会执行hello Thread,原因在于如果使用run方法并不会将"hello Thread"看作一个线程,可以理解为就是一个简单的方法调用,只有当调用的方法执行完毕之后会执行main方法中剩余的代码,而这个方法调用反而是死循环的,所以"hello main" 根本不会打印,这就是一个简单的方法调用,是存在先后顺序的,而多线程解决的是"并发编程",是同时执行
最后附上思维导图!!!