【多线程】认识Thread类及其常用方法

📄前言
本文是对以往多线程学习中 Thread类 的介绍,以及对其中的部分细节问题进行总结。


文章目录

  • 一. 线程的 创建和启动
    • 🍆1. 通过继承 Thread 类创建线程
    • 🍅2. 通过实现 Runnable 接口创建线程
    • 🥦3. 其他方法创建线程(本质上为上面两种写法的变形)
      • 🥑3.1 使用 Thread 的匿名内部类
      • 🥬3.2 使用匿名内部类实现 Runnable 接口
      • 🥒3.3 使用 lambda 表达式实现 Runnable 接口 (推荐,更加简洁!!!)
    • 🍉4. 线程的启动(关于 start方法 和 run方法 的思考)
  • 二. Thread类的属性和常用方法
    • 🍚1. Thread类的主要属性
    • 🍥2. 线程休眠
    • 🍭3. 线程等待
    • 🍦 4. 获取线程实例
    • 🧊5. 线程的中断 (关于线程中断的细节!!!)

之前的文章介绍过线程的引入能够更好地处理程序的并发执行问题。在Java中,线程的创建方式之一是通过 Thead类 (Thead封装了操作系统提供的API,使我们创建的线程能够系统的调度)。接下来我们先了解一下线程的创建方法。

一. 线程的 创建和启动

通过 Thread类 创建线程的方式总体来说可以分为以下两种:

  1. 继承 Thread类,重写类中的 run() 方法
  2. 实例化Thread类,实现 Runnable 接口,重写接口中的 run() 方法(通过Thread的构造方法传参 间接重写run() 方法)

这里我们应该可以发现一个共同点,这两种方法都需要重写 run() 方法,毫无疑问 run() 方法就是线程创建后执行代码逻辑的 “入口”方法,通过查看源码我们发现 Thread类 继承了Runnable,而 run() 方法就是该接口中的抽象方法。(如下)
在这里插入图片描述
在这里插入图片描述

🍆1. 通过继承 Thread 类创建线程

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("hello thread!");
        }
        System.out.println("线程结束!");
    }
}

🍅2. 通过实现 Runnable 接口创建线程

在这里插入图片描述

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("hello thread !");
        }
        System.out.println("线程结束 !");
    }
}

public class Demo2 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new MyRunnable());
    }

}

🥦3. 其他方法创建线程(本质上为上面两种写法的变形)

🥑3.1 使用 Thread 的匿名内部类

Thread t = new Thread(){
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("hello thread !");
        }
        
        System.out.println("线程结束 !");
    }
};

🥬3.2 使用匿名内部类实现 Runnable 接口

Thread t = new Thread(new Runnable() {
   @Override
    public void run() {
		while(true) {
		    System.out.println("hello thread !");
		}
		
		System.out.println("线程结束 !");
   	}
});

🥒3.3 使用 lambda 表达式实现 Runnable 接口 (推荐,更加简洁!!!)

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

🍉4. 线程的启动(关于 start方法 和 run方法 的思考)

其实线程的启动方式很简单,直接调用 Thread类中的 start()方法 即可启动线程,为了观察新创建的线程和 “主线程main” 的并发执行效果,代码中调用 Thread类的类方法 sleep(), 让每次语句执行后休眠 1s。(代码及程序运行效果如下)

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.start();
	
	// 主线程
	for(int i = 0; i < 5; i++) {
	    System.out.println("hello main !");
	    Thread.sleep(1000);
	}
}

在这里插入图片描述

通过上面代码的执行结果我们容易知道:创建的线程执行的代码其实就是 run() 方法中的代码,那么我们是否可以通过执行 t.run() 来代替 t.start() 呢?
答案很明显是否定的,因为直接调用 run() 方法本质上与一个自定义函数的调用并无任何差异。我们都知道,在程序的某处调用一个函数,程序会在该函数执行结束后才继续执行后面的代码,因此不能使用 t.run() 来代替 t.start()。
总结:调用run() 方法只是一次简单的函数调用,程序依旧是顺序执行;只有调用 start() 才能在操作系统真正创建一个主线程并发执行的线程


二. Thread类的属性和常用方法

🍚1. Thread类的主要属性

在这里插入图片描述
注意:所有创建的线程默认为前台线程,只有当所有前台线程运行结束后,程序才会结束,主线程main运行结束,不会影响其他前台线程的运行。(可以通过setDaemon()方法将线程设置为后台线程)

🍥2. 线程休眠

在这里插入图片描述
代码示例及程序运行结果如下:让新线程每1s进行一次打印,主线程每2s进行一次打印(注意:使用sleep()方法需要处理可能抛出的异常)

public static void main(String[] args) {
        
	Thread t = new Thread(() -> {
	    for (int i = 0; i < 6; i++) {
	        System.out.println("hello thread !");
	        try {
	            Thread.sleep(1000);
	        } catch (InterruptedException e) {
	            e.printStackTrace();
	        }
	    }
	});
	t.start();
	
	for (int i = 0; i < 3; i++) {
	    System.out.println("hello main !");
	    try {
	        Thread.sleep(2000);
	    } catch (InterruptedException e) {
	        e.printStackTrace();
	    }
	}
}

在这里插入图片描述

🍭3. 线程等待

在这里插入图片描述
代码实例和程序运行结果如下:让主线程进行一次打印后,等待新线程结束再继续运行。

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        for (int i = 0; i < 5; i++) {
            System.out.println("hello thread !");
        }
    });
    t.start();

    for (int i = 0; i < 5; i++) {
        System.out.println("hello main !");
        if(i == 0) {
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

在这里插入图片描述

🍦 4. 获取线程实例

在线程的创建过程中,由于 Thread类 还没有被实例化,因此不能通过类对象的引用变量直接得到该对象,而应该使用 Thread类提供的方法得到该对象的引用。
在这里插入图片描述

🧊5. 线程的中断 (关于线程中断的细节!!!)

在正式了解线程中断的方法之前,我们需要知道一个基本的事实:
Java中线程的中断方式并不是让一个正在运行的线程直接中止,而是以一种 “通知的方式” 告诉线程,“你当前应该结束线程了”,而具体是否立即结束当前该线程,由线程接收通知后的代码处理逻辑决定

==========

关于线程的中断方法,Thread类 提供了以下三个方法:
在这里插入图片描述

Thread类收到通知的方式有以下两种情况:

  1. 当前线程因 sleep/join/wait 等方法引起阻塞而挂起,以 InterruptedException 异常的形式通知,并清除中断标志,此时线程的阻塞状态立即结束,而是否立即结束由 try/catch 中catch的处理逻辑决定。(注意:线程是否有后续的处理权,取决于该线程是否有能引起阻塞状态的代码
  2. 当前线程处于正常运行状态,将中断标志位设置为true。

==========

  1. 当线程处于阻塞状态时,存在以下三种对应的处理方式:
    1)不管这个通知,继续运行。 2)直接结束线程。 3)进行一些特定的收尾工作再结束线程
public static void main(String[] args) {
    Thread t = new Thread(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("hello thread !");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 1. 无视通知
                e.printStackTrace();

                // 2. 直接退出
                // break;

                // 3. 进行收尾工作,再退出
                // System.out.println("此处是后续的处理代码");
                // break;
            }
        }
    });
    t.start();

    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t.interrupt();
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

看到第一个程序运行的结果,不知道大家会不会有一个疑惑,使用 isInterrupted()方法是不会清除标志位的,那 3s过后该线程不应该只打印3次 “hello thread !”,接着由于循环的判断条件为 false 而直接退出吗?
其实 isInterrupted() 方法调用后确实不会清除标志位,程序继续运行的原因是因为3s后主线程调用interrupt()方法,解除了 t线程 的阻塞状态,同时 sleep() 引起的异常会顺便清除标志位。

因此,如果将代码改为下面的写法,线程会在当前代码逻辑执行完毕 或 进入下一次循环判断后直接退出。

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("hello thread !");
        }
        System.out.println("---3s后 t线程退出---");
    });
    t.start();

    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t.interrupt();
}

程序运行结果如下:
在这里插入图片描述

======

  1. 当前线程处于正常运行状态,将中断标志位置为true。
    从前面我们可以知道:Thread.interrupted() 判断后会清除标志位,isInterrupted() 不会清除标志位,现有以下代码:
private static int count = 100;

public static void main(String[] args) {
    Thread t = new Thread(() -> {
       for (int i = 1; count > 0 || Thread.currentThread().isInterrupted(); i++) {
           if(Thread.currentThread().isInterrupted()) {
               System.out.println("我当前收到中断通知了,这是第 " + i + " 次打印");
           } else {
               System.out.println("我当前没有收到中断通知, 这是第 " + i + " 次打印");
           }
           count--;
           if(i == 150) break;
       }
    });
    t.start();


    try {
        Thread.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    t.interrupt();
}

程序的运行结果如下:
在这里插入图片描述
这段代码的运行结果是:程序在第58次打印时标志位发生了改变,并且后续的打印不再发生改变,可以预见的是:如果没有使用 i 的值判断使循环退出,程序将会无终止地进行打印。原因就是 isInterrupted()方法 不会清除标志位,因此判断条件永远为真

若把循环中的方法修改为 Thread.interrupted(),程序的运行结果就发生了改变:

private static int count = 50;

public static void main(String[] args) {
    Thread t = new Thread(() -> {
       for (int i = 0; count > 0 || Thread.interrupted(); i++) {
           if(Thread.interrupted()) {
               System.out.println("我当前收到中断通知了,这是第 " + i + " 次打印");
           } else {
               System.out.println("我当前没有收到中断通知, 这是第 " + i + " 次打印");
           }
           count--;
       }
    });
    t.start();


    try {
        Thread.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    t.interrupt();
}

在这里插入图片描述
可以看到在第27次打印时标志位发生了改变,说明此时线程收到了中断通知,但后面的打印内容为“没有收到通知”,且程序最终打印了50次便结束了,这都说明了调用 Thread,interrupted()方法 后标志位被清除了


以上就是本篇文章的全部内容了,如果这篇文章对你有些许帮助,你的点赞、收藏和评论就是对我最大的支持。
另外,文章可能存在许多不足之处,也希望你可以给我一点小小的建议,我会努力检查并改进。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/331614.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue3的创建及认识

1、创建项目 使用creat-vue搭建vue3项目 2、认识creat-vue create-vue是Vue官方新的脚手架工具&#xff0c;底层切换到了 vite &#xff08;下一代前端工具链&#xff09;&#xff0c;为开发提供极速响应 3、创建create-vue项目 npm init vuelatest 4、认识vue3 首先熟悉一下v…

使用docker部署RStudio容器并结合内网穿透实现公网访问

文章目录 前言1. 安装RStudio Server2. 本地访问3. Linux 安装cpolar4. 配置RStudio server公网访问地址5. 公网远程访问RStudio6. 固定RStudio公网地址 前言 RStudio Server 使你能够在 Linux 服务器上运行你所熟悉和喜爱的 RStudio IDE&#xff0c;并通过 Web 浏览器进行访问…

电视台频道太多要管理

下午看了黑龙江卫视五个频道&#xff0c;三个在播医疗广告&#xff0c;二个在播没头尾的电视剧。绞尽脑汁做不出节目就不要搞很多频道。但话说回来&#xff0c;若各个频道都同时充斥精彩节目&#xff0c;观众眼花缭乱了&#xff0c;也看不过来&#xff0c;结果乱调台&#xff0…

算法之【前缀和】讲解

前言&#xff1a; 我们首先要明白何前缀和&#xff1f; 前缀和就是快速求出数组中某一个连续区间的和。算法的时间复杂度会将一个等级&#xff01; 本文章主要讲解前缀和模板&#xff0c;分别为一维前缀和和二维前缀和。 一维前缀和&#xff1a; 第一步&#xff1a;预处理…

抖音矩阵云混剪系统源码(免授权版)多平台多账号一站式管理,附带系统搭建教程

搭建教程 MySQL 5.6 PHP 7.2 Apache 数据库名称 juzhen Nginx环境切换伪静态 1、解压安装包到项目根目录&#xff0c;找到application/database.php 更换自己的数据库密码 2、阿里云现有的配置不要动 其他按照文档进行添加 3、项目访问目录&#xff1a;public 4、域名…

数组练习 Leetcode 566.重塑矩阵

在 MATLAB 中&#xff0c;有一个非常有用的函数 reshape &#xff0c;它可以将一个 m x n 矩阵重塑为另一个大小不同&#xff08;r x c&#xff09;的新矩阵&#xff0c;但保留其原始数据。 给你一个由二维数组 mat 表示的 m x n 矩阵&#xff0c;以及两个正整数 r 和 c &#…

【Git】常用的Git操作集合

常用的Git操作集合 1. 分支操作1.1 查看本地所有分支git branch 1.2 查看所有分支&#xff08;包含本地远程仓库&#xff09;git branch -a 1.3 切换分支git checkout test 2. 常用基本操作2.1 查看 git 各存储区内(文件)状态git status 2.2 查看工作区与暂存区文件差异git dif…

半波整流电路原理详解+参数与计算公式

什么是半波整流电路&#xff1f; 半波整流电路的基本操作非常简单&#xff0c;输入信号通过二极管&#xff0c;由于只能通过一个方向的电流&#xff0c;二极管的整流作用&#xff0c;单个二极管只允许通过一半的波形。 下图说明了半波整流电路的基本原理。 半波整流电路工作图…

ping github 请求超时 100%丢失

1&#xff0c;现象&#xff1a;github网站打不开 作为程序员&#xff0c;经常需要从github上下载东西&#xff0c;这次想要下载nvm&#xff08;Node版本管理器&#xff09;&#xff0c;发现不行&#xff0c;网页打不开。 网上各种找&#xff0c;说要ping&#xff0c…

大模型重构千行百业,寻找数智化的春天 | 2024 AI科技峰会

文&#xff5c;郝鑫、黄小艺 刚刚过去的2023年&#xff0c;注定是不平凡的一年。 年初ChatGPT点燃了引线之后&#xff0c;百模大战、文生图应用爆火、AI Agent风靡......从IT基础设施&#xff0c;到千行百业&#xff0c;AI都在马不停蹄地重构一切&#xff0c;进入狂奔时代。 …

SPI传感器接口设计与优化:基于STM32的实践

SPI&#xff08;串行外设接口&#xff09;是一种常用的串行通信协议&#xff0c;用于在微控制器和外部设备之间进行全双工的高速数据传输。在本文中&#xff0c;我们将探讨如何基于STM32微控制器设计和优化SPI传感器接口&#xff0c;并提供相应的代码示例。 1. SPI传感器接口设…

分享 GitHub 上的敏感词汇工具类:sensitive-word

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

Unity之铰链关节和弹簧组件

《今天闪电侠他回来了&#xff0c;这一次他要拿回属于他的一切》 目录 &#x1f4d5;一、铰链关节组件HingeJoint 1. 实例 2. 铰链关节的坐标属性 ​3.铰链关节的马达属性Motor &#x1f4d5;二、弹簧组件 &#x1f4d5;三、杂谈 一、铰链关节组件HingeJoint 1. 实例 说…

PyQt5零基础入门(四)——信号与槽

信号与槽 前言信号与槽单对单直接连接使用lambda表达式 信号与槽多对多一个信号连接多个槽多个信号连接一个槽信号与信号连接 自定义信号 前言 PyQt5的信号与槽是一种对象之间的通信机制&#xff0c;允许一个QObject对象发出信号&#xff0c;与之相连接的槽函数将会自动执行。…

[STM32F407ZET6] GPIO

GPIO模式 F4的GPIO功能比F1的功能更多一些, 但是整体框架一样. F4的输出配置和F1的不同, F4的配置后, 施密特触发器将会开启, 还会对输入寄存器进行采样读取. F1的配置后, 推挽输出将会关闭施密特触发器, 开漏模式读取会读输入寄存器, 推挽模式会读取输出寄存器的值. 输出(全…

贪心算法-活动安排-最详细注释解析

贪心算法-活动安排-最详细注释解析 题目&#xff1a; 学校在最近几天有n个活动&#xff0c;这些活动都需要使用学校的大礼堂&#xff0c;在同一时间&#xff0c;礼堂只能被一个活动使用。由于有些活动时间上有冲突&#xff0c;学校办公室人员只好让一些活动放弃使用礼堂而使用…

Three.js 学习笔记之模型(学习中1.18更新)

文章目录 模型 几何体 材质模型点模型Points - 用于显示点线模型Line | LineLoop | LineSegments网格模型mesh - 三角形 几何体BufferGeometry缓冲类型几何体BufferGeometry - 基类创建几何体的方式BufferAttribute Types定义顶点法线 geometry.attributes.normal BufferGeom…

CSS实现的 Loading 效果

方式一、纯CSS实现 代码&#xff1a;根据需要复制 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>CSS Animation Library for Developers and Ninjas</title><style>/* ---------------…

阿里云服务器哪个地域好?地域选择的影响因素与建议

阿里云服务器地域和可用区怎么选择&#xff1f;地域是指云服务器所在物理数据中心的位置&#xff0c;地域选择就近选择&#xff0c;访客距离地域所在城市越近网络延迟越低&#xff0c;速度就越快&#xff1b;可用区是指同一个地域下&#xff0c;网络和电力相互独立的区域&#…