• 多线程的学习第二篇


    previewfile_28144258300213

    多线程

    线程是为了解决并发编程引入的机制.
    线程相比于进程来说,更轻量
    ~~ 更轻量的体现:

    • 创建线程比创建进程,开销更小
    • 销毁线程比销毁进程,开销更小
    • 调度线程比调度进程,开销更小

    进程是包含线程的.
    同一个进程里的若干线程之间,共享着内存资源和文件描述符

    每个线程被独立调度执行.每个线程都有自己的状态/优先级/上下文/记账信息

    进程是操作系统资源分配的基本单位
    线程是操作系统调度执行的基本单位


    Thread类 ~~ 五种创建线程的写法

    1.继承Thread,重写run
    2.实现Runnable,重写run
    3.使用匿名内部类,继承Thread
    4.使用匿名内部类,实现 Runnable
    5.使用lambda表达式

    使用了不同的方式来描述, Thread里的任务是什么.
    上述办法,只是语法规则不同,本质上都是一样的方式.
    ~~ 这些方法创建出来的线程,都是一样的.

    对Thread的run方法和start方法的区别
    run ~~ 描述了线程要做的工作.
    start ~~ 真的在操作系统内核里弄了个线程,并且让新线程调用run方法.


    Thread的用法

    Thread 的常见构造方法

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

    Thread(Runnable target, String name)
    Thread(String name)
    这2个方法的参数String name
    => 给线程起个名字 ~~ 为了方便调试
    线程默认的名字,叫做thread-0之类的…
    thread-1, 2, 3
    为了加深印象,写个线程名字为myThread的代码,通过 JConsole 观察线程的名字

    public class ThreadDemo6 {
        public static void main(String[] args) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    while(true){
                        System.out.println("hello");
                    }
                }
            },"myThread");
            t.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    image-20230920004942382

    这图中没有主线程的存在 ~~ 主线程执行完了start之后,紧接着就结束了main方法.
    对于主线程来说, main方法完了,自己也就没了.
    myThread线程也是如此,run方法执行完了,此时线程也就没了.
    图示:
    image-20230920010312996

    两个线程在微观上,可能是并行(两个核心),也可能是并发的(一个核心)
    宏观上感知不到(应用程序这里),咱们看到的始终是随机执行,抢占式调度…
    操作系统决定到底怎么使用CPU, 我们是不知道的.

    Thread的几个常见属性

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

    getId(): 获取到线程的ID, ID 是线程的唯一标识,不同线程不会重复
    getName(): 获取到构造方法里的名字
    getState(): 获取到线程状态, 状态表示线程当前所处的一个情况(Java里线程的状态要比操作系统原生的状态更丰富一些)
    getPriority(): 优先级,这个不仅可以获取,还可以进行设置,优先级高的线程理论上来说更容易被调度到 ,但是呢,实际上起不到什么作用!!!
    isDaemon(): 是否是“守护线程”,也可以叫做是“后台线程” ~~ 相对应的还存在“前台线程”这个概念.
    ~~ 玩手机的时候,手机上一般都会运行多个APP,在运行多个APP的时候,你打开微信,来聊天,此时微信就处在‘’前台‘’的状态.然后,你想打游戏,就切到王者荣耀的游戏界面.这时,微信就跑到了‘’后台‘’.
    前台线程: 会阻止进程结束, 前台线程的工作没做完, 进程是不会结束的
    后台线程: 不会阻止进程结束, 后台线程工作没做完, 进程也是可以结束的
    代码里手动创建的线程,默认都是前台线程,包括 main 默认也是前台的.
    其他的 JVM 自带的线程都是后台的
    ~~ 也可以使用 setDaemon 设置成后台线程.
    是“后台线程”,就是‘’守护线程‘’.

    注:要把线程是否是后台,和线程调度切换,区分开,两者不是一回事.
    线程的后台前台,只是取决于你调用的setDaemon这个方法做了什么…
    image-20230920132218590

    isAlive(): 在真正调用start之前,调用t.isAlive 就是 false.调用start之后, isAlive就是 true.
    在真正调用start之前,调用t.isAlive 就是 false.调用start之后, isAlive就是 true.
    ~~ 另外,如果内核里线程把 run千完了,此时线程销毁, pcb随之释放,
    但是 Thread t这个对象还不一定被释放的,此时isAlive 也是 false.

    image-20230920184520089

    Thread t这个对象还不一定被释放的,此时isAlive 也是 false的例子:
    image-20230920191341931

    如果t的run还没跑, isAlive 就是false
    如果t的run正在跑, isAlive 就是true
    如果t的run跑完了, isAlive 就是false

    时刻牢记!!线程之间是并发执行的,并且是抢占式调度的.

    image-20230920195527226

    线程调度,就好比,顾客们去餐馆吃饭一样.
    如果把CPU 想象成餐馆,每个线程就是来餐馆吃饭的人
    假设ABCD这四个人是天天来的常客 工
    但你无法确定,今天来的这四个人的顺序是ABCD,还是DCBA,还是BADC……
    由于每个人的行为之间都是独立的.A这个来的时间和BCD是没关系的.
    即使假设,按照ABCD顺序来了.如果这四个人来的时间差很多,是一个接一个来的,此时也无法保证,上菜的顺序也是ABCD……. => 充满了完全不可预期的因素

    以前写代码,只要读懂代码的顺序,就是固定按照从上到下的顺序来执行的…
    但是现在不是了,理解多线程代码,要考虑无数种顺序.

    中断一个线程

    中断的意思是,不是让线程立即就停止,而是通知线程,你应该要停止了.
    是否真的停止,取决于线程这里具体的代码写法.
    例子: 暑假在家的时候,你正在打王者,突然,你妈喊你下楼去买包盐,
    你的选择如下:
    1.放下游戏,立即就去.
    2.打完这把,再去,稍后处理.
    3.假装没听见,就完全不处理.
    不同的选择,就对应不同的结果

    停止线程的方式

    目前常见的有以下两种方式:

    1.使用标志位来控制线程是否要停止.

    示例代码

    public class ThreadDemo8 {
        public static boolean flag = true;
        public static void main(String[] args) {
            Thread t = new Thread(()->{
                while (flag){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
    
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //在主线程里就可以随时通过 flag变量的取值,来操作t线程是否结束.
            flag = false;
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    运行结果

    image-20230920214459036

    这个代码之所以能够起到,修改flag, t 线程就结束
    => 完全取决于t 线程内部的代码.代码里通过flag控制循环.
    这个线程是否要结束,啥时候结束都是线程内部自己代码来决定的.
    缺陷
    自定义变量这种方式不能及时响应.尤其是在sleep 休眠的时间比较久的时候

    2.使用Thread自带的标志位进行判定

    ~~ 它是可以唤醒上面的这个sleep这样的方法的.
    示例代码

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

    !Thread.currentThread().isInterrupted()
    它是Thread类的静态方法,通过这个方法可以获取到当前线程.
    哪个线程调用的这个方法,就是得到哪个线程的对象引用. ~~ 很类似于this
    在博主所写的上述代码中,是在t.run中被调用的.此处获取的线程就是t线程.

    isInterrupted() ~~ 前面加了个‘!,表示逻辑取反.
    为true表示被终止.
    为false表示未被终止.(应该要继续走)

    t.interrupt(); => 终止 t 线程

    ​ 注: isInterrupted() => 这个方法背后,就相当于是判定一个boolean变量
    t.interrupt() => 就是在设置这个 boolean 变量

    ​ main 线程调用 t.interrupt() 相当于main通知 t 线程你要终止了.

    方法说明
    public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位
    public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
    public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位

    image-20230920234340825

    image-20230921000821986

    为啥 sleep要清除标志位?
    唤醒之后,线程到底要终止,还是不要到底是立即终止还是稍后,就把选择权
    交给程序猿自己了!!!

    等待一个线程

    ~~ join()
    线程是一个随机调度的过程.
    等待线程,做的事情,就是在控制两个线程的结束顺序.
    示例代码

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

    运行结果

    image-20230921004505244

    画图对代码进行解释:

    image-20230921004425415

    主线程,等待 t 线程彻底执行完毕之后,才继续往下执行了

    假设开始执行join的时候, t 线程已经结束了,此时join 会咋样?
    如果是执行join的时候, t 线程已经结束了join不会阻塞,就会立即返回 => 保证线程的结束顺序

    方法说明理解
    public void join()等待线程结束无参数版本,“死等”,不见不散
    public void join(long millis)等待线程结束,最多等 millis 毫秒指定一个超时时间(最大等待时间) ~~ 这种操作方式是更常见的 ~~ 死等很容易出问题
    public void join(long millis, int nanos)同理,但可以更高精度不做注释

    文章总结 ❤️❤️❤️

    Thread 类
    标准库提供的,用来操作线程的类

    1. 创建线程
    2. Thread的一些属性和方法
    3. 终止线程
      isInterruptted => 判定, interrupt => 中断
      线程 A 只是通知线程 B 是否要终止, B 线程是否要终止, 看 B 线程有关代码的写法!!!
      为什么不设计成 A 让 B 终止, B 就立即终止呢?
      => A,B线程之间是并发执行的,随机调度的,导致B这个线程执行到哪里了, A线程是不知道的.
    4. 等待线程 ~~ join
      在线程t1中,调用线程t2.join,让线程t1等待线程t2结束,此时线程t1就会进入阻塞等待的状态.

    知识扩展

    多线程是实现并发编程的基础方式.比较接近系统底层,使用起来不太友好
    为了简化并发编程,也引入了很多更方便,更不容易出错的写法
    比如,erlang(编程语言),引入actor 模型.
    go,引入了csp模型
    js, 使用回调的方式 => async await
    python => async await
    ……

  • 相关阅读:
    001 ElasticSearch7.x 、IK分词器、Kibana 环境搭建、安装
    自动驾驶仿真:python和carsim联合仿真案例
    KafkaConsumer-Kafka从入门到精通(十)
    关于QGC Landing Pattern规划的计算过程
    C# + Oracel 批量插入数据(List,Array等)
    mysql中自定义变量(浅显易懂简洁版)
    元宇宙会给万亿市场的音乐产业带来哪些变化?
    MATLAB程序设计课后作业三、四
    以K近邻算法为例,使用交叉验证优化模型最佳参数
    Bug:elementUI样式不起作用、Vue引入组件报错not found等(Vue+ElementUI问题汇总)
  • 原文地址:https://blog.csdn.net/m0_73740682/article/details/133110251