JavaEE 初阶(5)——多线程3之线程核心操作

目录

一. 创建并启动一个线程 -- start()

二. 中断(终止)一个线程   

方式一:使用自定义的变量来作为标志位

方式二:isInterrupted()方法代替自定义标志位

三.等待一个线程 --> join()

四.获取当前线程的引用 -- currentThread()

五.休眠当前线程 -- sleep()


一. 创建并启动一个线程 -- 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中,结束线程 是一个更“温柔”的一个过程,不是直接粗暴的。主要是怕某个工作执行了一半,强制结束了,此时对应的结果数据是一个“半成品” ( 我们更希望是一个完整的过程 )

目前常见的两种中断方式: 

  1. 通过共享的标记来进行沟通
  2. 调用 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++代码实现的。 

总结:

  1. Java中线程的终止,是线程自身(run方法)决定的,而不是其他线程能强制决定的
  2. Thread提供的内置的标志位islnterrupted,同时通过interrupt方法触发
  3. Interrupt 方法能够设置标志位,也能唤醒 sleep 等阻塞方法
  4. 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占用

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值