【多线程-从零开始-叁】线程的核心操作

news2024/9/29 16:13:52

一、创建一个线程-start()

startrun 的区别:(经典面试题

  • run 描述了线程要执行的任务,也可以称为“线程的入口”
  • 此处 start 会根据不同的系统,分别调用不同的 API,来执行系统函数,在系统内核中,创建线程(创建 PCB,加入到链表中)
  • 创建好的新的线程,再单独来执行 run
  • start 的执行速度一般是很快的(创建线程,比较轻量),一旦 start 执行完毕,新线程就会开始执行,调用 start 的线程也会继续执行(main 线程)(开始兵分两路,并发执行)
  • 调用 start 不一定非得是 main 线程调用,任何的线程都可以创建其他线程,如果系统资源充裕,就可以任意的创建线程(但线程不是越多越好)
public static void main(String[] args) {  
  
    Thread t = new Thread(() -> {  
        System.out.println("hello t");  
        Thread t2 = new Thread(() -> {  
            System.out.println("hello t2");  
        });        
        t2.start(); //hello t2  
    });    
    t.start();  //hello t
}

注意:

  • 先执行的是 main 创建的线程,因为 main 是程序执行入口
  • 所以先打印 hello t
  • 一个 Thread 对象,只能调用一次 start,多次调用就会报线程状态异常 image.png|583

线程的状态:

  • 由于 Java 中希望,一个 Thread 对象,只能对应到一个系统中的线程,因此就会在 start 中,根据线程状态做出判定。
  • 如果 Thread 对象是还没有调用 start 的,此时就是一个 NEW 状态,之后就可以顺利地调用 start
  • 如果 Thread 已经调用过 start,就会进入其他状态,
  • 只要不是 NEW 状态,接下来执行 start 都会抛出异常

二、线程的终止(中断)

线程的终止
B 正在执行,A 想让 B 结束

  • 其实核心就是 A 要想办法,让 B 的 run 方法执行完毕,此时 B 就自然结束了
  • 而不是说 B 的 run 执行一半,A 直接把 B 强制结束了

1. 通过共享的标记来进行沟通

public class Demo3 {  
    public static boolean isQuit = false;  
  
    public static void main(String[] args) throws InterruptedException {  
        boolean isQuit = false;  
        Thread t = new Thread(() -> {  
            while(!isQuit){  
                System.out.println("hello thread");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }  
            System.out.println("t 线程执行结束");  
        });  
        t.start();  
        Thread.sleep(2000);  
        //修改 isQuit 变量,就能狗影响到 t 线程的结束了  
        System.out.println("main 线程尝试终止 t 线程");  
        isQuit = true;  
    }  
}

通过改变变量,让 t 线程跳出了 while 循环,最终线程终止

变量捕获

  • lambda 表达式 / 匿名内部类的一个语法规则
  • isQuitlambda 定义在一个作用域中,此时 lambda 内部是可以访问到 lambda 外部(和 lambda 同一个作用)中的变量
  • 但是 Java 的变量捕获有特殊要求,要求捕获的变量得是 final / 事实 final(虽然没有 final 修饰,但没有修改)image.png|555
public class Demo3 {  
    //public static boolean isQuit = false;  
  
    public static void main(String[] args) throws InterruptedException {  
        boolean isQuit = false;  
        Thread t = new Thread(() -> {  
            while(!isQuit){  
                System.out.println("hello thread");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }  
            System.out.println("t 线程执行结束");  
        });  
        t.start();  
        Thread.sleep(2000);  
        //修改 isQuit 变量,就能影响到 t 线程的结束了  
        System.out.println("main 线程尝试终止 t 线程");  
        isQuit = true;  //会报错
    }  
}

isQuit 定义在 main 内部后

  • 由于 isQuit 变量前后由 false 被改为 true 了,所以会报错
  • 若将最后一行注释掉,此时 isQuit 就是一个事实 final(虽然没有 final 修饰,但没更改) ,isQuit 变量就会被捕获到,程序就不会报错

  • 上面写成成员变量,就可以正常修改访问,因为走的语法是“内部类访问外部类的成员”,和“变量捕获”无关
  • lambda 表达式本质上是一个“函数式接口”产生的“匿名内部类”,外面写的 “class Demo3“ 就是外部类

2. 调用 interrupt 方法来通知

  • 初始情况下 Thread 类里面有一个成员,boolean 类型的 interrupted
  • 初始情况下,这个变量是 false,未被终止,但一旦外面的其他线程,调用一个 interrupt() 方法,就会设置上述标志位
public class Demo4 {  
    public static void main(String[] args) throws InterruptedException {  
        Thread t = new Thread(() -> {  
            //先获取到线程的引用  
            Thread currentThread = Thread.currentThread();  
            while(!currentThread.isInterrupted()){  
                System.out.println("hello thread");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }        
        });        
        
        t.start();  
        
        Thread.sleep(1000);  
        //在主线程中,控制 t 线程被终止,设置上述标志位  
        //将其由 false 设为 true,意味着循环就结束了,线程也就结束了  
        t.interrupt();  
    }
}
  • currentThread()Thread 类的静态方法,可以获取到调用这个方法的线程的实例,哪个线程调用,返回的引用就只想哪个线程的实例,类似 this
  • lambda 的代码在编译器眼里,出现在 Thread t 上方,此时 t 还没有被定义,所以不能直接用 t 调用 isInterrupted() 方法

  • 由于判定 isInterrupted() 和执行打印,这两个操作太快了,因此整个循环,主要的时间都是花在 sleep
  • main 调用 Interrupt 的时候,大概率 t 线程正处于 sleep 状态中,此处 Interrupt 不仅仅能设置标志位,还能把刚才这个 sleep 操作给唤醒
  • 比如,当还在 sleep 的时候,此时 Interrupt 被调用了,此时 sleep 就会被直接唤醒,并且抛出 InterruptedException 异常
  • 由于 catch 中的默认代再次抛出异常,但没人 catch,最终就到了 JVM 这一层,进程就直接异常终止了image.png

综上: interrupt 不仅能设置标志位,还能唤醒 sleep

  • 只不过由于 catch 中生成的默认代码影响了执行的结果,导致我们看起来像整个进程都结束了

  • 为了排除干扰,我们可以把 catch 中的 throw 给干掉,换成一个打印“执行到 catch 操作”

public class Demo4 {  
    public static void main(String[] args) throws InterruptedException {  
        Thread t = new Thread(() -> {  
            //现货区到线程的引用  
            Thread currentThread = Thread.currentThread();  
            while(!currentThread.isInterrupted()){  
                System.out.println("hello thread");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    //throw new RuntimeException(e);   //屏蔽掉生成的默认代码干扰
                    System.out.println("执行到catch操作");  
                }            
            }        
        });  
        t.start();  
			  
        Thread.sleep(1000);  
        t.interrupt();  
    }
}

image.png|228

  • 从运行结果可以看到,此时调用 interruptsleep 唤醒了,触发异常,被 catch 住了。但是虽然 catch 住了,但是循环还在执行,看起来就像标志位没被设置一样

注意:

  • 首先,Interrupt 肯定会设置这个标志位的
  • 其次,当 sleep 等阻塞的函数被唤醒后,就会清空刚才设置的 Interrupted 标志位,下一轮循环判定的时候,就会认为标志位没有被设置,于是循环就会继续执行
    因此,如果确实想要结束循环,结束线程,就需要在 catch 中加上 return / break

清除标志位

  • 清除标志位这个操作可以让编写 B 线程的程序员有更大的操作空间
  • A 希望 B 线程终止,B 收到这样的请求之后,B 需要自行决定,是否要终止或立即执行还是稍后执行 ~~~ (B 线程内部的代码决定,与其他线程无关)
    • 如果 B 想无视 A,就直接 catch,啥都不做,B 线程仍然会继续执行。sleep 清除标志位,就可以使 B 做出这种选择,如果 sleep 不清除标志位的话,B 就势必会结束,无法写出让线程急速执行代码了
    • 如果 B 线程想立即结束,就直接在 catch 中写上 return / break,此时 B 线程就会立即结束
    • 如果 B 线程想要稍后再结束,就可以在 catch 中写上一些其他的逻辑(比如释放资源、清理一些数据、提交一些结果… 收尾工作)。这些逻辑完成之后,再进行 return / break
  • 这是 JVM 内部的逻辑,需要结合 JVM 的源码才能看到这个操作

三、等待一个线程-join()

操作系统针对多个线程的执行,是一个“随机调度,抢占式执行的过程”。因为我们写代码的时候不希望是随机,希望是确定,所以期望通过一些变成手段来对这里的随机进行干预

  • 线程等待就是在确定两个线程的“结束顺序”。因为我们无法确定两个线程调度执行的顺序,但是可以控制结束的先后顺序
  • 让后结束的线程等待先结束的线程即可
  • 此时后结束的线程就会进入阻塞,一直到先结束的进程真的结束了,阻塞才解除
  • 比如现在有两个线程 AB
  • A 线程中调用 B.join(),意思就是让 A 线程等 B 线程先结束,然后 A 再继续执行
  • 此处的 B 是被等待的一方
public class Demo5 {  
    public static void main(String[] args){  
        Thread t = new Thread(() -> {  
            for (int i = 0; i < 3; i++) {  
                System.out.println("这是线程 t");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }            
            System.out.println("线程 t 结束");  
        });        
        t.start();  
        //让主线程等待 t 线程  
        System.out.println("main 线程开始等待");  
        try {  
            t.join();   //执行到这里,main线程阻塞等待
        } catch (InterruptedException e) {  
            throw new RuntimeException(e);  
        }   
         
    	//当 t 线程执行结束后,join才会返回,main才会继续执    
        System.out.println("main 线程等待结束");  
    }
}

 //t 线程先结束,main 线程后结束
  • 上述代码是 main 先等待,然后 t 执行了半天才结束,此时 main 在阻塞。
  • 如果调整一下,让 t 先结束,然后 main 才开是 join,这个时候也不会出现阻塞,因为 t 线程已经结束了,而 join 就是用来确保被等待的线程先结束,若已经结束了,join 就不必再等待了

  • 任何的线程之间都是可以相互等待的,不是说必须得 main 线程等待别人
  • 任何线程,也不一定是两个线程之间,一个线程可以同时等待多个别的线程,或者若干线程之间也能相互等待
public class Demo6 {  
    public static void main(String[] args) throws InterruptedException {  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 2; i++) {  
                System.out.println("hello t1");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }            
            System.out.println("t1 结束");  
        });  
        
        Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 2; i++) {  
                System.out.println("hello t2");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }            
            }            
            System.out.println("t2 结束");  
        });        
        t1.start();  
        t2.start();  
        System.out.println("main 开始等待");  
        t1.join();  
        t2.join();  
        System.out.println("main 结束");  
    }
}
  • 此时两个 join 都是写在 main 里面的,所以都是 main 线程等待 t1t2t1t2 之间没有等待关系
  • 若在 t2 中写 t1.join,则 t2 需要等待 t1 线程先执行完

  • 上述 join 都是无参的,意思是“死等”,“不见不散”,被等待的线程,只要不执行完,就会持续阻塞
  • 上述死等操作,其实不是一个好的选择,因为一旦被等待的线程代码出现了一些 bug 了,就可能使这个线程迟迟无法结束,从而使等待线程一直阻塞而无法进行其他操作
  • 所以在实际开发中,都会设置一个“超时时间”,最多等多久,就是 join 的参数image.png

这个精度和操作系统相关, 像 Windows, Linux 系统,线程调度开销比较大;计算机中还有一类系统——“实时操作系统”,就能把调度开销尽可能降低,开销小于一定的误差要求,从而可以做到更精确。一般用于航空航天,军事… 不过实时操作系统是舍弃了很多功能换来的实时性

四、获取当前线程引用

想在某个线程中,获取到自身的 Thread 对象的引用,就可以通过 currentThread 来获取到

public class Demo7 {  
    public static void main(String[] args) throws InterruptedException {  
        
        Thread mainThread = Thread.currentThread();  
        
        Thread t = new Thread(()->{  
            //需要在 t 中调用主线程.join  
            System.out.println("t 线程开始等待");  
            try {  
                mainThread.join();  
            } catch (InterruptedException e) {  
                throw new RuntimeException(e);  
            }            
            System.out.println("t 线程结束");  
        });        
        t.start();  
        Thread.sleep(1000);  
        System.out.println("main 线程结束");  
    }
}
//打印顺序:
//t 线程开始等待
//main 线程结束
//t 线程结束
  • 任何线程中,都可以通过这样的操作,拿到线程的引用
  • 任何需要的时候,都可以通过这个方法来获取到

五、休眠当前线程

  • Thread.sleep 可以让调用的线程阻塞等待,是有一定时间的
  • 线程执行 sleep,就会使这个线程不参与 CPU 调度,从而把 CPU 资源让出来给别人使用
  • 也把 sleep 这种操作,称为“放权”,放弃使用 CPU 的权利
  • 有的开发场景中,发现某个线程的 CPU 占用率过高,就可以通过 sleep 来进行放权改善
  • 虽然 CPU 就是给程序用的,但是有的程序可能包含很多线程,这些线程之间是有“轻重缓急”的
  • 虽然线程的优先级就可以产生影响,但是优先级的影响是比较有限的,也可以通过 sleep 来更明显的影响到这里的 CPU 占用

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1980076.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

原生多模态跟GPT聊天部分测试,大家都用他来做什么;字节推出AI音乐产品-海绵音乐,可以媲美Udio和Suno

✨ 1: 跟GPT聊天 原生多模态跟GPT聊天部分测试&#xff0c;大家都用他来做什么。 说各个国家的语言&#xff0c;例如普通话&#xff0c;或者是广东话等。 ChatGPT担任富有激情的足球比赛解说员 使用新的高级语音模式 视觉&#xff0c;进行实时日语翻译&#xff01; 地址&…

Java编程规范 空格

public static void main(String[] args) { // 缩进4 个空格 String say "hello"; // 运算符的左右必须有一个空格 int flag 0; // 关键词if 与括号之间必须有一个空格&#xff0c;括号内的f与左括号&#xff0c;0与右括号不需要空格 if (flag 0) { System…

秃姐学AI系列之:模型选择 | 欠拟合和过拟合 | 权重衰退

目录 训练误差 泛化误差 验证数据集和测试数据集 验证数据集 Validation Dataset&#xff1a; 测试数据集&#xff1a; K-则交叉验证 总结 过拟合和欠拟合 模型容量 模型容量的影响 估计模型容量 数据复杂度 总结 权重衰退 weight decay 使用均方范数作为硬性…

【八】Zookeeper3.7.1集成Hadoop3.3.4集群安装

文章目录 1.基本原理2.下载并解压ZooKeeper3.配置环境变量4.配置ZooKeeper5.创建数据目录并初始化myid6.启动ZooKeeper7.配置ZooKeeper集成到Hadoop8.重启Hadoop9.ZooKeeper状态检查 1.基本原理 ZooKeeper 是一个分布式协调服务&#xff0c;用于分布式系统中管理配置信息、命名…

51单片机—智能垃圾桶(定时器)

一. 定时器 1. 简介 C51中的定时器和计数器是同一个硬件电路支持的&#xff0c;通过寄存器配置不同&#xff0c;就可以将他当做定时器或者计数器使用。 确切的说&#xff0c;定时器和计数器区别是致使他们背后的计数存储器加1的信号不同。当配置为定时器使用时&#xff0c;每…

vue3 手写日历组件

找了很久vue3的element样式一直没办法修改实现。只能手写日历了。借鉴了一些大佬的代码 调用&#xff1a; 再要使用的地方引入 import calendarelement from ./calendarelement.vue //日历组件 <div > <calendarelement /> //日历</div> 效果&#…

押金原路退回系统在医院中应用,一键操作秒到账 押金+身份证+电子押金单

一、医院押金管理必要性 保障医疗服务的连续性&#xff1a;患者缴纳押金能够确保在治疗过程中&#xff0c;医院有足够的资金来提供必要的医疗服务、药品和设备&#xff0c;不会因为费用问题而中断治疗。例如&#xff0c;在紧急手术或需要持续使用昂贵药物的情况下&#xff0c;…

【Vue3】组件通信之$attrs

【Vue3】组件通信之$attrs 背景简介开发环境开发步骤及源码总结 背景 随着年龄的增长&#xff0c;很多曾经烂熟于心的技术原理已被岁月摩擦得愈发模糊起来&#xff0c;技术出身的人总是很难放下一些执念&#xff0c;遂将这些知识整理成文&#xff0c;以纪念曾经努力学习奋斗的…

开发在线客服系统新的宣传推广站【微客客服】

打造一个软件宣传官网&#xff0c;这事儿可不简单。咱们得先搞清楚&#xff0c;这个网站要给谁看&#xff0c;要传达啥信息&#xff0c;需要哪些功能。 我们网站是宣传【在线客服系统】的&#xff0c;所以需要把主要功能展示清楚 在线网址&#xff1a;https://weikefu.com.cn 然…

Python面试宝典第27题:全排列

题目 给定一个不含重复数字的数组nums&#xff0c;返回其所有可能的全排列 。备注&#xff1a;可以按任意顺序返回答案。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#xff1a;[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]] 示例 2&#xff1a; 输…

Qt之Gui

组件依赖关系 应用 #mermaid-svg-GADicZtZJRVVUeiF {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GADicZtZJRVVUeiF .error-icon{fill:#552222;}#mermaid-svg-GADicZtZJRVVUeiF .error-text{fill:#552222;stroke:#…

【Spark计算引擎----第三篇(RDD)---《深入理解 RDD:依赖、Spark 流程、Shuffle 与缓存》】

前言&#xff1a; &#x1f49e;&#x1f49e;大家好&#xff0c;我是书生♡&#xff0c;本阶段和大家一起分享和探索大数据技术Spark—RDD&#xff0c;本篇文章主要讲述了&#xff1a;RDD的依赖、Spark 流程、Shuffle 与缓存等等。欢迎大家一起探索讨论&#xff01;&#xff0…

【Gold菜鸟】Linux知识回忆(8)——进程和计划任务

前言 这部分让我们来继续了解Linux中进程和计划任务的相关知识吧~ 相关技术交流欢迎添加VX: wenjinworkon 目录 进程和内存管理 什么是进程 进程结构 进程相关概念 物理地址空间和虚拟地址空间 用户和内核空间 进程使用内存问题 进程状态 内存淘汰数据机制&#xff1a;…

数学建模评价类—Topsis法

目录 文章目录 前言 切记&#xff1a;以下内容仅用于参考理解&#xff0c;不可用于数模竞赛&#xff01;&#xff01;&#xff01; 一、Topsis的基本原理 二、Topsis的建模过程 1.判断矩阵是否需要正向化 2.原始矩阵正向化 3.矩阵标准化 4.计算距离&#xff0c;给出得…

Can Large Language Models Provide Feedback to Students? A Case Study on ChatGPT

文章目录 题目摘要相关工作方法结果讨论意义 题目 大型语言模型能为学生提供反馈吗&#xff1f;ChatGPT 案例研究 论文地址&#xff1a;https://ieeexplore.ieee.org/abstract/document/10260740 摘要 摘要——教育反馈已被广泛认为是提高学生学习能力的有效方法。然而&#x…

Python | Leetcode Python题解之第322题零钱兑换

题目&#xff1a; 题解&#xff1a; class Solution:def coinChange(self, coins: List[int], amount: int) -> int:dp [float(inf)] * (amount 1)dp[0] 0for coin in coins:for x in range(coin, amount 1):dp[x] min(dp[x], dp[x - coin] 1)return dp[amount] if d…

Python的if语句及其运用

一、条件测试 每条if语句的核心都是一个值为True或False的表达式&#xff0c;这种表达式称为条件测试。如果测试的条件满足if语句则为True&#xff0c;接着执行if里的语句&#xff1b;如果测试的条件不满足if语句则为False&#xff0c;则不执行if里的语句。 1.1、检查是否相等…

C++ | Leetcode C++题解之第322题零钱兑换

题目&#xff1a; 题解&#xff1a; class Solution { public:int coinChange(vector<int>& coins, int amount) {int Max amount 1;vector<int> dp(amount 1, Max);dp[0] 0;for (int i 1; i < amount; i) {for (int j 0; j < (int)coins.size();…

二叉树(真题)

1.用非递归遍历求二叉树结点个数【计学2020】 算法思想:用先序非递归遍历 当前指针不为空或栈不为空进行循环&#xff1b; 当前指针不为空访问当前结点&#xff0c;当前节点入栈&#xff0c;进入左子树 当前指针为空&#xff0c;栈顶元素出栈&#xff08;回溯&#xff09;&…

【kickstart+pxe批量安装linux系统】

目录 一、实验环境准备二、安装kickstart1、kickstart自动安装脚本的制作 三、安装web服务器&#xff0c;提供网络源四、安装dhcp五、安装syslinux&#xff0c;tftp-server六、虚拟机中新建新主机 一、实验环境准备 1、rhel7主机 2、开启主机图形 init 5 开图形 3、配置网络可…