目录
四.获取当前线程的引用 -- currentThread()
一. 创建并启动一个线程 -- start()
形象的比喻:
- 线程对象可以认为是 张三(主线程)把 李四(新线程)叫过来了
- 覆写 run 方法是提供给线程要做的事情的指令清单
- 而调用 start() 方法,就是喊⼀声:“行动起来!”,线程才真正独立去执行了。
调用 start 方法,才真的在操作系统的底层创建出一个线程。start的本质 是 调用系统的API,系统的API会 在系统内核里创建线程(创建PCB,加入链表)
start 的执行速度一般是比较快的(创建线程比较轻量),一旦start 执行完毕,新线程就会开始执行,调用start的线程也会继续执行(main)--> 并发执行
注:
- 调用start不一定必须是main线程调用,任何的线程都可以创建其他的线程。如果系统资源充裕,就可以任意创建线程(线程不是越多越好)
- 一个Thread对象只能调用一次start,如果多次调用start就会抛出IllegalThreadStateException异常
(由于Java中,希望一个Thread对象,只能对应到一个系统中的线程,这样便于管理。因此,就会在start中根据线程状态做出判断:如果Thread对象还未调用start,此时状态就是NEW状态,接下来就可以顺利调用start)
一个经典面试题 start() 和 run() 之间的差别
start():
- 调用 start() 方法后,真正在系统内核中创建线程(创建PCB,加入到链表中),线程会进入线程调度队列,等待CPU分配时间片来执行 run() 中的代码。(start会根据不同的系统调用不同的API)
- 一个线程对象只能调用一次 start() 方法。如果重复调用 start(),会抛出IllegalThreadStateException。
- start() 方法调用后,线程会并发执行,即与主线程同时运行。
run():
- run() 方法是 “线程的入口点”,它包含线程要执行的代码。当线程被 线程调度器 选中执行时,run() 方法会被调用。
- run() 方法可以像普通方法一样被直接调用,但这不会创建新线程,而是在当前线程中顺序执行 run() 方法中的代码。
如果调用start()方法,则会创建并启动一个新线程,该线程将并发执行 run() 方法中的代码。如果直接调用 run() 方法,则 run() 方法将在主线程中顺序执行,不会创建新线程。
二. 中断(终止)一个线程
形象的比喻:
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。
Java中,结束线程 是一个更“温柔”的一个过程,不是直接粗暴的。主要是怕某个工作执行了一半,强制结束了,此时对应的结果数据是一个“半成品” ( 我们更希望是一个完整的过程 )
目前常见的两种中断方式:
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
方式一:使用自定义的变量来作为标志位
public class InterruptDemo1 {
private static boolean isQuit = false;//设置为静态成员属性
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(!isQuit){
System.out.println("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;
}
}
运行结果:
变量捕获: lambda 表达式/ 匿名内部类 的一个语法规则(Java中的变量捕获做了简化,把同作用域下的变量都给捕获了,JS也是和Java类似的效果,而C++需要手动指定要捕获的变量)
如果将 isQuit 和 lambda定义在一个作用域中,此时lambda内部是可以访问到 lambda 外部(和lambda同一个作用域)中的变量。
但是Java的变量捕获,有特殊要求:要求捕获的变量得是final / effectively final(即不能进行修改)。
如果将isQuit的值进行修改,编译会报错。(这是Java的特殊限制,C++、JS等语言没有这个限制,都是可以直接捕获变量并进行修改的)
写成 成员变量 之后就可以了--> 是因为此时走的语法 是“内部类访问外部类的成员” 本身是可以的,和“变量捕获”无关了 !!! lambda 表达式本质上是一个 “函数式接口” 产生的 “匿名内部类”,内部类本来就能访问外部类。
方式二:isInterrupted()方法代替自定义标志位
Thread.currentThread():currentThread() 是 Thread 类的静态方法,能获取到调用这个方法的线程的实例。哪个线程调用,返回的引用就指向哪个线程的实例(类似于 this)。比如,在 main 线程中调用会获得 main 线程的引用。
public boolean isInterrupted():初始情况下,这个变量是false,未被终止。一旦外面的其他线程调用 interrupt() 方法,就会设置上述标志位
public class InterruptedDemo2 {
public static void main(String[] args) throws InterruptedException {
//下列的Lambda的代码在编译器眼里,出现在Thread t 的上方的.
//此时 t 还没有被定义,因此不可以用t.isInterrupted
Thread t = new Thread(()->{
//先获取到线程的引用
Thread currentThread = Thread.currentThread();
while(!currentThread.isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
//在主线程中,控制 t 线程被终止,设置上述标志位。
t.interrupt();
}
}
运行结果:
由于 判定 isInterrupted() 和 执行打印 这两个操作太快了,因此整个循环 主要的时间都是花在 sleep(1000) 上。main调用 interrupt() 的时候,大概率 t 线程正处于 sleep 状态中,此处的interrupt 不仅能设置标志位,还能把sleep 操作唤醒。
比如,sleep 此时刚睡了 100ms,还剩 900ms,此时 Interrupt 被调用了,sleep 就会直接被唤醒,并抛出 InterruptedException 异常
此时调用 interrupt 把 sleep 唤醒了,触发异常,被catch住了,虽然catch住了,但是循环还是在继续执行,看起来就好像这个标志位没有被设置一样??
注意这里的设定!!
首先,interrupt 肯定会设置这个标志位的 !!
其次,当 sleep等 阻塞方法 被唤醒之后,就会先清空刚才设置的interrupted标志位.....
因此,要想真正结束循环,结束线程,就需要在 catch 中加上 return/break 语句。
A 希望 B 线程终止,B 收到这样的请求后,B 自行决定是否要终止:由 B 线程内部的代码来决定,其他线程无权干涉(sleep清除标志位,就可以使B能够做出这种选择;如果sleep不清楚标志位的话,B 就势必会结束,无法写出让线程继续执行的代码了)
- 如果 B 线程想无视 A,就直接catch中啥都不做,B 线程会继续执行
- 如果 B 线程想立即结束,就直接在catch中写上return或者break,此时,B 线程就会立即结束
- 如果 B 线程想稍后再结束,就可以在catch中写上一些其他的逻辑(比如释放资源,清理一些数据,提交一些结果...收尾工作) 这些逻辑完成之后,再进行return/break
以上给了程序员更多的操作空间...
sleep方法 是被native关键字修饰的,即“本地方法”,是在JVM 内部,通过C++代码实现的。
总结:
- Java中线程的终止,是线程自身(run方法)决定的,而不是其他线程能强制决定的
- Thread提供的内置的标志位islnterrupted,同时通过interrupt方法触发
- Interrupt 方法能够设置标志位,也能唤醒 sleep 等阻塞方法
- sleep 被唤醒之后, 又能清空标志位
三.等待一个线程 --> join()
形象的比喻:
有时,我们需要等待一个线程完成他的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功后,才能决定是否存钱,这时我们需要一个方法明确等待线程的结束。
操作系统针对多个线程的执行 是一个“随即调度,抢占式执行”过程。
线程等待,就是在确定两个线程的“结束顺序”。无法确定两个线程调度执行的顺序,但是可以控制“谁先结束,谁后结束”。让后结束的线程等待先结束的线程即可。此时,后结束的线程就会进入阻塞,一直到先结束的线程真的结束,阻塞才解除。
比如,现在有两个线程,a , b
在 a 线程中调用 b.join() ,就是让 a 线程等待 b 线程执行结束后,a 线程再执行
public class JoinDemo1 {
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) {
e.printStackTrace();
}
}
System.out.println("线程 t 执行结束");
});
t.start();
//让主线程等待t线程执行完毕
System.out.println("main 线程开始等等");
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main 线程等待完毕");
}
}
运行结果:
当 t 线程执行结束,此时join才会返回,main才会继续执行 。
上述代码是 main 先等待,然后t 执行半天才结束,此时main在阻塞。
如果调整一下,让 t 先结束,然后 main 才开始join,这个时候是否会出现阻塞??
此时 join 并没有使 main 线程阻塞,因为 t 线程已经结束了。
join 是确保 被等待的线程 能够 先结束,如果已经结束了,join就不会造成阻塞。
刚刚使用的 join,都是无参数版本。无参数版本意思是:“死等”“不见不散” 被等待的线程只要执行不结束, 这里的阻塞 就会持续....
上述的“死等”操作,其实不是一个好的选择,因为一旦被等待的线程代码,出现了一些bug,就可能使这个线程迟迟无法结束,从而使等待线程,一直阻塞,无法执行其他操作了....
因此,join 有一个带参数版本的方法。
long millis:毫秒级时间
int nanos:精度纳秒级(因为计算机其实很难做到精确计时,一般能精确到 ms 就不错了,与操作系统有关:像Windows,Linux系统线程调度开销比较大;计算机中还有一类 “实时操作系统”,就能把调度开销尽可能降低,开销小于一定的误差要求,从而可以做到更精确,多用于航空航天、军事领域......实时操作系统,相当于舍弃了很多功能,换来的实时性)
四.获取当前线程的引用 -- currentThread()
想在某个线程中,获取到自身的Thread对象的引用,就可以通过currentThread来获取
线程终止操作,也是通过获得线程的引用来设置标志位
五.休眠当前线程 -- sleep()
Thread.sleep(1000) 让调用的线程阻塞等待,这个过程是有一定时间的。
线程执行sleep,就会使这个线程不参与cpu调度,从而把cpu资源让出来,给别人使用。也把sleep这样的操作 称为 “放权”-->放弃使用cpu的权利。
有的开发场景中,发现某个线程cpu占用率过高,就可以通过sleep来进行改善。虽然线程的优先级 就可以对线程调用产生影响。但是优先级的影响是比较有限的,可以通过sleep来更明显地影响到这里的cpu占用