• 多线程之一(进程理解、线程理解与创建、Thread类、线程状态)


    目录

    1. 进程理解

    1.1 进程管理

    1.2 PCB属性

     1.3 虚拟地址空间&进程间通信

    2.多线程理解(Thread)

    2.1 线程概念

    2.2 线程的作用

    2.3 进程和线程的区别(*)

    2.4 多线程需要注意的问题

    3. 线程创建

    3.1 创建线程(继承Thread类)

     3.2 Jconsole工具查看线程

     3.3 创建线程其他方法

    3.3.1 实现Runnable接口,重写run方法

    3.3.2 使用匿名内部类,实现创建Thread子类的方式

    3.3.3 使用匿名内部类,实现Runnable接口的方法

    3.3.4 lambda表达式

    4. Thread类的常见方法

    4.1 Thread常见构造方法 

     4.2 Thread的常见属性

     4.3 线程启动(start和run的区别*)

    4.4 线程中断(isInterrupted)

    4.5 线程等待(join)

    4.6 获取当前线程引用

    5. 线程状态


    1. 进程理解

    进程是操作系统对一个正在运行的程序的一种抽象,通俗的说,进程就是运行起来的程序;
    同一时刻系统中的进程,有很多,而操作系统就得给他们安排好,
    也就是 进程是操作系统进行资源分配的基本单位。

    1.1 进程管理

    进程管理 = 描述 + 组织

    描述:详细的表示一个进程中有哪些属性/信息,而结构体就是用来描述的;

    一个结构体对象对应一个进程,结构体也叫PCB(进程控制块)

    组织:通过一定的数据结构,将若干个用来描述的实体组织到一起,进行增删改查;

    系统通常会使用 双向链表 这样的结构来将PCB组织到一起。

    创建一个进程,本质上就是创建PCB,给进程分配资源赋值到PCB中,并且加入到链表上

    销毁一个进程,本质上就是从链表上删除对应的PCB结点,是否PCB持有的资源

    查看任务管理器的进程列表,本质上就是在遍历这个链表

    一个进程也可以是多个PCB,系统管理PCB的链表也可能是多个

    1.2 PCB属性

    (1)PID:进程的身份表示

    一个主机,同一时刻,进程的pid是唯一的(相当于身份证号),可以通过pid区分进程

    (2)内存指针:描述了这个进程使用的内存空间是哪个范围(分配空间范围)

    (3)文件描述符:描述了这个进程打开了哪些文件(文件就是存储在硬盘上的数据)

    下面几点都是和 “进程调度” 有关的

    进程调度就是通过 “并行” 和 “并发” 的方式,让计算机可以同时执行多个进程

    并行执行:每个CPU核心上,都可以独立运行一个进程

                      多个CPU核心上,就可以独立运行多个进程

    并发执行:一个CPU核心,先运行进程1,再运行进程2,再运行进程3...

                    宏观上,同时运行多个程序;微观上,同一时刻运行一个程序

    (4)进程状态: R(可执行状态)  S(可中断睡眠状态) X(退出状态)  D    T     Z

    阻塞状态的进程,无法被调度到CPU上执行

    就绪状态的进程,才可以在CPU上执行

    (5)进程优先级:优先给谁安排先执行

    系统调度的时候,就会根据优先级,来给进程安排执行时间

    创建进程时,也可以通过系统调用来干预优先级(相对的)

    (6)进程上下文:主要存储调度出CPU之前,寄存器的信息(把寄存器的信息保存到内存中)等到这个进程下次恢复到CPU上次执行时,将内存数据恢复到寄存器中

    通俗的说就是“读档”和“存档”

    进程在CPU上执行,要切换到别的进程,就需要保存当前运行结果的中间结果(存档),下次再到他执行时,就恢复之前的中间结果(读档),然后继续执行。

    (7)进程的记账信息:记录进程在CPU上执行时长

    再来辅助决定,进程是在CPU上继续执行,还是调度出去 

     1.3 虚拟地址空间&进程间通信

    为了不出现进程间互相影响,指针越界操作等情况,就需要让每个进程都有各自的内存空间,不让这些进程的活动范围重叠,给每个进程划分的内存空间,就叫做 “虚拟地址空间”(不是真实的物理内存地址),通过专门的设备MMU来完成虚拟地址,到物理之前的映射

    使用虚拟地址空间,就让进程间存在了 “隔离性”

    (一个进程不能直接访问另一个进程的内存数据,防止干扰操作,提高系统稳定性)

    但这样就会导致进程之间很难进行交互,为了解决这个问题又有了“进程间通信”

    进程间通信:核心原理就是,找多个进程都可以访问到的公共资源,然后基于公共资源进行数据交换。

    主流操作系统提供的进程通信机制有:文件、Socket(网卡) 、管道、消息队列、磁盘、信号量、信号

    2.多线程理解(Thread)

    2.1 线程概念

    一个线程就是一个执行流(按一定顺序执行的一组代码),
    每个线程之间都可以按照顺序执行自己的代码,多个线程之间“同时”执行多份代码

    同一个进程中的这些线程,共用一份系统资源(内存+文件)

    进程是 资源分配 的基本单位

    线程是 调度执行 的基本单位

    2.2 线程的作用

    首先是,因为“并发编程”可以更充分利用CPU

    其次,多进程虽然也可以实现并发编程,但线程比进程更轻量

    (创建、销毁、调度线程都比进程更快)

    所以,使用多线程:

    1.能够充分利用多核CPU,能够提高效率

    2.只是创建第一个线程时,需要申请资源,后续再创建新的进程,都是共用一份资源

    销毁线程时,也是销毁到最后一个的时候,才真正释放资源,前面的线程销毁,都不必真的释放资源

    最后,又有了更好的方法,“线程池”和“协程”

    2.3 进程和线程的区别(*)

    1.进程包含线程(多个)

    2.线程比进程更轻量(创建、销毁、调度更快)

    3.同一个进程的多个线程之间共用同一份内存/文件资源,

       进程和进程之间,则是独立的内存/文件资源

    4.进程是操作系统资源分配的基本单位

       线程是操作系统调度执行的基本单位

    2.4 多线程需要注意的问题

    1. 多线程,一定程度下,线程数目越多,效率越高;但CPU核心数是有限的,当线程数目多到一定程度的时候,CPU核心就被占满了,线程调度开销太大,反而会使效率变慢

    2. 当两个线程去争夺同一个资源是,此时两个线程就出现了问题,这就会使“线程不安全”

    3. 如果某个线程出现异常,并且异常没有处理好,此时就可能会使整个进程崩溃,此时其他线程也会运行出现问题


    3. 线程创建

    3.1 创建线程(继承Thread类)

    继承Thread类,重写run方法

    1. class MyThread extends Thread {
    2. @Override
    3. public void run() {
    4. System.out.println("thread!");
    5. }
    6. }

    标准库中,提供了一个Thread类,使用的时候就可以继承这个类

    Thread类,相当于是对操作系统中的线程进行的封装

    重写run方法,run是Thread父类中已有的方法,但是这里要重写

    run中的逻辑,就是这个线程要执行的工作


    创建子类,重写run方法,相当于“安排任务”

    1. public class Demo01 {
    2. public static void main(String[] args) {
    3. MyThread myThread = new MyThread();
    4. myThread.start();
    5. }
    6. }

    创建MyThread实例,并不会在系统中真的创建一个线程

    而是,调用start方法的时候,才可以真正创建线程

    新的线程才会执行run方法中的逻辑,直到run中逻辑执行完,新的线程才会运行结束

    1. class MyThread extends Thread {
    2. @Override
    3. public void run() {
    4. while(true) {
    5. System.out.println("hello thread!");
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. }
    13. }
    14. public class Demo01 {
    15. public static void main(String[] args) throws InterruptedException {
    16. MyThread myThread = new MyThread();
    17. myThread.start();
    18. while(true) {
    19. System.out.println("hello main");
    20. Thread.sleep(1000);
    21. }
    22. }
    23. }

    抢占式执行

     3.2 Jconsole工具查看线程

    (1)找到自己电脑上JDK安装路径中的bin—》jconsole.exe打开

     (2)点击本地进程,选择当前项目连接,连接成功后,就可以查看当前项目中的线程

     (3)选择查看线程信息

     3.3 创建线程其他方法

    3.3.1 实现Runnable接口,重写run方法

     创建了Runnable实例,将runnable实例作为参数传给线程

    将线程要干的工作 和 线程本身 分开了,使用Runnable来专门表示“线程要完成的工作”

    这种方法的好处是,降低了耦合性,如果以后要修改代码,改动相对较少

    只需要将修改后的Runnable传给其他的实体就可以了

    并且如果是多个线程干一样的工作,Runnable也更适合

    1. class MyRunnable implements Runnable {
    2. @Override
    3. public void run() {
    4. while(true) {
    5. System.out.println("hello thread!");
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. }
    13. }
    14. public class Demo02 {
    15. public static void main(String[] args) throws InterruptedException {
    16. MyRunnable runnable = new MyRunnable();
    17. Thread thread = new Thread(runnable);
    18. thread.start();
    19. while (true) {
    20. System.out.println("hello main!");
    21. Thread.sleep(1000);
    22. }
    23. }
    24. }

    3.3.2 使用匿名内部类,实现创建Thread子类的方式

    创建Thread的子类,同时实例化出一个对象

    1. public class Demo03 {
    2. public static void main(String[] args) {
    3. Thread thread = new Thread() {
    4. @Override
    5. public void run() {
    6. while(true) {
    7. System.out.println("hello thread!");
    8. try {
    9. Thread.sleep(1000);
    10. } catch (InterruptedException e) {
    11. e.printStackTrace();
    12. }
    13. }
    14. }
    15. };
    16. thread.start();
    17. }
    18. }

    3.3.3 使用匿名内部类,实现Runnable接口的方法

    匿名内部类的实例,作为构造方法的参数

    1. public class Demo04 {
    2. public static void main(String[] args) {
    3. Thread thread = new Thread(new Runnable() {
    4. @Override
    5. public void run() {
    6. System.out.println("hello thread!");
    7. try {
    8. Thread.sleep(1000);
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. }
    13. });
    14. thread.start();
    15. }
    16. }

    3.3.4 lambda表达式

    1. public class Demo05 {
    2. public static void main(String[] args) {
    3. Thread thread = new Thread(() -> {
    4. while (true) {
    5. System.out.println("hello thread!");
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. });
    13. thread.start();
    14. }
    15. }

    4. Thread类的常见方法

    4.1 Thread常见构造方法 

    方法说明
    Thread()创建线程对象
    Thread(Runnable target)使用Runnable对象创建线程对象
    Thread(String name)创建线程对象,并命名
    Thread(Runnable target, String name)使用Runnable对象创建线程对象,并命名

    1. public class demo01 {
    2. public static void main(String[] args) {
    3. Thread thread = new Thread(() -> {
    4. while (true) {
    5. System.out.println("hello");
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. },"这是线程名");
    13. thread.start();
    14. }
    15. }

    打开Jconsole.exe,可以看到线程的名字,就是我们自己命名出来的

     4.2 Thread的常见属性

    属性获取方法
    IDgetId()
    名称getName()
    状态getState()
    优先级getPriority()
    是否后台线程isDaemon()
    是否存活isAlive()
    是否被中断isInterrupted()

    (1)getId() 这个是java中给Thread对象安排的身份标识,和操作系统内核中PCB的pid,以及操作系统提供的线程API的线程id都不是一个

    (2)setDaemon() 判断是否是守护线程。默认创建的线程是“前台线程”,前台线程会阻止进程退出,如果main运行完,前台线程还没完,进程不会退出;如果是“后台线程”,后台线程不会阻止进程退出,如果main等其他的前台进程执行完,此时即使后台线程没执行完,也会退出。

    “只要有一个前台(main也是一个前台),进程就还活着,前台没了,后台也会没”

    设置操作得在start之前,如果线程启动,就不能修改 

    1. public class demo01 {
    2. public static void main(String[] args) {
    3. Thread thread = new Thread(() -> {
    4. while (true) {
    5. System.out.println("hello");
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. },"这是线程名");
    13. //使用setDaemon设置为后台线程
    14. //设置操作得在start之前,如果线程启动,就不能改
    15. thread.setDaemon(true);
    16. thread.start();
    17. System.out.println("main线程执行结束!");
    18. }
    19. }

    (3)isAlive() 判断内核中线程是否存活。Thread对象,虽然和内核中的线程,是一一对应关系,但是生命周期并非完全相同;Thread对象存在了,内核里的线程不一定有,调用start方法,内核线程才存在。当内核里的线程执行完(run运行完),内核的线程就销毁了,但是Thread对象还在

    (4)写一段代码看看Thread属性 

    1. public class Demo02 {
    2. public static void main(String[] args) {
    3. Thread thread = new Thread(() -> {
    4. while(true) {
    5. System.out.println("hello thread");
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. },"我的线程");
    13. thread.start();
    14. System.out.println(thread.getId());
    15. System.out.println(thread.getName());
    16. System.out.println(thread.getPriority());
    17. System.out.println(thread.getState());
    18. System.out.println(thread.isDaemon());
    19. System.out.println(thread.isAlive());
    20. System.out.println(thread.isInterrupted());
    21. }
    22. }

     4.3 线程启动(start和run的区别*)

    调用start才会真正创建线程,不调用start就没创建线程(在内核里创建PCB)

    1. class MyThread extends Thread {
    2. @Override
    3. public void run() {
    4. System.out.println("hello thread!");
    5. }
    6. }
    7. public class Demo03 {
    8. public static void main(String[] args) {
    9. MyThread myThread = new MyThread();
    10. myThread.start();//创建一个新线程,调用run
    11. //myThread.run();//在当前线程调用run
    12. }
    13. }

    start 和run的区别(*)

    直接调用run,并没有创建线程,只是在原来的线程中运行代码

    调用start,则是创建了线程,在新线程中执行代码(和原来的线程是并发的)

    作用功能不同:

    1. run方法的作用是描述线程具体要执行的任务;
    2. start方法的作用是真正的去申请系统线程

    运行结果不同:

    1. run方法是一个类中的普通方法,主动调用和调用普通方法一样,会顺序执行一次;
    2. start调用方法后, start方法内部会调用Java 本地方法(封装了对系统底层的调用)真正的启动线程,并执行run方法中的代码,run 方法执行完成后线程进入销毁阶段。

    4.4 线程中断(isInterrupted)

    线程中断,本质上是让run方法尽快结束,而不是run执行一半,强制结束

    有两种方法

    (1)直接自己定义一个标志位,作为线程是否结束的标记

    1. public class Demo04 {
    2. private static boolean isQuit = false;
    3. public static void main(String[] args) {
    4. Thread thread = new Thread(() -> {
    5. while (!isQuit) {
    6. System.out.println("hello tread!");
    7. try {
    8. Thread.sleep(1000);
    9. } catch (InterruptedException e) {
    10. e.printStackTrace();
    11. }
    12. }
    13. System.out.println("thread线程执行完了!");
    14. });
    15. thread.start();
    16. try {
    17. Thread.sleep(3000);
    18. } catch (InterruptedException e) {
    19. e.printStackTrace();
    20. }
    21. isQuit = true;
    22. System.out.println("设置让thread线程结束!");
    23. }
    24. }

     (2)使用标准库中自带的标记位interrupt

    1. public class Demo05 {
    2. public static void main(String[] args) {
    3. Thread thread = new Thread(() -> {
    4. while (!Thread.currentThread().isInterrupted()) {
    5. System.out.println("hello tread!");
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. System.out.println("thread线程执行完了!");
    13. });
    14. thread.start();
    15. try {
    16. Thread.sleep(3000);
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. }
    20. thread.interrupt();
    21. System.out.println("设置让thread线程结束!");
    22. }
    23. }

     运行程序,发现出现了异常,这是因为

    interrupt方法的行为,有两种情况

    1.thread线程在运行状态会设置,标志位为true

     2.thread线程在阻塞状态(sleep)不会设置标志位(实际是interrupt会设置标志位,但是sleep/wait等这些阻塞方法,会清除这个标志位),而是触发一个InterruptedException

    这个异常会把sleep提前唤醒

    所以,要想顺利结束循环,由线程自身的代码进行判定处理

    线程自身可以(1)立即结束线程(break)

    (2)啥都不干

    (3)稍后处理(sleep,break

     

    4.5 线程等待(join)

    线程之间的调度顺序,是不确定的,可以通过join来控制线程之间的结束顺序

    方法说明
    join()等待线程结束
    join(long millis)等待线程结束,最多等millios毫秒
    join(long millis, int nanos)也可以等待更高精度的时间 毫秒+纳秒

     在main中,调用join效果就是,让main线程阻塞等待,等到thread执行完,main才继续执行

    1. public class Demo06 {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread thread = new Thread(() -> {
    4. for (int i = 0; i < 5; i++) {
    5. System.out.println("hello thread!");
    6. try {
    7. Thread.sleep(1000);
    8. } catch (InterruptedException e) {
    9. e.printStackTrace();
    10. }
    11. }
    12. });
    13. thread.start();
    14. System.out.println("main线程join之前");
    15. thread.join();
    16. System.out.println("main线程join之后");
    17. }
    18. }

    4.6 获取当前线程引用

    currentThread();   获取到当前这个线程对应的Thread对象的引用

    5. 线程状态

    NEW: 安排了工作 , 还未开始行动(Thread对象创建出来了,但是内核的PCB还没创建,(还没真正创建线程))
    RUNNABLE: 可工作的 . 又可以分成正在工作中和即将开始工作(就绪状态(正在CPU上运行+在就绪队列中排队))
    BLOCKED: 这几个都表示排队等着其他事情(等待锁的时候进入阻塞状态)
    WAITING: 这几个都表示排队等着其他事情(特殊的阻塞状态,调用wait)
    TIMED_WAITING: 这几个都表示排队等着其他事情(按照一定的时间,进行阻塞sleep)
    TERMINATED: 工作完成了 (内核的PCB销毁了,但是Thread对象还在)

     

    1. public class Demo07 {
    2. public static void main(String[] args) throws InterruptedException {
    3. Thread thread = new Thread(() -> {
    4. try {
    5. Thread.sleep(2000);
    6. } catch (InterruptedException e) {
    7. e.printStackTrace();
    8. }
    9. });
    10. //tread开始之前,得到的就是 NEW
    11. System.out.println(thread.getState());
    12. thread.start();
    13. Thread.sleep(50);
    14. //tread正在工作,得到的是 RUNNABLE
    15. System.out.println(thread.getState());
    16. thread.join();
    17. //tread结束之后,得到的状态是 TERMINATED
    18. System.out.println(thread.getState());
    19. }
    20. }

  • 相关阅读:
    008.分隔符、循环、比较
    WordPress 安装教程
    【编程题】【Scratch二级】2021.12 绘制多边形
    RPA的价值和优势有哪些?
    Cento7 Docker安装Zabbix,定制自定义模板
    Linux常用的命令
    python基础(Python高级特性(切片、列表生成式)、字符串的正则表达式、函数、模块、Python常用内置函数、错误处理)培训讲义
    【Es基础入门必看】
    数学建模学习笔记(7):相关系数
    【CANoe/CANalyzer脚本】通过CAPL发送NM帧报文测试网络管理
  • 原文地址:https://blog.csdn.net/m0_58761900/article/details/126423041