多线程(二) --- Thread类的用法

目录

         一.Thread类的常见构造方法

          二.Thread类的几个常见属性

             三.线程中断

          四.线程等待

           五.线程休眠

           六.获取线程引用

           七.小结


       在上一篇博客当中,我介绍了多线程的一些基础知识,以及如何创建线程。我们现在知道了,创建线程时需要用到Thread类。因此,我会接着介绍Thread类的一些属性和其他用法,其中用法包括:线程中断、线程等待、线程休眠和获取线程实例。

Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。

而每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。

         一.Thread类的常见构造方法

          每一个类都有自己的构造方法,当然Thread类也不例外。在Thread类中,既提供了不带参数的构造方法,又提供了带参数的构造方法。具体的构造方法如下表所示:

方法对应说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
【了解】Thread(ThreadGroup group,
Runnable target)
线程可以被用来分组管理,分好的组即为线程组,这
个目前我们了解即可

          接下来,我来演示一下具体的代码该如何写:

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("线程名字t1");
Thread t4 = new Thread(new MyRunnable(), "线程名字t2");

           以上的四行代码分别对应表中的四种构造方法,我们可以根据具体情况来合理进行使用。

          二.Thread类的几个常见属性

           接着我们来看Thread类中的常见属性。具体参考下表:

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

Tips:

由于ID是线程的唯一标识,因此不同线程的ID不会重复。

对于名称这个属性,是各种调试工具用到的。

状态表示线程当前所处的一个情况,后面的博客我会进一步说明。

优先级高的线程理论上来说更容易被调度到。

关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。所以说,前台线程会影响到进程执行结束,后台线程不会影响进程结束。

是否存活,即简单的理解,就看 run 方法是否运行结束了。值得我们注意的是,Thread对象的生命周期往往比系统中的线程要更长。线程没了,Thread对象可能还在。

线程的中断问题,下面我会进一步说明。

             三.线程中断

              线程中断,就是要让一个线程停止运行(销毁)。在Java中,要销毁/终止线程,做法比较唯一的,就是想办法让run方法尽快执行结束。

终止线程的第一种方法,是可以在代码中手动创建出标志位,来作为run的执行结束的条件。

如图所示,很多线程执行时间久,往往是因为这里写了一个循环,循环要持续执行。要想让run方法执行结束,就是让循环尽快退出。

举如下代码为例:

当前这个代码,是使用了一个成员变量isQuit来作为标志位。当我们想要让这个线程停止运行时,就可以手动将isQuit设为true,这样一来,就能够退出循环,从而让线程中断。

由图可见,这里的isQuit是一个全局变量。那让我们来思考一下,如果把isQuit改成main方法中的局部变量,是否可以呢?

答案是不可以。这里是因为,lambda表达式有一个语法规则,叫变量捕获。lambda表达式里面的代码,是可以自动捕获到上层作用域中涉及到的局部变量的。

所谓的变量捕获,其实就是让lambda表达式把当前作用域中的变量在lambda内部复制了一份(此时,外面是否销毁就无所谓了)。

在Java中,变量捕获语法还有一个前提限制,就是必须只能捕获一个final或者是实际上是final的变量(变量虽然没有使用final,但是却没有修改内容,就是“实际上是final”)。

因此,当isQuit变量被修改时,由于它不再是final变量,编译这里就会报错。

 然而,第一种方案不够“优雅”,原因在于以下两点

第一,需要手动创建变量;

第二,当线程内部在sleep的时候,主线程修改变量,新线程内部不能及时响应。

所以,这里就需要使用另外的方法来完成上述操作 --- isInterrupted()

可以看出,这里使用Thread.currentThread()来获取到当前线程的实例(此处得到的就是t,哪个线程调用这个方法,就会返回哪个线程的对象),接着用这个实例来调用isInterrupted()方法。Thread内部有一个标志位,这个标志位就可以用来判定线程是否结束。

接着进行以上操作,这个操作就是把上述Thread对象内部的标志位设置为true了,从而可以跳出循环,提前终止线程。即使线程内部的逻辑出现阻塞(sleep),也是可以使用这个方法唤醒的。

但是当代码运行起来时,却是以下结果:

可以看到,运行结果抛了一个异常,而且t线程并没有结束。这又是为什么呢?

这是因为,sleep在此”捣乱“!在执行sleep的过程中调用interrupt,大概率sleep休眠时间还没到,就被提前唤醒了。提前唤醒时会做两件事:第一,抛出InterruptedException(紧接着就会被catch获取到);第二,清除Thread对象的isInterrupted标志位

因此,通过interrupt方法,已经把标志位设为true了,但是sleep提前唤醒操作,就把标志位又设回false了(此时循环还是会继续执行了)。要想让t线程彻底结束,只需要在catch中加上break就行了。

其实,sleep清空标志位,是为了给程序猿更多的“可操作性空间”。比如说,前一个代码写的是sleep(1000),结果现在1000还没到,就要终止线程。这就相当于是两个前后矛盾的操作。此时,是希望写更多的代码来对这样的情况进行具体处理的。此时程序猿就可以在catch语句中,加入一些代码来做一些处理:

1)让线程立即结束(加上break)。

2)让线程不结束,继续执行(不加break)。

3)让线程执行一些逻辑之后,再结束(写一些其他代码,再break)。

          四.线程等待

            所谓线程等待,其实就是让一个线程等待另一个线程执行结束,再继续执行。本质上就是控制线程结束的顺序。

我们常用join方法实现线程等待效果

我们需要注意的是,在主线程中调用t.join(),此时就是主线程等待t线程结束,主线程才能继续往下执行其他代码。

下面我来分析一下t.join()的工作过程:

1)如果t线程正在运行中,此时调用join的线程就会进入阻塞状态,一直阻塞到t线程执行结束为止。

2)如果t线程已经执行结束了,此时调用join的线程就直接返回了,不会涉及到阻塞。

此外,Java提供了多个join方法的版本,具体参考下图:

其中,第二个版本的join方法是我们在日常开发中使用比较多的。它是带有超时时间的等待,实际开发中一般不建议“死等”,最好要带有“超时时间”。

为了方便大家更好地理解线程等待,我在这里举个栗子🌰说明:

 高中的时候,有一天我约女神出来玩,约好19:00在学校门口碰头。那么就会出现三种情况:

1)如果我先到了,我发现女神还没来,我就要“阻塞等待”,等到女神来了之后,我俩就可以一起去吃麻辣烫了。

2)女神先到了。当我来到校门口的时候,虽然时间还不到19:00,但是我看到女神已经在了,此时我俩直接出发去吃麻辣烫就可以了,就不需要等待。

3)我来了之后,等了很久,女神还没出现,我仍然继续等(join默认是“死等”,“不死不休”)

一般来说,等待操作都是带有一个“超时时间”。

           五.线程休眠

            线程休眠,前面其实提到过,就是sleep方法。它也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。

关于sleep方法,这里提供了两个版本,具体参考下图:

值得我们注意的是,休眠时间的单位是毫秒(ms)。然而,sleep本身就是存在一定精度误差的,不是说sleep(1000),就是精确的正好1000,这中间还有一个调度的开销。

首先,系统会按照1000这个时间来控制让线程休眠。当1000时间到了之后,系统会唤醒这个线程(阻塞 -> 就绪)。但是,不是说这个线程成了就绪状态,就能立即回到CPU上运行的(这中间有一个“调度”开销)。

对于Windows或者Linux这样的系统来说,调度开销很大,可能达到ms级别。

           六.获取线程引用

             前面我们知道了,获取线程引用(Thread的引用)可以使用Thread.currentThread()。

当然,获取线程的引用还有其他方法:

当我们的代码创建线程的时候,如果使用的是继承Thread类,也可以通过this来获取当前线程的引用。

但如果是实现Runnable接口,或者使用的是lambda表达式的话,还是只能靠Thread.currentThread()来获取线程引用。

           七.小结

            以上,就是我对Thread类用法的相关介绍。 在下篇博客中,我会介绍线程的几种状态,大家可以期待一下。多线程的学习任重而道远,大家一定不要半途而废!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值