当前位置: 首页 > news >正文

Thread类

目录

创建线程        

一、继承Thread类

二、实现Runnable接口

三、匿名内部类

(1)Thread匿名内部类

(2)Runnable匿名内部类

四、lambda表达式

Thread类的方法

给线程命名

IsAlive()方法

setDaemon()方法

currentThread()方法

终止线程

(1)使用全局变量(成员变量)

(2)Thread提供interrupt()方法

线程休眠

线程等待

(1)无参join

(2)带参join


线程是操作系统提供的概念,操作系统提供了一些api供程序员使用,不过这些原生线程api是用c语言编写的,而且不同操作系统的线程api也不一样,java对操作系统中的api进行了统一的封装,于是出现了Thread类。

创建线程        

一、继承Thread类

class Mythread extends Thread {@Overridepublic void run() {while (true) {System.out.println("hello thread");}}
}
public class Demo {public static void main(String[] args) throws InterruptedException {Thread t = new Mythread();//真正在系统中创建一个线程t.start();while (true) {System.out.println("hello main");}}
}

上述代码创建了一个类Mythread继承Thread,并且重写了Thread类的run方法

在main方法中实例化Mythread类并向上转型,用对象引用t调用Thread类中的start()方法。

t.start()这句代码执行后真正的在系统中创建了一个线程(JVM调用操作系统的api完成线程的创建)

每个Thread对象都只能start一次,每次想创建一个新的线程,都得创建一个新的Thread对象(不能重复利用)

run方法不需要我们去调用,例如:t.run,这样虽然能打印出内容,但系统中并没有创建出这个线程。当我们使用t.strat()时,会自动的调用run方法,并且还会创建出线程。


二、实现Runnable接口

class MyRunnable implements Runnable {@Overridepublic void run() {while (true) {System.out.println("hello thread");}}
}public class Demo1 {public static void main(String[] args) throws InterruptedException {Runnable r = new MyRunnable();Thread t = new Thread(r);t.start();System.out.println("hello main");}
}

在MyRunnable类中也要重写接口Runnable的run方法

最终还是要通过Thread类去创建线程,但是通过Runnable接口这样写的好处是:

代码具有低耦合的特点,让要执行的任务本身和线程这个概念能够解耦合,后续如果要变更代码(比如不通过线程执行这个任务,通过其它方式...)采用Runnable这样的方案,代码的修改就会简单很多


三、匿名内部类

(1)Thread匿名内部类

 public static void main(String[] args) {//匿名内部类Thread t = new Thread() {@Overridepublic void run() {while (true) {System.out.println("hello Thread");}}};t.start();while (true) {System.out.println("hello Main");}}
}

这种方法是不再创建一个新的类去继承Thread类,而是在实例化的时候在后面写一个大括号,在大括号里面重写run方法。

这相当于是创建了Thread类的子类,但是这个子类的没有名字,称之为匿名内部类。

(2)Runnable匿名内部类

public static void main(String[] args) {Runnable runnable = new Runnable() {@Overridepublic void run() {while (true) {System.out.println("hello thread");}}};Thread t = new Thread(runnable);t.start();while (true) {System.out.println("hello main");}}

与Thread内部类一样,除了多了一句Thread t = new Thread(runnable);代码


四、lambda表达式

这个方法是笔者认为最舒服,最方便的写法

它用到了一个函数式接口:() -> {}

Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");}});

 这个函数式接口要放在Thread的括号种,并且需要注意的是lambda表达式中的定义是在new Thread之前的,也就是在Thread t声明之前的


上面介绍了线程创建的三种方法,我们需要知道main也是一个线程,可以称之为主线程,我们按照以前的认知,可能会觉得main线程结束了,程序就结束了(只针对单线程程序),在多线程中其实main线程结束和其它线程没有什么关系,等到所有的线程都运行结束了,这个程序才结束。

一个程序可以创建多个线程,主线程是这个程序最先开始运行的(因为要在主线程中去启动其它线程),但不代表主线程运行完其它线程才开始,各个线程之间都是并发执行的方式(串行执行和并行执行混合使用)

Thread类的方法

给线程命名

Thread t1 = new Thread(() -> {while (true) {System.out.println("hello a");}}, "a");Thread t2 = new Thread(() -> {while (true) {System.out.println("hello b");}}, "b");

首先我们要明白,Thread类后面的t1不是线程的名字,这只是线程声明的时候创建的一个变量,用来存储线程的引用。

在Thread()的括号内,第二个参数用来设置这个线程的名字,此时第一个线程名字是a,第二个线程的名字是b。

我们可以通过javajdk自带的一个软件去查看系统中正在执行的线程,但这个只能去查看java代码编写的线程:

我们此时编写以下两个线程:

上述代码中加入了sleep操作,是为了每隔1秒打印一次,否则打印太快,占用cpu资源会很大

首先在你的电脑上找到你jdk安装的位置:默认是C:\Program Files\Java

然后选中你这个idea正在使用的jdk版本,笔者使用的是jdk-17

点开jdk-17后选中bin目录:

然后找到jconsole.exe这个程序:

点开之钱我们要确保我们的线程一直在运行,就是让上述代码一直运行

点开后选中这个代码所在的类:

点击连接后再选择不安全的连接:

点击线程:

可以看到我们创建的三个线程:

它们的名字分别是a、b、c


IsAlive()方法

可以判断系统中的线程是否存活

java代码中创建的Thread对象和系统中的线程是一一对应的,但Thread对象的生命周期和系统中的线程生命周期是不同的,可能存在系统中的线程已经销毁,但Thread类的对象还存在。

public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {//三秒后线程销毁for (int i = 0; i < 3; i++) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("线程结束");});//启动线程t.start();//等待t线程运行完毕主线程才开始运行t.join();System.out.println(t.isAlive());}

上述代码就表明了虽然t线程在系统中被销毁了,但是这个Thread类的对象t还可以使用,可以通过isAlive去判断系统中的这个线程是否存在。


setDaemon()方法

这个方法可以将前台线程改为后台线程

首先我们要先知道什么是前台线程,后台线程。

前台线程就是一个进程结束不结束的关键,假设在一个进程中有t1、t2、t3这三个线程,当这个三个线程都结束了,这个进程才会结束。

什么是后台线程呢?后台线程可以称之为守护线程,顾名思义,它要等到前台线程都结束了,它才会结束,而且它的结束也不会影响到这一整个进程的结束与否,后台线程起到辅助这个进程的作用。

能影响到进程的结束就是前台进程,当前台进程都结束了,后台进程才会结束

上面的图是线程监视控制台,其中a、b、c是我们创建的线程,称之为前台线程。除了它们其它的线程都是后台线程,这些是JVM提供的线程,这些线程具有特殊功能比如垃圾回收线程等,它们的存在不影响进程结束,它们会随着进程的结束而结束。

我们可以通过t.setDaemon(true)这行代码,将t线程从前台线程转为后台线程。

public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});//把t设为后台线程t.setDaemon(true);t.start();for (int i = 0; i < 3; i++) {System.out.println("hello main");Thread.sleep(1000);}System.out.println("main结束");}

currentThread()方法

这个方法是一个静态方法,通过Thread类调用后返回当前这个线程的实例,在哪个线程中调用就返回哪个线程的实例。

Thread.currentThread();

终止线程

终止线程表示让这个线程直接停止,不会恢复。

(1)使用全局变量(成员变量)

定义一个全局变量(成员变量):

private static boolean isFinished = false;

初始值设置为false,需要停止某一个线程时,将这个全局变量的值改为true

完整代码:

public class Demo7 {private static boolean isFinished = false;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!isFinished) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("thread进程结束");});t.start();Thread.sleep(3000);isFinished = true;}
}

这个代码表示在t线程中每隔1秒打印一次“hello thread”,在主线程中3秒后让t线程结束。

为什么一定要定义一个全局变量,而不能将isFinished变量放入main方法中当作局部变量呢?

这里涉及到一些很细的知识点:

“变量捕获”

在上述代码中,我们用lambda表达式创建的线程,在lambda表达式中,如果希望使用到表达式外面的变量,会触发“变量捕获”这样的语法,原因是lambda是回调函数,执行时机是很久之后,操作系统真正创建出线程后才会执行,很有可能main线程都执行完了,main方法中的变量都销毁了,此时这个变量isFinished就没了。所以为了解决这个问题,Java的做法是,将被捕获的变量复制一份到lambda中,后续lambda外面的变量是否销毁就无所谓了。

但在lambda表达式中对这个局部变量进行修改时会触发以下问题:

这是由于Java通过变量捕获,将这个局部变量复制了一份到lambda表达式中,当在lambda中修改这个变量时,会出现同一个变量有两份,值分别不同的问题。所以java不允许在lambda中修改捕获后的变量。

拷贝意味着这样的变量就不适合进行修改,修改一方,另一方不会随之变化,这种一边变,一边不变,非常的奇怪,所以Java这里就压根不允许你进行修改,不让修改我们就无法让线程进行终止,所以局部变量这个方法是不行的,必须去使用全局变量。

把局部变量改成成员变量后,不再是“变量捕获”语法,此时切换成了“内部类访问外部类的成员”语法。

lambda本质上是函数式接口,相当于一个内部类,isFinished变量本身就是外部类Demo7的成员

内部类本来就能访问外部类的成员,而且成员变量的生命周期是让GC(垃圾回收)来管理的,

所以在lambda里不用担心变量声明周期失效的问题,也就不必拷贝,不必限制final之类的。


(2)Thread提供interrupt()方法

Java中提供了现成的变量,直接进行判定即可。

需要以下两个方法配合使用:

interrupt()   //终止线程(设置标志位的值(boolean类型))

isInterrupted()  //判断线程是否被终止

这两个方法都需要线程的实例去调用

public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!Thread.currentThread().isInterrupted()) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {//throw new RuntimeException(e);break;}}System.out.println("终结成功");});t.start();Thread.sleep(3000);System.out.println("尝试终结t线程");t.interrupt();}

上述代码中,在lambda表达式中由于lambda表达式中的定义是在new Thread之前的,也就是在Thread t声明之前的,所以无法直接使用t去调用,但是可以通过:Thread.currentThread()

但是当我们去使用interrupt方法去主动进行终止时,代码会报出以下错误:

这是由于interrupt除了设置boolean变量(标志位),还能唤醒sleep这样的阻塞方法

sleep的时间还没结束,但被提前唤醒

当在t线程中正在sleep时,interrupted可以提前唤醒sleep,此时sleep会抛出异常

在我们的上述代码中,如果线程不结束我们每隔1秒打印一次输出到控制台,main线程中过了3秒会主动去终止t线程,此时的t线程可能刚好正在休眠,但被提前唤醒,由于try / catch原因报出sleep  interrupted的错误,但我们不希望报出错误,因为终止线程我们不需要去注意这个线程是否在休眠,我们希望该终止时立刻终止,所以只需将catch的代码块中将 throw 一个异常改为break即可

如果没有break,这个线程就不会结束,就会一直运行下去。

所以Java中的线程终止,不是一个强制性的措施,不是main让t终止,t就一定终止,选择权在t自己手上(catch中的代码)。

catch中可以有以下做法:

        1.加上break(立即终止)

        2.啥都不写(不终止)

        3.catch中先执行一些其它的逻辑再break(稍后终止)


线程休眠

Thread.sleep()

用Thread类调用sleep方法,括号内的参数是时间,单位是ms

调用此方法并传入时间,此时这个线程会休眠相应的时间,休眠意味着这个线程被操作系统“挂起”,此时这个线程不会占用CPU资源,操作系统可以将CPU资源分配给其他线程。

       public static void main(String[] args) throws InterruptedException {        Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();while (true) {System.out.println("hello main");Thread.sleep(1000);}}

上述代码中t线程每隔1000ms也就是1秒打印一次,主线程是每隔3秒打印一次

调用sleep方法需要处理异常,但是在lambda表达式中只能用try/catch进行处理,因为在lambda表达式中重写了run方法,重写的性质是只能改变方法内容,不能改变方法的外观(不能在重写的run方法后throw一个异常),在main方法中可以直接throw一个异常

其实在sleep的参数中传入1000ms,线程实际的休眠时间可能要比1000ms多一点,代码调用sleep相当于是让当前线程让出cpu资源,后续时间到了,操作系统重新把这个线程调到cpu上,才能继续执行。但是时间到,意味着这个线程允许被操作系统调度了,而不是立即执行了。


线程等待

需要用到join方法,它是Thread类的一个实例方法

t.join();

(1)无参join

join方法能够实现多个线程之间结束的先后顺序

比如在主线程中调用t.join()方法就是让主线程等待t线程结束后,主线程再开始运行

public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t线程结束");});t.start();t.join();System.out.println("main线程结束");}

上述代码表示,等到5秒后t线程结束,main线程才能开始执行


(2)带参join

join的括号内可以传入参数,比如在main方法中调用t.join(3000),表示main线程最多等待t线程3秒,3秒过后main线程也开始执行。main线程不管t线程有没有结束,最多等待它3秒。

public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t线程结束");});t.start();t.join(2000);System.out.println("main线程结束");}

上述代码表示main线程在等待t线程2秒后,开始运行,过了两秒后main线程结束,打印到控制台如下:


结束~~~


http://www.mrgr.cn/news/57158.html

相关文章:

  • Linux基础 -- GCC 工具链的 `-fstack-usage` 用法
  • Echarts提示框(tooltip)浮层显示不全
  • Leetcode—194. 转置文件【中等】(Shell)
  • 自动化测试基础知识总结
  • 滑线变阻器的工作原理是什么?
  • Java面试宝典-并发编程学习02
  • react优化
  • Napkins:开源 AI 开发工具,实现截图或线框图到网页应用的快速转换
  • konva不透明度,查找,显示,隐藏
  • vTESTstudio系列14--vTESTstudio中自定义函数介绍1
  • RHCE时间服务器
  • Vscode + EIDE +CortexDebug 调试Stm32(记录)
  • Kamailio 网络拓扑案例分享
  • C++ set和map的模拟实现
  • Llama Tutor:开源 AI 个性化学习平台,根据主题自动制定学习计划
  • RTDETR 引入 MogaBlock | 多阶门控聚合网络 | ICLR 2024
  • ThinkPad中键打开网页关闭网页失灵
  • 【Linux】线程互斥与同步,生产消费模型(超详解)
  • Redis-05 Redis发布订阅
  • 得物App3D博物馆亮相“两博会”,正品保障助力消费体验升级
  • 10.23Python_matplotlib_乱码问题
  • 三菱FX5U PLC程序容量设置
  • vue3-06-html2canvas使用 + zoom、transform: scale图片缩放适配方案 + 动态引入静态资源(打包上线后也能使用)
  • Java面试题九
  • C语言_动态内存管理
  • 2024年软件设计师中级(软考中级)详细笔记【10】网络与信息安全基础知识(分值5分)