「JavaEE」线程

news2024/12/25 9:22:16

🎇个人主页:Ice_Sugar_7
🎇所属专栏:JavaEE
🎇欢迎点赞收藏加关注哦!

线程

  • 🍉线程
    • 🍌多线程
    • 🍌线程与进程的联系&区别
    • 🍌多线程编程
    • 🍌创建线程
    • 🍌Thread 其他重要属性与方法
  • 🍉操作系统内核

🍉线程

上篇文章中我们介绍了进程,但实际上在Java中是不太鼓励“多进程编程”的,大多数时候我们使用的是线程

进程可以很好地解决并发编程这样的问题,但是在一些特定的情况下,它的表现不尽人意。比如有些场景需要频繁创建和销毁进程,此时使用多进程编程的话,系统开销就会很大

开销是哪来的呢?一个进程刚启动时,需要把依赖的代码和数据从磁盘加载到内存中。而从系统分配一块内存并非一件易事,一般申请内存的时候需要先指定一个大小,然后系统内部把各种大小的空闲内存通过一定的数据结构组织起来,这个过程需要一定的时间开销

而线程就是解决上述问题的方案
线程也可以称为轻量级进程,它在进程的基础上做出改进

前面我们说一个进程是由 PCB 来描述的;其实 PCB 也可以用来描述一个线程

PCB 中有个属性,叫内存指针。多个线程 PCB 的内存指针指向的是同一个内存空间
这意味着在创建第一个线程的时候就需要从系统分配资源。后续的线程就不必再分配,直接共用前面那份资源就 ok 了
除了内存,文件描述符表也是多个线程共用一份的(共享经济属于是)

在这里插入图片描述
当然也不是随便两个线程都能共享资源,我们把能够共享资源的线程分成组,称为线程组
而一个进程可以有多个PCB,这就意味着这个进程包含了一个线程组(多个线程)


🍌多线程

多线程是指在同一个进程中同时运行多个线程,每个线程可以执行独立的任务并且能够同时运行,这样同时执行多个任务可以提高程序的性能和响应速度

但是线程也不是越多越好,当线程数量太多的时候,线程之间就会相互竞争 CPU 的资源(因为 CPU 调度执行线程的数量是有限的),导致不仅不会提高效率,还会增加调度的开销

而且多线程还有一个问题,就是线程之间可能会起冲突,这就会导致代码中出现一些逻辑上的错误(这是后面要讨论的线程安全问题);一个线程如果抛出异常,并且没有处理好,就可能导致整个进程终止


🍌线程与进程的联系&区别

  1. 进程是包含线程的
  2. 每个线程是一个独立的执行流,可以执行一些代码,并且单独参与到 CPU 调度中
  3. 每个进程有自己的资源,进程中的线程共享这一份资源(内存空间、文件描述符表等)

由2和3可以得出:进程是资源分配的基本单位;线程是调度执行的基本单位

  1. 进程与进程之间不会相互影响,但是线程会(线程安全问题)。如果同一个进程中的某个线程抛出异常,可能会影响到其他线程,甚至会导致整个进程中所有线程都异常终止
  2. 线程不是越多越好,差不多就得了,如果线程太多了,调度开销可能非常明显

🍌多线程编程

在Java中,写代码的时候推荐使用多线程并发编程,系统提供了多线程编程的 api,而Java标准库把这些 api 封装好了,在代码中就可以直接使用,比如Thread类

打开 idea,我们先写一个 MyThread 类继承 Thread,并写一个 run 方法:
在这里插入图片描述

这个 run 方法就类似于 main 方法,是一个 Java 线程的入口方法。一个进程中至少有一个线程,这个进程的第一个线程,称为主线程,所以 main 方法也就是主线程的入口方法(因为一个进程肯定要有一个 main 方法)

然后还有一点,就是 run 是不需要我们手动调用的,它会在合适的时机(线程创建好之后)被 jvm 自动调用执行(这样的函数称为回调函数)
我们前面所学的优先级队列,往它插入个对象,需要先指定比较规则,这就要实现 Comparable 或者 Comparator 接口,分别重写 compareTo 和 compare 方法,这两个也属于回调函数

说回正题,现在要搞一个线程,就是要让这个线程执行一些代码。显然,标准库自带的 run 肯定是不知道我们的需求,这就需要我们进行拓展(Thread 类有很多属性、方法,大部分都可以复用,只用把需要拓展的进行拓展即可)

我们重写一下 run 方法,并创建一个线程:

public class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("hello thread");
    }

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

那么现在上面的代码就有两个线程:t 线程和 main 线程
每个线程都是一个独立的执行流,它们都能独立去 CPU 上调度执行
以上面代码为例,现在稍微修改一下,两个线程都加个死循环:

public class MyThread extends Thread{
    @Override
    public void run() {
        while(true) {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000); //控制隔一秒才打印,降低循环速度,避免循环跑起来的时候跑太快,导致 CPU 占用率比较高
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t = new MyThread();
        t.start();
        while(true) {
            System.out.println("hello main");
            sleep(1000);
        }
    }
}

运行结果如下图

在这里插入图片描述

可以看到两个循环都在执行,因为这两个线程就是两个独立的执行流
具体的执行流程就是:在 main 方法中调用 start 创建线程之后“兵分两路”,一路沿着 main 方法继续执行,打印“hello main”,另一路进入到线程的 run 方法,打印“hello thread”

然后有个需要注意的点,当有多个线程的时候,这些线程执行的先后顺序是不确定的,这是因为操作系统内核中有一个“调度器”模块,这个模块的实现了一种类似“随机调度”的效果。所谓的随机调度,指的是:
①一个线程被调度到 CPU 上执行的时机是不确定的
②一个线程从 CPU 上下来,给其他线程让位的时机也是不确定的

这两点其实归因于线程执行采用抢占式执行的机制:操作系统根据优先级等参数来决定何时中断当前线程,并切换到其他线程
这个机制使得多线程程序可以更好地利用 CPU 资源,增加并发性和吞吐量,但是也带来了线程安全问题

还是以上面的代码为例,别看是先进入 main 方法就以为是先执行 main 线程,其实它和 thread 谁先谁后是不确定的


🍌创建线程

上面介绍了一种创建线程的方式,不过那不是主流的方式。我们通常使用 lambda 表达式创建一个线程

        Thread t1 = new Thread(()-> {
            System.out.println("hello thread");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t.start();

这个写法相当于实现 Runnable 接口并重写 run 方法,lambda 代替了 Runnable 的位置


🍌Thread 其他重要属性与方法

方法

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

我们自己创建的线程默认是按照 Thread-0 1 2……命名的,给不同线程起不同名字对于线程的执行没有影响,主要是方便调试。此外,线程之间的名字是可以重复的,但名字别乱起,最好要有一定的描述性

属性

属性获取方法
ID(jvm自动分配的身份标识,会保证唯一性)getID()
名称getName()
状态(进程有就绪状态,阻塞状态等,线程也有状态)getState()
优先级getPriority()
是否为后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

(为了让表格看上去不会冗杂,一些属性的说明放到这下面讲)
优先级:在 Java 中,由于系统是随机调度线程的,所以对线程设置优先级的效果不是很明显

后台线程:后台线程的运行不会阻止进程结束,与后台线程相对,还有前台线程,前台进程的运行,会阻止进程结束(注意这里的后台和我们平时手机的“杀后台”不是一回事)

我们来演示一下前台线程,只需把刚才代码中 main 线程的死循环去掉:

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

在这里插入图片描述

进程执行后,会一直打印,只有当我们停止进程后,出现红色方框中这句话,才表示进程结束
这是因为我们创建的线程默认是前台线程,即使 main 已经执行完了,只要前台线程没执行完,进程就不会结束

然后我们把 t 改为后台线程:

        t.setDaemon(true); //设为 true 就是改为后台,注意 setDaemon 一定要写在start前面
        t.start();

在这里插入图片描述
可以看到什么都没打印,进程就结束了

isAlive:它表示内核中的线程(PCB)是否还存在。如果线程已经启动并且还没有终止,那就会返回 true;反之返回 false

Java 代码中定义的线程实例虽然表示一个线程,但是这个实例本身的生命周期和内核中 PCB 的生命周期是不完全一样的

Thread t = new Thread(()-> {
	...
})

比如现在创建了 t 实例,由于线程还没有 start,所以此时 isAlive 的结果就是 false


🍉操作系统内核

我们在上文中多次提到“内核”这个概念

内核是操作系统中最核心部分的功能模块,它负责管理硬件,给软件提供稳定的运行环境
操作系统的内存空间分为两块:内核空间(内核态)和用户空间(用户态)

为什么要划分出这两个空间呢?主要是为了稳定,防止应用程序把硬件设备或软件资源搞坏了。系统封装了一些 api,这些 api 都是一些合法的操作,应用程序只能调用这些 api,这样就不至于对系统以及硬件设备产生太大危害

我们平时运行的普通应用程序,比如 idea、谷歌、微信……都是在用户态运行的。这些程序有时候需要针对一些系统提供的软硬件资源进行操作,这些操作都不是应用程序直接操作的,需要调用系统提供的 api,然后在内核中完成这些操作

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

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

相关文章

spring02:DI(依赖注入)

spring02:DI(依赖注入) 文章目录 spring02:DI(依赖注入)前言:一、构造器注入(constructor)二、set注入:分析: 1. Student类:2. Addres…

【大语言模型】轻松本地部署Stable Diffusion

硬件要求: 配备至少8GB VRAM的GPU,如果你的电脑只有CPU,请看到最后。根据部署规模,需要足够的CPU和RAM。 软件要求: Python 3.7或更高版本。支持NVIDIA GPU的PyTorch。Hugging Face的Diffusers库。Hugging Face的Tr…

什么是神经网络和机器学习?【云驻共创】

什么是神经网络和机器学习? 一.背景 在当今数字化浪潮中,神经网络和机器学习已成为科技领域的中流砥柱。它们作为人工智能的支柱,推动了自动化、智能化和数据驱动决策的进步。然而,对于初学者和专业人士来说,理解神经…

WordPress 告别 MySQL:Docker SQLite WordPress

本篇文章聊聊,如何将这个持续诞生和维护了 21 年的开源软件“脱离数据库”运行,让它能够更加轻量、适合低成本离线运行。 写在前面 2003 年,Michel Valdrighi 基于 b2/cafelog 创建了开源软件 WordPress,并在 GPL 协议下发布。 …

【Java EE】关于Spring MVC 响应

文章目录 🎍返回静态页面🌲RestController 与 Controller 的关联和区别🌴返回数据 ResponseBody🎋返回HTML代码片段🍃返回JSON🍀设置状态码🎄设置Header🌸设置Content-Type&#x1f…

012Node.js自定义模块文件名不是index.js引入的方法

nodejs默认会找node_modules对应模块db里的index.js //var dbrequire(db) //错误,因为nodejs默认会找node_modules对应模块db里的index.jsvar dbrequire(db); //没有错误,是因为在DB目录的CMD下执行了npm init --yes,生成了package.json文…

【VIC水文模型】模型输入/输出参数简介

VIC水文模型输入参数简介 输入数据1.1 背景参数1.2 植被分类及属性配置1.3 土壤数据库制作1.4 气象数据库制作1.5 区域控制文件1.6 汇流文件制作 输出数据参考 VIC水文模型是基于空间分布网格化的分布式水文模型。通过将研究区域网格化,分别考虑每个计算网格内裸土和…

单片机之ESP8266模块

目录 ESP8266简介 前言 ESP8266的工作模式 ESP8266引脚说明 ESP8266测试 步骤 单片机与esp8266交互 前言 收到数据的格式 AP模式 服务器模式 外部执行命令 代码内执行命令 代码部分 客户端模式 外部执行命令 内部执行命令 代码部分 STA模式 服务器模式 外…

Springboot+Vue项目-基于Java+MySQL的企业客户管理系统(附源码+演示视频+LW)

大家好!我是程序猿老A,感谢您阅读本文,欢迎一键三连哦。 💞当前专栏:Java毕业设计 精彩专栏推荐👇🏻👇🏻👇🏻 🎀 Python毕业设计 &…

Android 车载应用开发概述

前言 介绍 Android 车载应用开发 文章目录 前言一、Android Automotive OS 概述二、Android Automotive OS 架构三、常见的车载应用1、系统应用1)SystemUI是什么开发工作 2)Launcher是什么开发工作 3)Settings是什么开发工作 4)多…

SQL分组查询(Oracle)及SQL完整的查询语句

文章目录 dql--数据查询语言简单查询条件查询排序查询分组查询分组函数(聚合函数)分组查询 完整的查询语句执行顺序! DML--数据操纵语言DDL--数据定义语言dcl--数据控制语言tcl--事务控制语言 dql–数据查询语言 简单查询 SQL简单查询 条件…

JavaWeb--前端--03Vue入门

Vue入门 1 Vue概述2 快速入门3 Vue指令3.1 v-bind和v-model3.2 v-on3.3 v-if和v-show3.4 v-for3.5 案例 4 生命周期 1 Vue概述 个完整的html页面包括了视图和数据,数据是通过请求 从后台获取的,那么意味着我们需要将后台获取到的数据呈现到页面上&#…

Redis-键值设计

Redis-键值设计 1.设置key的规范 遵循基本格式:【业务名称】:【数据名】:【id】 可读性强,在客户端的情况下使用:如果前缀相同会分目录层级长度不超过44字节 string数据结构的三种类型,在44字节之内是embstring 内存…

1.5MHz,1.2A COT 架构同步降压变换器只要0.16元,型号:LN3435

推荐原因 1.5MHZ的开关频率,可以使用小电感,1.2A满足多数应用,价格感人,只要0.16元 产品概述 LN3435是一款电流模COT架构同步降压开关稳压器。 输入范围为 2.7V-6.0V,可提供 1.2A 的连续输出电流。 内部集成了低内阻…

【Excel2LaTeX】复杂表格制作的解决方案

刚开始用LaTeX写论文,遇到的第一道坎就是绘制表格,较小的普通表格可以通过简单的语法实现,但是较大的复杂的表格却让我无从下手。 Excel2LaTeX插件 这里介绍一种我用到非常顺手的工具:Excel2LaTeX插件,下载地址&#x…

SQL系统函数知识点梳理(Oracle)

这里写目录标题 函数系统函数转换函数to_date()to_char()将数值转换成字符格式 添加货币符号将日期转换成字符 其他不常用的转换函数 字符型函数连接函数大小写转换函数大写转换小写转换首字母大写,其余的小写 替换函数去除空格函数截取函数填充函数获取字符长度函数…

35、链表-LRU缓存

思路: 首先要了解LRU缓存的原理,首先定下容量,每次get请求和put请求都会把当前元素放最前/后面,如果超过容量那么头部/尾部元素就被移除,所以最近最少使用的元素会被优先移除,保证热点数据持续存在。 不管放…

宿州市水环境投资 | 邀您参加2024全国水科技大会暨技术装备成果展览会

嘉宾简介 田 云 宿州市水环境投资建设有限公司 董事长兼总经理 报告题目:宿州市主城区水环境智慧水务建设交流 男,回族,1976年6月生,新疆克拉玛依人,2007年4月加入中国共产党,1999年7月参加工作&a…

docker安装clickhouse数据库

1.创建目录 mkdir -p /data/clickhouse/data mkdir -p /data/clickhouse/conf mkdir -p /data/clickhouse/log2.拉取镜像 docker pull clickhouse/clickhouse-server3.创建临时容器 docker run -d --rm --name clickhouse-server --ulimit nofile262144:262144 clickhouse/c…

ELK日志分析系统之Kafka

目录 一、消息队列基本介绍 1、为什么需要消息队列(MQ) 2、使用消息队列的好处 1.解耦 2.可恢复性 3.缓冲 4.灵活性 & 峰值处理能力 5.异步通信 3、Kafka消息队列的两种模式 1.点对点模式 2.发布/订阅模式 二、Kafka基本介绍 1、Kafka定义 2、Kafka概念 3、…