小言_互联网的博客

【进程和线程】

499人阅读  评论(0)

目录

进程的五大状态

进程状态如何转移

OS具体怎样进行进程切换

并行(parallel) 和 并发(concurrent)

用户态(user space) 和 内核态(kernel space)

执行流(executionflow)

内存管理

内存管理主要研究的问题

进程通信IPC(Inter Process Communication)

进程间通信的常见方式

线程(Thread)

概念

(1)什么是线程

(2)进程(process) 和线程(thread) 的关系

(3)为什么OS要引入thread这一个概念。

(4)线程和进程的区别

线程的创建

方法一:继承 Thread 类

方法二: 实现 Runnable 接口 

怎么去理解t.start()做了什么?

区分 start() 和 run()

线程和方法调用栈的关系

理解栈和栈帧

Thread的常见属性和方法

(1)常见属性

(2)等待一个线程-join() 

(3)休眠当前线程

(4)获取当前线程引用——Thread.currentThread()

(5)让线程让出CPU——yield( )

(6)中断一个线程

线程的状态

前台线程和后台线程

JDK中自带的观察线程的工具——jconsole


进程的五大状态

  1. 新建:进程处于正在创建中
  2. 就绪:万物具备,只欠CPU
  3. 运行:进程的指令真正在CPU运行着
  4. 阻塞:进程由于等待外部条件,所以暂时无法继续
  5. 结束:进程的所有指令执行结束,但PCB暂时保留,OS还需要做一些其他工作的时候

进程状态转移图:

所有运行状态必须是从就绪状态变过去的因为进程的CPU必须经过OS系统分配才能得到。

进程状态如何转移

  • ->新建 : 随着程序的启动运行
  • 新建->就绪 : 进程的初始化工作完全完成,这个工作是由OS的指令完成的
  • 就绪->运行 : 进程被OS选中,并分配了CPU之后
  • 运行->结束 : 进程的最后一条指令执行结束,可粗略地理解为就是main方法执行结束了
  • 运行->就绪 : 1.被高优先级的进程抢占了;2. 时间片耗尽;3. 进程可以执行一些OS提供的系统调用,主动放弃
  • 运行->阻塞:等待一些外部条件,如等待I0设备;进程休眠一段时间等
  • 阻塞->就绪 : 外部条件满足,如 I0数据来了、休眠时间到了...
  • 结束-> : 进程PCB彻底被OS回收了

在同一时刻,处于不同状态下的进程不止一个。处于新建、就绪、堵塞、结束状态的可以有多个,但单核cpu情况下,同一时刻运行状态的只能有一个。

OS具体怎样进行进程切换

通过上下文切换:保护上一个进程的上下文 + 恢复下一个进程的上下文

上下文:以PC寄存器所代表的一组寄存器中的值。

保护上下文:把寄存器中的值,保存到内存的某个位置。 恢复上下文:把内存中之前保护的值,写入寄存器中

并行(parallel) 和 并发(concurrent)

并行:进程真的同时在执行,既微观角度的同一时刻,是有多个指令在执行的,所以只会在多CPU多核场景下

并发:进程假的同时在执行,微观上,表现为一次只执行一个进程,但宏观上,多个进程在“同时"执行

用户态(user space) 和 内核态(kernel space)

当CPU正在执行的是OS的指令时,就认为进入到内核态。反之,正在执行的是普通进程的指令时,就在用户态。 内核态的指令权限高,基本上所有的硬件都能访问;用户态的指令权限低,只能访问OS规定的资源。这是因为CPU本身就有权限开关。 一般来说,用户态的性能较好,内核态的性能较差。

执行流(executionflow)

各执行流拥有独立pc的一套指令,不同的执行流从现象上看起来是完全独立的。

内存管理

和cpu不同,cpu更注重在时间上的划分,而操作系统对内存资源的分配,采用的是空间模式 —— 不同进程使用内存中的不同区域,互相之间不会干扰。

内存大体分为:内核使用的内存、分配给普通进程使用的内存、空闲空间。要注意空间划分不保证是连续的。

内存管理主要研究的问题

  1. 管理哪些内存已经被分配,哪些内存暂时末被分配
  2. 已经分配出去的内存,何时进行回收、如何进行回收
  3. 物理地址和线性地址转换
  4. 内存碎片

进程通信IPC(Inter Process Communication)

理论上进程是独立的,但实际中往往是多个进程相互配合,来完成复杂的工作。因为OS进行资源分配是以进程为基本单位分配的,包括内存。分配给A进程的内存不会分配给B进程。所以,进程A、B之间直接通过内存来进行数据交换的可能性完全不存在了。所以OS需要提供一套机制, 用于让A、B进程之间进行必要的数据交换一进程间通信。

进程间通信的常见方式

  1. 管道(pipe)
  2. 消息队列(message queue)
  3. 信号量(semaphore)
  4. 信号(signal)
  5. 共享内存(shared memory)
  6. 网络(network)

线程(Thread)

概念

(1)什么是线程

线程是OS进行调度(分配CPU)的基本单位。一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行着 多份代码.

(2)进程(process) 和线程(thread) 的关系

进程和线程是一对多的关系的关系。一个线程一定属于一个进程;一个进程下可以允许有多个线程。
一个进程内至少有一个线程, 通常被这个一开始就存在的线程,称为主线程(main thread)。主线程和其他线程之间地位是完全相等的,没有任何特殊性。

(3)为什么OS要引入thread这一个概念。

由于进程这一个概念天生就是资源隔离的,所以进程之间进行数据通信注定是一个高成本的工作。
现实中,一个任务需要多个执行流一起配合完成,是非常常见的。所以,需要一种方便数据通信的执行流概念出来,线程就承担了这一职责。

(4)线程和进程的区别

  1. 进程是资源分配的最小单位,线程是程序执行的最小单位(资源调度的最小单位)
  2. 进程是包含线程的, 每个进程至少有一个线程存在,即主线程。
  3. 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
  4. 进程和进程之间不共享内存空间.,同一个进程的线程之间共享同一个内存空间。线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。
  5. 多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

线程的创建

方法一:继承 Thread 类

自定义线程类继承Thread
重写run()方法,编写线程执行体
创建线程对象,调用start()方法启动线程


  
  1. public class MyThread extends Thread{
  2. @Override
  3. public void run() {
  4. for ( int i = 0; i < 200; i++) {
  5. System. out.println( "good"+i);
  6. }
  7. }
  8. public static void main(String[] args) {
  9. //主线程
  10. MyThread myThread = new MyThread();
  11. //同时执行,主线程和上面的线程是同时执行的
  12. myThread.start();
  13. for ( int i = 0; i < 200; i++) {
  14. System. out.println( "我在学习"+i);
  15. }
  16. }
  17. }

运行结果:

 

可以看到主线程和上面的线程是同时执行的。

方法二: 实现 Runnable 接口 

定义MyRunnable类实现Runnable接口
实现run()方法,编写线程执行体
创建线程对象
创建Thread类对象,将 Runnable 对象作为参数,调用start()方法


  
  1. public class MyThread_II implements Runnable{
  2. @Override
  3. public void run( ) {
  4. for (int i = 0; i < 200; i++) {
  5. System. out. println( "good"+i);
  6. }
  7. }
  8. public static void main( String[] args) {
  9. //创建runnable接口的实现类对象
  10. MyThread_II myThread = new MyThread_II();
  11. //创建线程对象,通过线程对象开启线程(代理)
  12. Thread thread = new Thread(myThread);
  13. //Thread thread = new Thread(new MyThread_II());
  14. thread. start();
  15. }
  16. }

对比上面两种方法:

  • 继承 Thread 类,直接使用 this 就表示当前线程对象的引用。
  •  实现 Runnable 接口,this 表示的是 MyRunnable 的引用,需要使用 Thread.currentThread()

怎么去理解t.start()做了什么?

t.start()只做了一件事情:把线程的状态从新建变成了就绪。也就是说它不负责分配CPU。

线程把加入到线程调度器的就绪队列中,等待被调度器选中分配CPU。从子线程进入到就绪队列这一刻起,子线程和主线程在地位上就完全平等了。

所以,哪个线程被选中去分配CPU,就完全听天由命了。先执行子线程中的语句还是主线程中的语句理论上都是可能的。这就是为什么我们看到主线程和自定义的线程交替执行。

观察我们创建的第一个线程结果,大概率是主线程中的打印先执行,不是说两个线程地位平等吗?

t.start()是在主线程的语句。换言之,这条语句被执行了,说明主线程现在正在CPU上。(主线程运行状态),所以,主线程刚刚执行完t.start() 就马上发生线程调度的概率不大,因此大概率还是t.start()的下一条语句就先执行了。

区分 start() 和 run()

不要错误使用成 run() 方法,使用这个方法会被最先执行,而且在主线程使用,就是一个简单的方法调用。

使用 start():


  
  1. public class MyThread extends Thread{
  2. @Override
  3. public void run() {
  4. System.out.println( "我是"+ Thread.currentThread().getName());
  5. }
  6. public static void main( String[] args) {
  7. MyThread myThread = new MyThread();
  8. myThread.start();
  9. //主线程
  10. System.out.println( "我是"+ Thread.currentThread().getName());
  11. }
  12. }
  13. //输出:
  14. 我是main
  15. 我是 Thread -0

使用run():


  
  1. public class MyThread extends Thread{
  2. @Override
  3. public void run() {
  4. System.out.println( "我是"+ Thread.currentThread().getName());
  5. }
  6. public static void main( String[] args) {
  7. MyThread myThread = new MyThread();
  8. myThread.run();
  9. //主线程
  10. System.out.println( "我是"+ Thread.currentThread().getName());
  11. }
  12. }
  13. //输出:
  14. 我是main
  15. 我是main

我们写的无论是Thread的子类还是Runnable的实现类,只是给线程启动的“程序”。所以,同一个程序,可以启动多个线程。

示例:


  
  1. public class MyThread extends Thread{
  2. @Override
  3. public void run () {
  4. System.out.println( "我是" + Thread.currentThread().getName());
  5. System.out.println( "我的id是" + Thread.currentThread().getId());
  6. }
  7. public static void main (String[] args) {
  8. MyThread t1 = new MyThread();
  9. t1.start();
  10. MyThread t2 = new MyThread();
  11. t2.start();
  12. MyThread t3 = new MyThread();
  13. t3.start();
  14. MyThread t4= new MyThread();
  15. t4.start();
  16. }
  17. }

输出:

 

注意:每次输出的结果不是固定的哦 

线程和方法调用栈的关系

每个线程都有自己独立的调用栈


  
  1. public class Add {
  2. public static int add(int a,int b){
  3. return a + b;
  4. }
  5. }
  6. public class MyThread extends Thread{
  7. @Override
  8. public void run() {
  9. System. out.println(Add. add( 5, 5));
  10. }
  11. }
  12. public class Main {
  13. public static void main(String[] args) {
  14. MyThread t = new MyThread();
  15. t.start();
  16. // 自己在主线程中也去调用 add 方法
  17. System. out.println(Add. add( 10, 20));
  18. }
  19. }

 打上断点,右键选择 Thread。调试器中看到的方法调用栈每一行都是一个栈帧(frame),保存的就是运行方法时的临时数据,对于我们来说最主要的就是局部变量。

由于每个线程都是独立的执行流,A在调用过哪些方法,和B根本就没关系。表现为每个线程都有自己的独立的栈。上面是主线程的,下拉框可以选择别的线程


 

根据程序=指令+数据,此处我们调用的是同一个方法,说明执行的是同一批指令——把两个数相加并返回
因为栈不同(帧不同) :说明执行指令时,要处理的数据是不同。所以就可独立的对主线程和子线程,进行调试,二者互不影响。

理解栈和栈帧

在没有多线程和外部输入的情况下,程序的运行其实就是一个状态机


 在函数调用过程中,会产生新的状态,最后返回的结果依然是最初的状态,只是数据发生了变化

如果发生了多次嵌套调用,像下面这样:

可以看到,这些框出现的顺序是 先进后出 因此,我们就可以使用栈去维护这些框。
栈:当前执行流的当前时刻(时间停止状态时)的状态框有哪些(现实方法的调用次序)
框:栈帧(frame)装的就是运行该方法时需要的一些临时数据(主要就是具备变量)

Thread的常见属性和方法

(1)常见属性

属性 获取方法
ID getId()
名称(name) getName()
状态 getState()
优先级

getPriority()

是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()
  • 本进程(JVM进程)内部分配的唯一的,只能 get不能set
  • 默认情况下,如果没有给线程名字,线程名字遵守Thread-..;第一个是Thread-0、Thread-1、 Thread-2,主线程默认为 main。可以get也可以set,也可以是重复的
  • 状态表示线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到。线程也可以set自己的优先级,但是要注意,优先级的设置,只是给JVM一些建议,不能强制让哪个线程先被调度。优先级最低为 1 ,最高为 10,默认为 5 
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了 线程的中断问题

设置name:


  
  1. public class SetNameThread extends Thread{
  2. //方法一
  3. // public SetNameThread(){
  4. // setName("我是张三");
  5. // }
  6. //方法二
  7. // public SetNameThread() {
  8. // super("我是张三"); // 调用父类(Thread)的构造方法
  9. // }
  10. @Override
  11. public void run() {
  12. System.out.println( this.getName());
  13. }
  14. public static void main( String[] args) {
  15. System.out.println( Thread.currentThread().getName());
  16. SetNameThread t1 = new SetNameThread();
  17. //方法三
  18. t1.setName( "我是t1");
  19. t1.start();
  20. SetNameThread t2 = new SetNameThread();
  21. t2.setName( "我是t2");
  22. t2.start();
  23. }
  24. }

或者也可以创建有参构造来设置名字:


  
  1. public class SetNameThread extends Thread{
  2. public SetNameThread( String name){
  3. super(name);
  4. }
  5. @Override
  6. public void run( ) {
  7. System. out. println( this. getName());
  8. }
  9. public static void main( String[] args) {
  10. System. out. println( Thread. currentThread(). getName());
  11. SetNameThread t1 = new SetNameThread( "我是t1");
  12. t1. start();
  13. SetNameThread t2 = new SetNameThread( "我是t2");
  14. t2. start();
  15. }
  16. }

上面的方法一、二是该类将线程的名字全都设置为一致的。

(2)等待一个线程-join() 

有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,A到饭店吃饭,发现没带钱包,于是让B回家拿钱包,那么A就必须等B把钱包拿来之后才能结账走人。


  
  1. public class Main {
  2. private static class B extends Thread {
  3. @Override
  4. public void run( ) {
  5. // 模拟 B 要做很久的工作
  6. try {
  7. TimeUnit. SECONDS. sleep( 5);
  8. } catch ( InterruptedException e) {
  9. e. printStackTrace();
  10. }
  11. println( "B 说:我的任务已经完成");
  12. }
  13. }
  14. private static void println( String msg) {
  15. Date date = new Date();
  16. DateFormat format = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss");
  17. System. out. println(format. format(date) + ": " + msg);
  18. }
  19. public static void main( String[] args) throws InterruptedException {
  20. B b = new B();
  21. b. start();
  22. println( "A 自己先去吃饭");
  23. b. join();
  24. println( "A 说:B 给我把钱送来了,结账走人");
  25. }
  26. }
  27. //输出:
  28. 2022- 06- 21 17: 34: 46: A 自己先去吃饭
  29. 2022- 06- 21 17: 34: 51: B 说:我的任务已经完成
  30. 2022- 06- 21 17: 34: 51: A 说:B 给我把钱送来了,结账走人

可以发现,一定是等B拿来了钱包之后A还能结账走人。使用b.join(); 后, 目前的线程会阻塞,直到B运行结束才返回,这个时候B一定已经结束了。

同时join方法也有重载形式:

  • join(long millis)等待线程死去,但最多等millis毫秒
  • join(long millis, int nanos)时间单位更精确

(3)休眠当前线程

  • TimeUnit.SECONDS.sleep(秒数);——以秒为单位
  • Thread.sleep(秒数);——以毫秒为单位

让线程休眠,就是让当前线程从 运行 -> 阻塞,等待要求时间过去之后,线程从 阻塞 ->  就绪

(4)获取当前线程引用——Thread.currentThread()


  
  1. public class Main {
  2. public static void main(String[] args) {
  3. Thread thread = Thread. currentThread();
  4. System.out. println(thread. getName());
  5. }
  6. }
  7. //输出:main

(5)让线程让出CPU——yield( )

让线程从运行状态变为就绪状态


  
  1. public class YieldTest extends Thread{
  2. @Override
  3. public void run() {
  4. while ( true){
  5. System.out.println( "我是"+ Thread.currentThread().getName());
  6. }
  7. }
  8. public static void main( String[] args) {
  9. YieldTest t1 = new YieldTest();
  10. t1.setName( "张三");
  11. YieldTest t2 = new YieldTest();
  12. t2.setName( "李四");
  13. t1.start();
  14. t2.start();
  15. }
  16. }

观察运行结果我们能看到两个线程出现的概率是差不多的,如果这个时候,李四高风亮节,选择让出CPU,让我们看看结果如何


  
  1. public void run() {
  2. while ( true){
  3. System. out.println( "我是"+Thread.currentThread().getName());
  4. if(Thread.currentThread().getName(). equals( "李四")){
  5. Thread. yield();
  6. }
  7. }
  8. }

运行之后我们发现,基本上打印的都是张三,但不是完全没有出现李四哦。 

yield主要用于执行一些耗时较久 的计算任务时,为让防止计算机处于“卡顿”的现象,时不时的让出一些CPU资源,给OS内的其他进程。

(6)中断一个线程

方法一:deprecate 暴力停止,直接kill掉线程,目前基本上已经不采用了。原因是不知道该线程把工作进行的如何了

方法二:interrupt();A想让B停止,A给B主动发一个信号,B在一段时间里,看到了停止信号之后,就可以主动,把手头的工作做到一个阶段完成,主动退出。

那么B怎样感知到有人让它停止:

  1. 通过interrupted()方法,检查当前线程是否被终止,true:有人让我们停止false: 没人让我们停止
  2. B可能正处于休眠状态,JVM的处理方式是,以异常形式,通知B : InterruptedException

要注意,B具体要不要停,什么时候停,怎么停,完全自己做主

线程的状态

  • NEW: 安排了工作, 还未开始行动
  • RUNNABLE: 可工作的, 又可以分成正在工作中和即将开始工作。
  • BLOCKED: 排队等着其他事情
  • WAITING: 排队等着其他事情
  • TIMED_WAITING: 排队等着其他事情
  • TERMINATED: 工作完成了

 线程状态只能获取,不能设置。

前台线程和后台线程

前台线程一般是做一 -些有交互工作的,后台线程又称精灵线程或者守护线程,一般是做一 些支持工作的线程。

比如:一个音乐播放器1.线程响应用户点击动作(前台)2.线程去网络上下载歌曲(后台)

要注意:当所有的前台线程都退出了,JVM进程就退出了,即使后台线程还在工作,也正常退出

JDK中自带的观察线程的工具——jconsole

该工具可以用来观察JVM运行中的一些相关情况,比如内存、类加载情况、线程情况

打开后找到idea启动的线程名称,双击进去,就可以观察了。


转载:https://blog.csdn.net/m0_58672924/article/details/125367471
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场