当前位置: 首页 > news >正文

多线程 -- Thread类

Thread 的常见构造方法

最后一个构造方法中的 ThreadGroup 线程组是 Java 中的概念,和系统内核中的线程组不是一个东西。我们自己创建的线程,默认是按照 Thread- 0 1 2 3 4...命名。但我们也可以给不同的线程,起不同的名字(不同的名字,对线程的执行,没有什么影响,主要是方便我们调试)

举例如下:

Thread 的几个常见属性

  1.  ID 是线程的唯一表示,JVM会自动进行分配,不同线程不会重复
  2.  名称是各种调试工具中,会使用到
  3.  状态表示线程当前所处的一个情况,进程有状态,分为就绪状态和阻塞状态。线程也有状  态,Java中对线程的状态,又进行了进一步的区分(比系统原生的状态,更丰富一些)
  4. 线程也有优先级,优先级高的线程,理论上来说更容易被调度到,但在Java中,效果其实并不是很明显(会对内核调度器的调度过程产生一些影响),总体上还是抢占式调度。
  5. daemon --> 线程守护,也可以称为是”后台线程“,和其对应的,还有”前台进程“(注意,这里的前台和后台,与Android系统上的前后台APP是完全不同的)

前台线程的运行,会阻止进程结束,后台线程的运行,不会阻止进程的结束

示例如下:

我们可以打开 jconsole 观察一下:

 上面示例代码在执行过程中, t 会持续进行(因为是while(true)死循环),但 main 已经结束了。jconsole 观察中,可以看到,除了我们创建的线程 Thread-0,其他都是JVM内置的线程,那些都是后台线程,不会阻止进程的结束。并且,在列表中,已经没有 main 线程了。按照我们之前的理解,main 执行完毕,进程应该结束,但很明显,此时这个进程仍然在继续执行中!

当我们强制结束,打印台线程显示如下的话,才表明进程结束了。

我们代码创建出来的进程,默认就是前台线程,会阻止进程结束,只要前台线程没执行完,进程就不会结束,即使 main 已经执行完毕了

但我们若是进行一些稍加改动,即在调用 start 方法之前,就调用 setDaemon 方法,设置进程为后台进程 

此时在重新运行程序,就会发现,控制台什么都没打印,进程就结束了。

setDaemon方法中,传入参数为 true ,则该线程为后台,不设 true,则是前台。

后台不会阻止进程的结束,前台会阻止进程的结束。

        6.isAlive(),即该线程是否存活表示了内核中的线程(PCB)是否还存在,Java代码中定义的线程对象(Thread)实例,虽然表示一个线程,但这个对象本身的生命周期,和内核中的 PCB 声明周期,是完全不一样的。

当执行这段代码之后,此时 t 对象是有了,但是内核中 PCB 还没有,isAlive 就是 false。

真正 t 调用 start方法,即 t.strat() 的时候,才真正在内核中 创建出这个PCB,此时 isAlive就是 ture了。当线程 run 执行完了,此时 内核中的线程 就结束了(内核PCB 就释放了),但是,此时 t 变量还存在,但 isAlive是 false。

示例代码:

打印结果:

Thread 类 使用 start 方法,启动一个线程,对于同一个 Thread 对象来说,start 方法只能调用一次。

示例代码:

运行程序之后:

虽然可以正常打印,但是会有报错的Exception

我们可以分析一下上面这个异常,IllegalThreadStateException,即非法的线程状态异常

面试题:start 和 run 的区别

本质上,strat 和 run 是八竿子打不着,互不相干的内容。

如图,我们有一个这样的代码:

在 main 函数中,调用 start 方法,结果如下:

如果注释掉 strat 方法,调用 run 方法,结果如下:

这里看起来的执行结果是一样的。但两个方法打印的时候,操作所在的线程的不一样的

t.strat() --> 这行代码是创建一个新的线程,由新的线程执行 hello

t.run() --> 这行代码的操作,仍然是在主线程中,打印的 hello

如果我们对代码进行一些修改:

打印结果就只有 hello thread,即代码此时就只能停留在 run 的循环中,下方 main 中的循环(打印 hello main 是无法执行的)

但如果此时是调用 t.start()

结果如下:

就会创建一个新的进程,然后在进程里面执行run循环,但因为 Java 是抢占式进程,此时就能够执行 main 中的循环。

终止一个线程

李四⼀旦进到⼯作状态,他就会按照⾏动指南上的步骤去进⾏⼯作,不完成是不会结束的。但有时我 们需要增加⼀些机制,例如⽼板突然来电话了,说转账的对⽅是个骗⼦,需要赶紧停⽌转账,那张三 该如何通知李四停⽌呢?这就涉及到我们的终止线程的⽅式了。

终止一个线程:即,让线程 run 方法(入口方法)执行完毕

那如何让线程提前终止呢?

核心问题也就是:如何让 run 方法能够提前结束呢?这就很取决于我们具体代码的实现方式了。

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

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

        2.调用 interrupt() 方法来通知

引入:

我们也可以引入一个标志位 isQuite 如下图

通过上述代码,就可以让线程结束掉,具体什么时候结束,就取决于我们在另一个线程中的代码实现(即,在另一个线程中何时修改 isQuite 的值)

还有就是,在 main 线程中,要想让 t 线程结束,大前提,一定是 t 线程中的代码,对这样的逻辑有所实现,即有 isQuite 这种标志位,而不是 t 里面的代码随便怎么写,都能够随意提前结束的。

通过刚才的写法,其实是并不够优雅的,雷军好同志曾经说过,他大学期间的代码,优雅到诗一般,我们这个就比较拉跨了。

Thread 类还提供了一种更优的选择 -->  Thread 对象,内置了一个变量 --> currentThread

改进代码如下:

在这个代码中 while 循环中的参数是 Thread.currentThread().isInterrupted() 

其中,Thread.currentThread 操作是获取当前线程实例( t ),那个线程调用,得到的就是那个线程的实例,类似于 this,把我们引入中的 isQuite 改成判定 isInterrupter。

Thread.currentThread 补充:

该方法是获取到当前线程的引用(Thread的引用),如果是继承 Thread 类,就直接可以使用 This 来拿到线程实例,如果是 Runnable 或者 lambda 的方式,this 就无能为例了,此时 this 已经不再指向 Thread 对象了,就只能使用 Thread.currentThread()了。

下面的代码,本质上,是使用了 Thread 实例,内部自带的标志位,来代替刚才手动创建的 isQuit变量了,最后一行代码 t.interrupt() 就相当于 isQuit = true了。

执行代码如下:

可以看到,代码执行到了14行的时候,出现了一个异常,并且 t 线程 并没有真的结束。

我们研究报出的异常 InterruptedException 这不就是 try - catch 中的吗?

再观察报出的异常:

好像是这里的 interrupt 导致 sleep 出现了异常

如果没有 sleep interrupt ,线程是可以顺利结束的,但有了 sleep 就引起了变数。

在执行 sleep 的过程中,调用了 interrupt,大概率是 sleep 的休眠时间还没有到,就被 interrupt 提前唤醒了。

sleep 提前被唤醒,会做两件事:

        1. 抛出 InterruptedException (紧接着就会被 catch 获取到)

        2. 清除 Thread 对象的 isInterrupted 标志位

通过 interrupt 方法,已经把标志位设置位 true 了,但是 sleep 提前被唤醒之后,又会清除 Thread 对象的 isInterrupted 标志位,即又把标志位设回 false 了,所以此时循环还是会继续执行了。

如果我们想要让线程结束的话,只需要在 catch 中 加上 break 就可以了。

结果如下:

这样,循环就可以结束了。但还是会报出Exception,但这个日志是我们代码中 e.printStackTrace()中打出来的,如果我们不写打印,就不会存在了。

sleep 清空标志位,是为了给程序员更多的“可操作空间”的。前一个代码,写的是 sleep(1000),结果现在, 1000 还没有到,就要终止线程,这就相当于是两个前后矛盾的操作,此时,也是需要更多的代码,来对这样的情况进行具体处理的。

此时程序员就可以在 catch 语句中,加入一些代码,来做一些处理。

        1. 让线程立即结束 --> break

        2. 让线程不结束,继续执行 --> 不加 break

        3. 让线程执行一些逻辑之后,再结束 --> 写一些其他的代码,再 break

对 try - catch 块的补充:(在实际开发中, catch 里应该要写什么样的代码???如果程序出现了异常,该如何处理,是更加合理的???)

对于一个服务器来说,稳定性,是十分重要的,我们无法保证服务器一直不出问题,这些所谓的“问题”,在 Java 代码中,就会以 异常的形式体现出来,可以通过 catch 语句,对这些异常进行处理。

        1. 尝试自动恢复。能自动恢复,就尽量自动恢复。比如出现了一个网络通信相关的异常,我们就可以在 catch 中尝试重新连接网络。

        2. 记录日志(异常信息记录到文件中)有些情况,并非是很严重的问题,只需要把这个问题记录下来即可(并不需要立即解决),等到后面程序员有空闲的时候,再进行解决。

        3.发出报警。这个是针对一些比较严重的问题了,包括但不限于,给程序员 发邮件,发短信,发微信,打电话等等.......

        4. 也有少数正常的业务逻辑,会依赖到 catch (比如文件操作中 有的方法,就是要通过 catch 来结束循环...)(非常规用法)

在 Java 中, 线程的终止,是一种“软性”操作,必须要对应的线程去进行配合,才可以把终止落实下去。

相比之下,系统原生的 API 其实提供了强制终止线程的操作。无论线程是否愿意配合,无论线程执行到了那行代码,都能够强行的把线程给干掉!!

这样的操作,Java  的 API 是没有提供的,上述强制执行的做法,利大于弊。

如果要强行终止一个线程,很可能线程执行到一般,就被强制终止,会出现一些残留的临时性质的“错误”的数据。比如这个线程正在执行写操作,写文件的数据有一定的格式要求(写一个图片文件) --> 如果写图片写了一般,线程被终止了,图片就尴尬了,图片文件是存在的,里面的内容不正确,无法正确打开了。

private static boolean isQuit = false

如果把 isQuit 作为 main 方法中的局部变量,是否可行? -- > 不可行。

这是我们在 lambda 表达式中曾经研究过的一个语法 -- > 变量捕获

lambda 表达式 / 匿名内部类 是可以访问到 外面定义的局部变量的(变量捕获规则)

报错信息告诉我们,捕获的变量,必须是 final 修饰的 或者是 “事实”final(即虽然没写 final 但是没有修改), 但is Quit 又必须要修改!!!此处的 final,也不是“试试”final,所以局部变量这一手,是行不通的。

因此,必须写成成员变量。那为什么,写成成员变量就行得通了呢?这又是那个语法规则呢?

lambda表达式,本质上是“函数式接口“ ==》 匿名内部类。 内部类来访问外部类的成员,这个事情本身就是可以的,这个操作就不受到变量捕获的影响了。

那为什么,Java 对于变量捕获操作,有 final 的限制呢???

isQuite 是局部变量的时候,是属于 main 方法的栈帧中,但是 Thread lambda 是由自己独立的栈帧的(是另一个线程中的方法),这两个栈帧的生命周期是不一致的。

这就可能导致 --> main 方法执行完了,栈帧就销毁了,main 方法执行完了,栈帧就销毁了,但此时 Thread 的栈帧还在,还想继续使用 isQuit。Java 中的做法就非常的简单粗暴,变量捕获的本质上就是传参,换句话说,就是让 lambda 表达式在自己的栈中创建一个新的 isQuit,并把外面的 isQuit 值给拷贝过来(为了避免 isQuit 的值不同步, Java 干脆就不让 isQuit 修改)。

等待一个线程 - join()

有时候,我们需要等待一个线程完成它的工作之后,才能进行自己的下一步工作。例如:张三只有等李四转账成功之后,才能对现在的吃饭行为进行付款,这时候,我们需要一个方法明确的等待线程的结束。

多个线程的执行顺序是不固定的(随即调度,抢占式执行),虽然线程底部的调度是无序的,但是可以在应用程序中,通过一些 API,来影响到线程执行的顺序。 --> join 就是一种方式,影响线程结束的先后顺序。比如,t2 线程等待 t1 线程,此时,一定是 t1 线程先结束,t2 线程后结束,其中就使用到 join 使得 t2 线程阻塞。

示例代码:

打印结果如下:

补充:

如果不适用 join,使用 sleep,是具有随机性的,如果将 join 换位 sleep,如下:

在 sleep 5 秒之后,是先打印”这是主线程“还是先打印”线程执行完毕“,是无法确定的。虽然,我们可以进行修改,sleep 中的参数可以传为 6000,这也是一个办法,但是不完全可行,我们给 sleep 传参数,是能够对线程 t 的执行时间有一个预期,才能这样些,如果都不知道 t 要执行多久,那 sleep 的参数就没办法传了。所以最好的办法还是 join 方法,让 main 线程等待 t 线程结束【谁等谁,一定要分清楚,在那个线程中调用 join 方法,就是在 那个线程中等待 调用 join 方法的线程,如上图例子,在 main 线程中,t 线程调用 join 方法,则是 main 线程 等待 t 线程】。

执行 join 的时候,就看 t 线程是否正在运行,如果 t 运行中,main 线程就会阻塞(main 线程就暂时不去参与 CPU 执行了),如果 t 运行结束, main 线程就会总阻塞中恢复过来,并且继续往下执行。(阻塞:使得线程的结束时间,产生了先后关系。)

补充:

        1.这个 join 阻塞和优先级还是不同的。优先级,是系统调度器在内核中完成的工作,即使优先级有差异,但是每个线程的执行顺序仍然是随机的。

线程优先级是调度器的重要参考,但实际执行顺序还受调度策略、时间片、线程状态、资源竞争等因素影响。优先级决定的是线程获取 CPU 的 “机会”,而非绝对顺序。因此,即使优先级有差异,线程执行顺序仍可能表现出随机性。

上述线程结束顺序的先后,在代码中,是通过 API 来控制的,让 main 线程,主动放弃了去调度器中调度,其中 t 线程 虽然也可能和其他线程共同进行调度,但由于主线程一直在等待,即使 t 线程中间经历了多次 CPU 的切换,仍然不影响 t 线程最终能够正确先执行完毕。

join 方法中,也是可以有参数的,若没有参数,我们称为“死等”,就必须要要等待线程结束,再进行当前线程,这是机器不科学的,尤其是再我们的计算机中(如果我们的代码中,因为死等,导致程序卡住了,无法继续处理后面的逻辑,这是一个非常严重的 bug !)

若传入一个参数,就是带有超时时间的等,等操作是由一个时间上限的,等待的时间达到超时时间,就不等了,该干啥干啥了。

完!

http://www.dtcms.com/a/98206.html

相关文章:

  • vue学习
  • Linux内核同步机制:解锁并发编程的奥秘
  • 软件的常用设计模式。可参考一个一个学习
  • 用Nginx实现负载均衡与高可用架构(整合Keepalived)
  • [Linux]在vim中批量注释与批量取消注释
  • 进程Kill杀死后GPU显存没有释放仍然被占用,怎么杀死僵尸进程
  • 跟着StatQuest学知识08-RNN与LSTM
  • Claude 在 SVG 绘图创作中的潜力与技巧
  • 【软考-架构】10.1、软件工程概述-CMM-软件过程模型-逆向工程
  • Pycharm (十)字符串扩展:统计小串在大串中出现的次数
  • C++23:现代C++的模块化革命与零成本抽象新高度
  • 笔记:遇见未来——6G协同创新技术研讨会
  • FPGA调试笔记
  • 从代码学习深度学习 - 含并行连结的网络(GoogLeNet)PyTorch版
  • 淘宝双十一大促监控系统开发:实时追踪爆品数据与流量波动
  • 谷粒微服务高级篇学习笔记整理---异步线程池
  • SQL Server数据库引擎服务启动失败:端口冲突
  • 电源系统的热设计与热管理--以反激式充电器为例
  • 1688 店铺清单及全商品数据、关键词检索 API 介绍
  • 【蓝桥杯】每日练习 Day15
  • 【自用记录】本地关联GitHub以及遇到的问题
  • 从代码学习深度学习 - 使用块的网络(VGG)PyTorch版
  • 谈谈你对多态的理解
  • coding ability 展开第七幕(前缀和算法——进阶巩固)超详细!!!!
  • 算法基础——二叉树
  • Java 程序员面试题:从基础到高阶的深度解析
  • Elasticsearch 完全指南
  • 【HarmonyOS 5】初学者如何高效的学习鸿蒙?
  • Bitnode和Bitree有什么区别 为什么Bitree前多了*
  • 缴纳过路费--并查集+优先队列