【基础】线程的创建、常见属性、状态

本文详细介绍了线程与进程的区别,强调线程在资源管理和执行效率上的优势。讨论了线程的创建、运行、状态转换、中断和阻塞操作,以及如何通过JavaAPI进行线程管理。还提醒了线程数量过多可能影响效率,并介绍了使用jconsole工具检查线程执行的情况。
摘要由CSDN通过智能技术生成

前言

本文主要介绍的是【线程】相关内容,注意也要掌握【线程】相关代码的书写以及运行分析。

一、【进程】回顾

虽然多进程已经实现了并发编程,但是存在重要的问题:假如针对每一个客户端都分别创建进程,那么就会造成频繁创建/销毁进程,使得多进程就比较低效。

进程创建步骤:
① 创建PCB;
② 给进程分配资源(内存/文件),然后赋值到PCB中;
(这对操作系统来说是不太容易的,比较消耗时间)
③ 把PCB插入链表

进程销毁步骤:
① 把PCB从链表上删除;
② 把PCB中持有的资源释放;
③ 销毁PCB

二、线程Thread

  1. 线程Thread
    ① 线程是被包含在进程中的。
    ② 一个进程默认会有一个线程,当然也可以有多个线程; 每个线程都是一个单独的“执行流”,可以单独在CPU上进行调度。
    ③ 同一个进程中的这些线程 共用同一份系统资源(内存+文件)

  2. 线程又被称为“轻量级进程”
    理由:创建线程的开销比创建进程小;且销毁线程的开销比销毁进程小。

  3. 使用线程:
    ① 能够更充分利用多核CPU,能够提高效率;
    ② 在同一个进程中,只是创建第一个线程的时候需要申请资源,后续再创建新的线程都是共用同一份资源(节省了申请资源的开销); 销毁线程的时候,只有销毁到最后一个线程的时候才真正释放资源,前面的线程销毁都不是真正的释放资源。

  4. 操作系统的内核是通过一组PCB来描述一个进程的,每个PCB对应一个线程;
    ① 这组PCB上的内存指针和文件描述符表其实都是一样的,是共用的;
    ② 而状态、上下文、优先级以及记账信息则是每个PCB(每个线程)自己有一份。
    所以: 进程是 资源分配 的基本单位; 线程是 调度执行 的基本单位。

  5. 重要面试题: 谈谈进程和线程之间的区别【高频】(面试必考!背!!)

    答:① 进程包含线程;
    ② 线程比进程更轻量,创建更快、销毁也更快;
    ③ 同一个进程的多个线程之间共用一份内存和文件资源,而进程和进程之间则是独立的文件和内存资源;线程共用资源就省去了线程分配资源的过程
    ④ 进程是资源分配的基本单位,线程是调度执行的基本单位

  6. 多线程的线程数目不是越多越好,因为CPU核心数是有限的。当线程数目达到一定数量的时候,CPU的核心就已经被吃满了,此时再增加线程也无法提高效率了;反而会因为线程太多,线程调度开销太大而影响了效率。

  7. 现代的CPU一般都有“超线程技术”,如一个核心可以并行跑两个线程

  8. 如果两个线程同时修改同一个变量,也容易产生“线程不安全”问题。

  9. 如果某个线程发生了意外(出现异常),并且该异常没有处理好就可能导致这个进程崩溃,此时后续的所有线程都难以运行。

  10. 线程的一些相关操作在操作系统中都提供了一系列的API。
    (因为系统原生的线程的API是C语言的,所以java就把这些API给封装成java风格的了)

  11. 多个线程的执行是“抢占式执行”;线程是独立的执行流。

  12. 多线程的当前程序确实是跑起来了,但是如果想要查看是不是多个线程同时执行的,就可以借助jdk中的jconsole工具来进行查看

    jconsole双击打开–> 本地进程–> 选择刚才运行的java进程(双击)–>
    选择“不安全连接”(如果双击打开了jconsole之后啥都没有,则尝试 右键-以管理员方式运行)

    ① 有的机器上可能权限管理比较严格,导致jconsole查看不到进程信息,所以就使用“管理员权限运行”
    ② 打开查看线程的时候会发现除了自己运行的N个线程之外;还有别的线程,这是JVM自己创建的内置功能,有些是负责垃圾回收的、有些是用来辅助完成调试的、还有些是监控进程是否收到一些特殊信号并进行动作的
    ③ 这里的状态是java自己搞的状态,和操作系统中的PCB里的状态还不太一样;
    此处的堆栈跟踪 :描述了当前线程、调用栈是啥样的;是方法之间相互调用的一个关联关系

三、创建线程

  1. 创建线程的办法
    ① 继承Thread类,重写run方法,run方法是新线程的入口
    ② 实现Runnable接口,重写run
    ③ 使用匿名内部类,实现创建Thread子类的方式
    ④ 使用匿名内部类,实现 实现Runnable接口的方式
    ⑤ 使用Lambda表达式(lambda本质上是一个“匿名函数”)
    (其实lambda表达式一般用于一个方法的实现上,该方法可以作为参数传入)
    线程的创建不不止这5中,还有两种后续介绍。

    【面试题:java中有哪些方式来创建线程?】

    (注:在实际工作中,在写代码时使用下划线将数字进行分割是一个有效技巧)

  2. 创建线程的相关事宜

    // 创建MyThread实例:创建实例并不会在系统中真的创建一个线程!!
    MyThread myThread = new MyThread();
    // 调用 start方法的时候才是真正创建出一个新的线程!!!
    // 而此时新的线程就会执行run里面的逻辑,一直到run里的代码执行完新的线程才是运行结束。
    myThread.start();
    // 调用 start方法 其实是另外启动了一个线程来执行Thread中的run方法!
    // 新的线程是一个单独的执行流,和现有线程的执行流不相关。 并发(并发+并行)执行
    
    // 如果main方法里面没有Thread子类创建的实例对象调用start方法,则调用的其实是main里面自带的默认线程(也叫 主线程)
    // main主线程(JVM创建)和MyThread创建出来的新线程是一个“并发执行”的关系。(但是此处的并发其实是指:并发+并行)
    // 并发执行:两边同时执行,各自安好、互不干扰!
    // MyThread的线程中run是该线程的入口方法
    
    
  3. 阻塞等待

    t1.start();
    t2.start(); // 此处才创建线程
    
    // 阻塞等待线程结束 join
    // 在main中调用 t1.join(); 的效果其实就是 让main线程阻塞,一直到t1执行完run后main才继续执行!!!
    // 在main中调用 t2.join(); 的效果其实就是 让main线程阻塞,一直到t2执行完run后main才继续执行!!!
    // 注意:t1和t2是并发执行的(宏观上是同时执行的),而不是先执行完t1再执行t2!!
    
    try { // 抛异常
        t1.join();
        t2.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    

    ① 使用两个线程消耗的时间不一定是使用一个线程消耗的时间的50%。因为:其实两个线程的并发执行=并行+并发,在执行过程中,并行(其实会变快)和并发(总时间没有减少,反而会因为线程切换而调度开销变长)执行次数不确定
    创建线程的实例的过程也是有开销的:串行执行没有额外创建线程,并发执行就需要额外创建线程。
    如果计算量大、计算的久,创建线程的开销就不明显;但是如果计算量小、计算的快,创建线程的开销影响较大,多线程提升效率就不明显,反而可能会降低执行效率

THINK

重点掌握

  1. 线程的创建以及运行
  2. 面试题:进程和线程的区别
  3. 面试题:java中有哪些方式来创建线程

四、Thread类及主要方法

  1. 【复习】进程包含了线程,一个线程对应一个PCB,一个进程对应一组PCB(内存指针和文件描述符表都是相同的一份,但是状态、优先级、上下文、记账信息是每个线程独立的)
    (一个进程对应一组PCB:pid其实是不一样的,但是属性tgrp_id是相同的)
  2. 每个线程都有一个唯一的 Thread 对象与之关联。

1. Thread常见构造方法

  1. 可以在创建线程的时候给线程起名字(这个名字是允许重复的),目的就是为了方便程序员来调试。
    如果不手动起名字,默认JVM会按照thread-0、thread-1、…这样子起名字,但是可读性不好。

  2. Thread常见的构造方法

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UuAXDA6S-1679473678226)(E:\Users\yu\Desktop\数据结构\多线程.assets\b05b22aab0a44d92a07aed541b3bee8d.png)]

示例:

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

2. Thread的几个常见属性

  1. Thread的常见属性
    在这里插入图片描述

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

  2. 常见属性相关介绍
    ① getId() 是java给Thread对象安排的身份标识,和操作系统内核中的PCB的pid以及和操作系统提供的线程API中的线程id都不是一回事儿。
    ② 身份标识可以有多个,在不同的环境下使用不同的标识。

    一个线程:
    ①在JVM中有一个id
    ②在操作系统的线程API中有个id
    ③在内核PCB中还有个id

(但是这些id效果其实是一样的,只是在不同环境中使用)

​ ③ 咱们默认创建的线程是“前台线程”,前台线程会阻止线程退出
​ 如:如果main运行完了而前台线程还没运行结束,则进程不会退出。
​ 如果是后台线程,后台线程是不阻止进程退出的。
​ 如:如果main等其他的前台线程执行完了,此时即使后台线程没有执行完进程也会退出(主线程main也是前台线程)
(前台线程:一般是重要的线程,也就是数据较为重要,一定要完成才能够结束的那种。如:转账)
​ ④ 可以使用setDaemon(true)来设置为后台线程。—— 在start之前设置!! 一旦程序启动就没办法设置。
​ ⑤ isAlive() 是判断线程是否存活 ,也就是判断内核中的线程是否还存在
​ Thread对象虽然和内核中的线程是一一对应的关系,但是生命周期并非完全相同。

​ Thread对象已经创建后内核里的线程还不一定有,调用start方法后内核线程才有;当内核线程执行完了(run运行完了),内核的线程就销毁了,但是Thread对象还在。

3.线程启动

  1. 调用start才会真正创建线程,不调用start就没有创建线程(注意这里指的是在内核里创建PCB)
  2. 调用 start 方法, 才真的在操作系统的底层创建出一个线程。
  3. 注意理解:run和start的区别==【经典面试题】==
    答:① 直接调用run并没有创建线程,只是在原来的线程中运行代码,只是相当于调用方法
    ② 调用start则是创建了线程,在新线程中执行代码(和原来的线程是并发执行的(并发+并行))

4.线程中断

  1. 线程的中断
    run方法执行完则线程结束,那么有没有办法让线程提前结束呢?
    ——就是通过线程中断的方式来进行的(本质上仍然是让run方法尽快结束,而不是让run执行一半就强制结束)

  2. 方法:① 直接自己定义一个标志位,作为线程是否结束的标志。
    ②使用标准库里面自带的一个标志位

  3. interrupt方法的行为有两种情况:①t线程在运行状态会设置Thread.currentThread().isInterrupted() 为true
    (注:public static Thread currentThread();
    // 返回当前线程对象的引用,在哪个线程里调用得到的就是哪个线程的引用)
    ②t线程在阻塞状态(sleep)不会设置标志位,而是触发一个InterruptedException异常,这个异常会把sleep提前唤醒开始运行; 所以要想顺利结束运行就加一个break;

  4. 中断线程,目前常见的有以下两种方式:
    1) 通过共享的标记来进行沟通(也就是自定义标记)
    2) 调用 interrupt() 方法来通知

    1)自定义标记:
    需要给标志位上加 volatile 关键字(不加也是ok的)
    如: public volatile boolean isQuit = false;

​ 2)调用 interrupt() 方法来通知:
​ ① 使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位
在这里插入图片描述
​ ② 使用 thread 对象的 interrupted() 方法通知线程结束

thread 收到通知的方式有两种:

① 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码 的写法.。可以选择忽略这个异常, 也可以跳出循环结束线程。
② 否则,只是内部的一个中断标志被设置,thread 可以通过 Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。

5.在java中,中断线程并不是强制的,而是线程自身的代码来进行判定处理的。

线程自身能怎么处理呢?
①立即结束线程; ②不理会; ③稍后理会 【也就是说取决于代码么写】

5.线程阻塞

  1. 阻塞等待join:
    线程之间的调度顺序是不确定的,可以通过一些特殊的操作来对线程的执行顺序作出干预。如join就是一个方法来控制线程的结束顺序。

  2. 在main中调用t.join效果就是:让main线程阻塞等待,等到t执行完了main才继续执行。

  3. 【java中的多线程方法,只要这个方法会阻塞都可能抛出InterruptedException异常】

  4. 【如果是调用join之前t线程就已经结束了,此时main线程还需要等待吗?不需要!】

  5. join有带时间版本:等待,但并不是无限等待
    实际开发中,很少使用无线等待;大多数都指定了最大等待时间,避免程序因为死等导致“卡死”情况。

    join相关方法
    在这里插入图片描述

6.线程休眠

  1. sleep指定休眠时间,让线程休息一会儿(阻塞一会儿)。
  2. 操作系统管理这些线程的PCB的时候是有多个链表的。
    • 就绪队列的PCB才会参与线程调度,而阻塞队列中的PCB暂时不会参与线程的调度。
    • 当就绪队列中的PCB sleep之后进入阻塞队列,当时间达到的时候该PCB又从阻塞队列挪回就绪队列,但是**并不代表可以立即能够上CPU运行,**还得看系统啥时候调度到该CPB。
  3. 所以:sleep(N); 并不一定是真的只休眠了N ms,一般是要略大于N的,具体时间看调度的时间开销。 因为:从阻塞到就绪之后不一定CPU就直接调度到该PCB!
  4. 【所以,为了解决上述调度的等待调度时间的不可预知性,就出现了“实时操作系统”,该操作系统的特点就是:任务调度的开销是可预期的,等待调度时间在可控范围内。
    (但是做到实时,是需要付出一些代价的) 如:知名实时操作系统:vxworks、wind river】
    sleep相关方法:(了解)
    在这里插入图片描述

五、线程状态

  1. Java中有自带的状态,但是觉得不是特别合适;所以又自己搞了一套状态,具体如下:
    ① NEW:Thread 对象创建出来了,但是内核的PCB还没有创建,也就是说:还没有真正创建线程。
    ② TERMINATED:内核的PCB销毁了,但是Thread 对象还在。
    ③ RUNNABLE:就绪状态(正在CPU上运行+在就绪队列中排队)
    ④ TIMED_WAITING:按照一定的时间进行阻塞。 调用sleep、join这类带时间的都是TIMED_WAITING
    ⑤ WAITING:特殊的阻塞状态,调用wait等
    ⑥ BLOCKED:等待锁的时候进入的状态
    (其实整体来说,状态就是:就绪+ 阻塞(分成三种具体情况了④⑤⑥))

  2. 状态转换

在这里插入图片描述

  1. .补充:
    ① RUNNABLE:(运行+排队)可被服务的状态,是否开始服务,则看调度器的调度
    ② isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。
    ③ Thread. yield() 大公无私,让出 CPU。
    yield 不改变线程的状态, 但是会重新去排队。

CKED:等待锁的时候进入的状态
(其实整体来说,状态就是:就绪+ 阻塞(分成三种具体情况了④⑤⑥))

  1. 状态转换

    [外链图片转存中…(img-8UuMEkRL-1679473678230)]

  2. .补充:
    ① RUNNABLE:(运行+排队)可被服务的状态,是否开始服务,则看调度器的调度
    ② isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。
    ③ Thread. yield() 大公无私,让出 CPU。
    yield 不改变线程的状态, 但是会重新去排队。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值