Thread 的常见构造方法
最后一个构造方法中的 ThreadGroup 线程组是 Java 中的概念,和系统内核中的线程组不是一个东西。我们自己创建的线程,默认是按照 Thread- 0 1 2 3 4...命名。但我们也可以给不同的线程,起不同的名字(不同的名字,对线程的执行,没有什么影响,主要是方便我们调试)
举例如下:
Thread 的几个常见属性
- ID 是线程的唯一表示,JVM会自动进行分配,不同线程不会重复
- 名称是各种调试工具中,会使用到
- 状态表示线程当前所处的一个情况,进程有状态,分为就绪状态和阻塞状态。线程也有状 态,Java中对线程的状态,又进行了进一步的区分(比系统原生的状态,更丰富一些)
- 线程也有优先级,优先级高的线程,理论上来说更容易被调度到,但在Java中,效果其实并不是很明显(会对内核调度器的调度过程产生一些影响),总体上还是抢占式调度。
- 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 !)
若传入一个参数,就是带有超时时间的等,等操作是由一个时间上限的,等待的时间达到超时时间,就不等了,该干啥干啥了。