JavaEE之多线程(创建线程的五种写法)详解

news2024/9/24 13:15:09

😽博主CSDN主页: 小源_😽

🖋️个人专栏: JavaEE

😀努力追逐大佬们的步伐~


目录

1. 前言 

2. 操作系统"内核"

3. 创建线程的五种写法 (我们重点要掌握最后一种写法!!)

3.1 继承 Thread, 重写 run

3. 2 实现 Runnable 接口, 重写 run

3.3 继承 Thread, 重写 run, 使用匿名内部类

3.4 实现 Runnable, 重写 run, 使用匿名内部类

3.5  [常用/推荐] 使用 lambda 表达式

4. 小结


1. 前言 

我们在写代码的时候, 可以使用多进程进行并发编程, 也可以使用多线程并发编程.

但是多进程并发编程在 Java 中不太推荐, 因为很多和多进程编程相关的 api 在 Java 标准库中都没有提供, 并且在上篇文章中我们讲解了多线程并发编程时效率更高(在需要频繁创建和销毁进程的时候), 并且对于 Java 进程, 需要启动 Java 虚拟机, 导致开销更大 (搞多个 Java 进程就是搞多个 Java 虚拟机)

系统提供了多线程编程的 api, Java 标准库中把这些 api 封装了, 在代码中可以直接使用. 我们重点学习 Thread 这样的类

本章重点

本文着重讲解了创建线程的五种写法


2. 操作系统"内核"

我们在学习创建线程之前, 需要先了解操作系统"内核", 它是操作系统中最核心的模块 (用来管理与硬件和给软件提供稳定的运行环境)

操作系统有两个状态: 内核态和用户态, 并且各有自己的空间 (内核空间, 用户空间)

比如我们平时运行的普通的应用程序 (如 idea, java, 画图板, qq音乐......) 都是运行在用户态的, 当操作这些从程序时, 不是应用程序直接操作的, 而是需要调用系统的 api, 在内核中完成操作

为什么要划分出这两个状态呢??

最主要的目的是为了 "稳定": 防止你的应用程序破坏硬件设备或者软件资源

系统封装了一些 api, 这些 api 都是一些合法的操作, 应用程序只能调用这些 api, 就不至于对系统火与硬件设备产生危害 (如果应用程序可以直接操作硬件, 极端情况下, 代码出现 bug, 可能把硬件烧坏)


3. 创建线程的五种写法 (我们重点要掌握最后一种写法!!)

每个线程都是一个独立的执行流, 每个线程都能够独立的去 cpu 上调度执行

3.1 继承 Thread, 重写 run

  1. 创建一个自己的类, 继承自这个 Thread
  2. 根据刚才的类, 创建出实例
  3. 调用 Thread 的 start 方法 
package thread;

// 1. 创建一个自己的类, 继承自这个 Thread
    // 这个 Thread 类能直接使用, 不需要导入包, 是因为 Java 标准库中, 有一个特殊的包 java.long, 和 String 类似 (也在 java.long 包中)
class MyThread extends Thread {
    // 这里重写的 run 入口方法必须手动指定, 针对原有的 Thread 进行扩展 (把一些能复用的复用, 需要扩展的扩展)
    @Override
    public void run() {
        // run 方法就是该线程的入口方法. 和 main 方法类似, main 方法是一个进程的入口方法 (也可以说 main 方法是主线程的入口方法)
        // 一个进程至少有一个线程, 这个进程中的第一个线程就叫做"主线程", 如果一个进程只有一个线程, 即 main 线程就是主线程
        System.out.println("hello world");
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        // 2. 根据刚才的类, 创建出实例. (线程实例,才是真正的线程).
        MyThread t = new MyThread();
        // Thread t = new MyThread();
        // 3. 调用 Thread 的 start 方法, 才会真正调用系统的 api, 在系统内核中创建出线程, 然后线程就会执行上面的 run 方法了
        t.start();
    }
}

按照之前的理解 (没有学习多线程之前), 如果一个代码出现了死循环, 最多只能执行一个, 另一个循环是进不去的, 下面我们来创建两个线程

package thread;

class MyThread2 extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello thread");

            // (我们使用的 sleep 是 Java 中封装后的版本, 是 Thread 提供的静态方法) 加 sleep 来降低循环的速度 (这里让 t 线程睡眠 1s (1000ms 等于 1s)), 先写第 19 行代码把鼠标指针放在 sleep 上, 按 Alt + Enter 即可
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class ThreadDemo2 {
    public static void main(String[] args) {
        Thread t = new MyThread2();

        t.start();

        while (true) {
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

两个线程都在执行, 互不干扰, 运行结果为: 

我们可以发现:  每一秒打印的顺序都是随机的, 这里涉及到一个重要结论, 当一个进程中有多个线程时, 这些线程执行的先后顺序, 是完全随机的. (因为操作系统内核中, 有一个"调度器" 模块, 这个模块的实现方式类似 "随机调度" 的效果)

什么是"随机调度"

  1. 一个进程什么时候被调度到 cpu 上执行的时机是不确定的
  2. 一个线程什么时候从 cpu 上下来, 给别人让位的时机也是不确定的

这是主流操作系统"抢占式执行"的体现, 但是给我们的多线程的安全问题埋下了伏笔

刚才我们只是通过打印的方式看到了两个执行流, 我们也可以使用一些第三方工具更直观地看到多个线程的情况

在 jdk 中, 有一个叫 jconsole 的工具 

选择本地进程中我们刚刚执行的 ThreadDemo2 代码, 然后直接连接即可, 

直接选择不安全的连接即可

线程是在正在不停的运行的, 当我们点击 Thread-0 线程的详细情况的一瞬间, 相当于"咔嚓"一个快照把这一瞬间的 Thread-0 线程的状态展示出来了 (再次点击时, 线程的详细情况可能会改变)

这里的"堆栈跟踪", 就是线程的调用栈, 描述了线程当前执行到哪个方法的第几行代码, 以及这个方法是如何一层一层调用过去的

除了 main 线程和 t 线程, 其余的线程都是 JVM 自带的线程, 完成一些垃圾回收, 以及监控统计各种指标 (如果我们的代码出现问题, 就可以从中提取一些参考和线索), 把统计指标通过网络的方式, 传输给其他程序,

 


3. 2 实现 Runnable 接口, 重写 run

只是实现接口时改变, 其余和上面的代码类似

package thread;

// Runnable 可理解为 "可执行的", 通过这个接口, 就可以抽象出一段可以被其他实体执行的代码
class MyThread3 implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello runnable");
            
            //睡眠 1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyThread3());

        t.start();

        while (true) {
            System.out.println("hello main");

            //睡眠 1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

3.3 继承 Thread, 重写 run, 使用匿名内部类

内部类是在一个类里面定义的类, 最多使用的就是匿名内部类 (这个类没有名字, 不能重复使用, "用一次就扔掉")

package thread;

public class ThreadDemo4 {
    public static void main(String[] args) {
        // 写 { 是因为要定义一个新的类, 继承自 Thread, {} 中定义子类的属性和方法, 此处最主要的目的是重写 run 方法
        // t 指向的实例不是单纯的 Thread, 而是新定义的匿名内部类 (Thread 的子类)
        Thread t = new Thread() {
            @Override
            public void run() {
                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");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

3.4 实现 Runnable, 重写 run, 使用匿名内部类

package thread;

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            // Thread 构造的方法的参数, 填写了 Runnable 的匿名内部类的实例
            @Override
            public void run() {
                while (true) {
                    System.out.println("hello runnable");
                    
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        
        t.start();

        while (true) {
            System.out.println("hello main");
            
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

3.5  [常用/推荐] 使用 lambda 表达式

这是最简洁的写法

lambda 主流语言都有: c++, Python 中叫做 lambda, JS, GO 直接叫做匿名函数

因为方法不能脱离类单独存在, 所以导致上面几种方法为了设置回调函数 而套上了一层类

因此引入了 lambda 表达式 (就是一个匿名函数/方法), Java语法首创, 函数式接口属于 lambda 背后的实现, 相当于在没有破坏原有规则 (方法不能脱离类单独存在) 的基础上, 给了lambda 一个合理的解释

package thread;

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            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");
            
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

4. 小结

上述的5种写法都是等价的, 可以互相转换的, 用 lambda 表达式是我们最常用, 最推荐, 最简洁的写法


最后,祝大家天天开心,更上一层楼!关注我🌹,我会持续更新学习分享...🖋️

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot Servlet容器启动解析

介绍 容器架构 容器处理请求 容器启动全局流程解析 启动前准备 WebServer创建入口 WebServer创建 Servlet启动 Web容器工厂类加载解析 Web容器个性化配置 属性注入 工厂类初始化 BeanPostProcessor方法实现 定制化流程 面试题 请描述下Servlet容器启动流程?介绍下…

最新android icon和splashScreen适配兼容至2024android

android在12做了splashScreen的变动,即,android12有自带的screenSplash过渡,不论你是否自己有变化,都会插入该动画。 android8做了icon的巨大变动。13做了图标的主题兼容。 一、icon制作 制作 使用android自带的工具&#xff0…

Modbus -tcp协议使用第二版

1.1 协议描述 1.1.1 总体通信结构 MODBUS TCP/IP 的通信系统可以包括不同类型的设备: (1)连接至 TCP/IP 网络的 MODBUS TCP/IP 客户机和服务器设备; (2)互连设备,例如:在 TCP/IP…

外包干了5天,技术明显退步。。。。。

先说一下自己的情况,本科生,19年通过校招进入南京某软件公司,干了接近2年的功能测试,今年年初,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了2年的功能测试&…

C# Onnx yolov8 building segmentation

目录 效果 模型信息 项目 代码 下载 C# Onnx yolov8 building segmentation 效果 模型信息 Model Properties ------------------------- date:2023-12-22T10:51:07.627471 author:Ultralytics task:segment license:AGPL-…

HBase分布式数据库的原理和架构

一、HBase简介 HBase是是一个高性能、高可靠性、面向列的分布式数据库,它是为了在廉价的硬件集群上存储大规模数据而设计的。HBase利用Hadoop HDFS作为其文件存储系统,且Hbase是基于Zookeeper的。 二、HBase架构 *图片引用 Hbase采用Master/Slave架构…

YOLOv9(3):YOLOv9损失(Loss)计算

1. 写在前面 YOLOv9的Loss计算与YOLOv8如出一辙,仅存在略微的差异。多说一句,数据的预处理和导入方式都是一样的。因此如果你已经对YOLOv8了解的比较透彻,那么对于YOLOv9你也只是需要多关注网络结构就可以。 YOLOv9本身也是Anchor-Free的&a…

彩虹外链网盘界面UI美化版超级简洁好看

彩虹外链网盘界面UI美化版 彩虹外链网盘,是一款PHP网盘与外链分享程序,支持所有格式文件的上传,可以生成文件外链、图片外链、音乐视频外链,生成外链同时自动生成相应的UBB代码和HTML代码,还可支持文本、图片、音乐、…

机试:成绩排名

问题描述: 代码示例: #include <bits/stdc.h> using namespace std;int main(){cout << "样例输入" << endl; int n;int m;cin >> n;int nums[n];for(int i 0; i < n; i){cin >> nums[i];}// 排序for(int i 0; i < n; i){//…

Leetcode 3.14

Leetcode hot100 二叉树1.二叉树的层序遍历2.验证二叉搜索树3.二叉树的右视图 二叉树 1.二叉树的层序遍历 二叉树的层序遍历 二叉树的层序遍历可以用先进先出的队列来实现。 将每一层的所有node都添加到队列中&#xff0c;记录下当前队列的长度&#xff0c;即该层的元素数量&…

Java操作Sql语句 出现迭代死循环 (Bug排查)

目录 1. 问题所示2. 原理分析3. 解决方法4. 彩蛋1. 问题所示 Java执行Sql语句来查询一些数据的时候 虽说数据量很大,但是查询过程中一直迭代查询 截图如下所示: 2. 原理分析 至于迭代死循环,可能是不满足的条件也进入查询(本身我的数据量就很大) 主要可能引起的两个原…

创新发展,探索智慧园区平台架构设计与实现

随着信息技术的快速发展&#xff0c;智慧园区平台作为集成物联网、大数据、人工智能等技术的综合性服务平台&#xff0c;正逐步成为推动企业数字化转型的重要驱动力。本文将深入探讨智慧园区平台的架构设计思路、关键技术和应用场景&#xff0c;助力读者了解如何打造智慧化、协…

【洛谷 P8637】[蓝桥杯 2016 省 B] 交换瓶子 题解(贪心算法)

[蓝桥杯 2016 省 B] 交换瓶子 题目描述 有 N N N 个瓶子&#xff0c;编号 1 ∼ N 1 \sim N 1∼N&#xff0c;放在架子上。 比如有 5 5 5 个瓶子&#xff1a; 2 , 1 , 3 , 5 , 4 2,1,3,5,4 2,1,3,5,4 要求每次拿起 2 2 2 个瓶子&#xff0c;交换它们的位置。 经过若干次…

linux查看服务器登录成功和登录失败的命令

last 查看成功登录服务器的信息&#xff0c;包括ip&#xff0c;时间&#xff0c;登录用户&#xff0c;时长。lastb 查看登录服务器失败的信息。 last命令实例&#xff1a; 其他参数&#xff1a; -a&#xff1a;把从何处登入系统的主机名称或ip地址&#xff0c;显示在最后一行…

sqllab第十一关通关笔记

知识点&#xff1a; 发现登录框就可以尝试注入登录框一般都是字符型注入通过注入可以获取其他表的信息绕过手段 单引号闭合联合注入也可以进行错误注入 首先看界面是一个登录框&#xff1b;通过admin admin登录进去&#xff0c;发现页面会把用户名和密码的登录信息打印出来&am…

Node.js入门基础—day01

个人名片&#xff1a; &#x1f60a;作者简介&#xff1a;一名大二在校生 &#x1f921; 个人主页&#xff1a;坠入暮云间x &#x1f43c;座右铭&#xff1a;给自己一个梦想&#xff0c;给世界一个惊喜。 &#x1f385;**学习目标: 坚持每一次的学习打卡 文章目录 初识node.js什…

sqllab第二十一关通关笔记

知识点&#xff1a; 错误注入 最大长度为32超过需要利用截取函数分段读取cookie注入base64加密会保留符号的原始属性 通过admin admin进行登录发现和第二十关显示的内容一样&#xff0c;猜测应该还是cookie注入&#xff1b; 直接截取带有cookie的数据包&#xff0c;发现uname…

使用canvas实现图纸标记及回显

图纸 图纸标记后的效果图 最近做的一个qms项目里面&#xff0c;需要前端在图纸上实现标记及标记后的内容还要能够回显&#xff0c;然后后端通过标记的点&#xff0c;去读取标记图纸的内容&#xff0c;如一些公式、数据之类的&#xff0c;目前实现的功能有 在图纸上面进行矩形…

整型变量的原子操作

什么是原子操作 原子操作&#xff08;Atomic Operation&#xff09;是指不可中断的操作&#xff0c;即在多线程环境下&#xff0c;当一个线程在执行原子操作时&#xff0c;不会被其他线程的调度和中断所影响。这种操作在多线程编程中尤为重要&#xff0c;因为它能保证操作的原…

根据服务器系统选择对应的MySQL版本

1. 根据服务器系统选择对应的MySQL版本 MySQL有多个版本&#xff0c;选择对应的版本&#xff0c;重点信息是Linux的GLIBC版本号&#xff0c;Linux的版本、系统位数。 1.1 查看Linux的GLIBC版本号 通常libc.so会支持多个版本&#xff0c;即向前兼容&#xff0c;查看该文件中…