除了上一节“继承Thread,重写run”创建线程(方法1)外,还有很多种方法,如下所示:

1. 创建线程

1.1 实现Runable,重写run

代码示例:

运行结果:

其中, 就是一个任务,一段要执行的逻辑。

最终还是要通过Thread真正创建线程,线程里要干啥,通过Runnable来表示。

值得注意的是,我们需要让要执行的任务本身和线程这个概念,能够解耦合,后续变更代码会方便一些(比如不通过线程执行这个方式,通过其他的方式……),采用Runnable这个方案,代码的修改就会更简单。

那么什么是耦合??? 简单来说,两个代码的关联关系越大,耦合就越大(如果耦合太大,改一个代码就可能改坏一片),在代码中,希望低耦合(开发中,需求量老变,代码经常修改)模块之间,依赖尽量小,影响尽量小。

大家可能听过“高内聚低耦合”这句话,耦合咱们知道了,如何理解“内聚”呢?

比如我们写了一个项目,有很多代码,很多文件,很多类,很多逻辑……,如果把有关联的各种代码放到一起(只要和某个功能逻辑相关的东西,放到一起),就叫做高内聚(一个模块之内,有关联的东西放到一起),反之则是低内聚。

1.2 使用匿名内部类(本质上就是方法1)

代码示例:

,做了三件事:
1)创建了一个Thread的子类,但是子类叫啥名字,不知道是匿名的

2){}里面可以编写子类的定义代码。子类里要有哪些属性,有哪些方法等

3)创建了这个匿名内部类的实例,并且把实例的引用赋值给t

运行结果和上述方法都一样

这样做的好处就是可以少定义一些类(比如上回的MyThread类)

应用场景:一般如果某个代码是“一次性”

1.3 使用Runnable,匿名内部类

运行结果和上述方法都一样

1.4 引入lambda函数

()->{}:创建了一个匿名的函数式接口的子类,并且创造出对应的实例,并且重写了里面的方法

运行结果和上述方法都一样

对于我而言,我更喜欢这种方法

还有很多种方法,我们之后再讨论~

这里结合之前学的知识总结一下,线程和进程的区别(重点):

进程包含线程

进程是操作系统资源分配的基本单位

线程是操作系统调度执行的基本单位

同一个进程中的多个线程之间共用同一份资源(内存 文件)

2. Thread类的其他的属性和方法

2.1 方法

Thread():创建线程对象(使用这个方法,必须要重写Thread的run())

Thread(Runnable target):使用Runnable对象创建线程对象

Thread(String name):创建线程对象,并命名

Thread(Runnable target,String name):使用Runnable对象创建线程对象,并命名

比如:

可以给线程随便起名字,名字的意义,就是方便程序员调试

2.2 属性

ID(Java中给每个运行的线程分配Iid,id起到标识线程身份的效果,类似于pid):getId()

1)名称:getName()

2)状态:getState()

3)优先级:getPriority()

4)是否后台线程:isDaemon()

什么是后台线程(守护线程)? 什么是前台线程?

前台线程:能够影响到进程继续存在的线程(我们自己代码创建的线程,包括main线程,默认都是前台线程,可以通过SetDaemon方法来修改)

把线程设置为后台线程(这样的设置得在start之前进行):SetDaemon(true)

后台线程:不影响进程结束的线程(JVM自带的线程,属于有特殊功能的线程)

如果有多个前台线程,必须得所有的前台线程都结束,进程才结束

代码示例:

把t线程设置为了后台线程,无法阻止进程结束

运行结果:

5)是否存活:isAlive()

Java代码中创建的Thread对象和系统中的线程是一一对应的关系。但是,Thread对象的生命周期和系统中的线程的生命周期是不同的(可能存在Thread对象还存活,但是系统中的线程已经销毁的情况)。

代码示例:

运行结果:

这个代码的逻辑3秒之后就结束了,线程的入口方法里的逻辑结束了,系统中对应的线程也就随之销毁了。但是t对象依然存在。

在t.start()之前

System.out.println(t.isAlive());

肯定为false,因为t线程还没创建呢

6)是否被中断:isInterrupted()

中断一个线程(让线程能够结束):让线程的入口方法执行完毕,线程就随之结束了(run方法尽快return)。

代码演示:

运行结果:

值得注意的是isFinished这个变量不可以定义成局部变量,

意思就是变量应该是final 或者 实际上是final(虽然没加关键字,但是代码中不会修改)

为什么呢???

这涉及到了变量捕获的语法,lambda里面想使用外面的变量会触发“变量捕获”的语法。lambda是回调函数,它的执行时机是很久之后(操作系统真正创建出线程之后,才会执行),很有可能,后续线程创建好了,但是当前main的方法执行完了,isFinished销毁了。为了解决上述的问题,Java把被捕获的变量给拷贝一份,拷贝给lambda里面,所以外面的变量是否销毁,就不会影响lambda里面的执行了。但是,拷贝意味着这样的变量就不适合进行修改,就算修改一方,另一方也不会随之改变(本质上是两个变量)。这种一边变,一边不变就会造成很大的困扰。所以Java的大佬们就想了个办法,就是不允许这里进行修改。

那问题来了,为什么上面的成员变量可以呢???

这就涉及到了另一个语法,不是变量捕获了,而是“内部类访问外部类的成员”的语法。lambda本质上是函数式接口,相当于一个内部类,而isFinished变量本身就是外部类(demo8)的成员,内部类本来就能够访问外部类的成员。

上面是我们自己写的变量来终止线程,Java的Thread对象中提供了现成的变量,直接进行判定,就不需要自己创建了

这个变量:

其中Thread.currentThread()在哪个线程中调用,获取到的就是哪个线程的Thread引用

比如:

运行结果:

终止线程的代码:

其中,判定线程是否终止了(判定Thread里的boolean变量的值)

 主动去进行终止(修改这个boolean变量的值)

运行结果:

诶,怎么还报错了呀?

仔细一看,不难发现sleep被唤醒了。其实 除了能设置boolean变量(标志位)之外,还能够唤醒sleep这样的阻塞方法。每次执行循环,绝大部分时间都是在sleep,主线程中调用t.interrupt(),极大的概率t线程在sleep,这个操作就能够把sleep唤醒,就会抛出异常。所以,我们这样:


运行结果:

上面的代码直接加上break就是立即终止,啥都不写就是不终止,catch中先执行一些其他逻辑再break就是稍后终止

Java这里的设定,就把决定权交给被终止的线程自己了

OK啦,到此结束!!!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐