多线程(2)
除了上一节“继承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啦,到此结束!!!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)