【javaEE】多线程--认识线程、多线程

文章目录



这里是@那我掉的头发算什么
刷到我,你的博客算是养成了😁😁😁


前言

上文@计算机是如何运行的的最后我们谈到,进程由于创建和销毁的开销很大,频繁的创建和销毁会产生很多不必要的输出,所以,我们引入线程,线程类似于轻量级的进程,创建和销毁的开销都比较小。


线程

进程包含线程

每一个进程都包含一个或者多个线程

在我们之前java学习的过程中,使用的都是单进程单线程模型,涉及的都是单个进程。这个单线程就是main线程,具体的我们后面解释。

进程是操作系统资源分配的基本单位

进程的创建和销毁会涉及到资源的申请与释放,这个过程的开销很大。

而对于线程来说,只有创建第一个线程时会涉及到资源的申请,只有最后一个线程被销毁才涉及到资源的释放。而且在同一个进程内部管辖的多个线程之间共享所有的资源。
进程与进程之间的资源是各自独立不共享的。

线程是CPU上调度执行的基本单位

我们在上节学到,进程之间如果想要并行执行,需要进行进程调度。同样,线程在运行时也会产生相关的问题,也需要线程调度。

如果不能协调好线程内部运行的逻辑问题,就会产生"线程安全问题"。

知识回溯:

还记得我们之前学习stringbuffer和stringbuilder的时候提到过,stringbuffer是线程安全的,stringbuilder是线程不安全的。这里涉及的线程安全与本文提到的线程安全是一个问题。

上文我们提到过,线程中存在着以下的基本信息:

进程状态:如运行态(正在占用 CPU)、就绪态(等待 CPU)、阻塞态(等待资源 / 事件)。

上下文数据:进程切换前的 CPU 寄存器值、程序计数器(下一条要执行的指令地址)等。

进程优先级:不同进程因为需求不同有着不同的优先级。

进程的记账信息:统计每个进程在CPU上运行了多久,如果一个进程长时间阻塞可能考虑给这个进程一些资源让它运行。

线程中也类似存在着这样的信息,这一个进程中有多少个线程,就存放着多少个这样的信息,但是这些线程共同用一个内存指针和文件描述符表。

多线程

假设有两个房间,里面有两个人,他们同时在吃两只鸡:

这样的吃鸡效率很高,而且不会有抢食物的问题存在。

但是多一个房间意味着更大的开销,会产生资源浪费。

如果我们把鸡和人都装到一个房间里,省了空间的同时效率仍然很高。

现在我们假设有100只鸡,两个人来吃,一般来说不会产生争抢的问题。

但是如果我们增加人数嘞?

如果是100个人吃两只鸡,肯定分不过来,那么如果有的人没吃到鸡,直接生气不吃了,掀桌子,就会产生"异常",如果不及时处理异常,程序就会崩溃。这样就会产生线程安全问题。

虽然提高线程的数目确实可以提高线程的效率,但是有"度"。

线程的数目不能无休止的增加,线程的数目达到一定程度之后,就算线程再多,也无法起作用相反甚至可能降低效率,因为会增加线程调度的开销。

进程与线程的区别⭐

定义本质
进程:操作系统资源分配的基本单位(比如内存、文件句柄)。
线程:进程内的执行调度基本单位(实际干活的 "执行流")。

资源占用

进程:有独立的地址空间、内存等资源,进程间资源不共享。

线程:共享所属进程的所有资源(同一进程内的线程共用内存、文件等)。

开销成本

进程切换:开销大(需切换整个资源环境)。

线程切换:开销小(仅切换执行上下文,资源不用换)。

独立性与稳定性

进程:崩溃通常不影响其他进程("隔离性强")。

线程:崩溃会导致整个进程崩溃(因为共享资源)。

java代码实现多线程

api

api又叫应用程序编程接口,它是一套预先定义好的 "规则 / 工具",让不同软件、组件能互相调用功能,不用关心对方内部是怎么实现的。大白话讲可以理解为别人写的一些函数/类,你直接拿来就用。

广义概念:标准库,第三方库,就算是你的同学你的同事提供的一段代码,你调用了也算。

api的作用就是方便编程。

创建线程

操作系统提供的原生线程api是c语言的,不同操作系统提供的线程api,不一样。在java中,统一将线程封装成一个类Thread类(标准库提供),使用时无需导入,因为实在java.lang包下的类,而java.lang包是默认导入的。

先不看源码,Thread里面有一个run方法,这个方法在Thread类中其实是一个空方法,需要自己重写定义。run方法相当于一个任务,我们在run中定义任务,然后启动线程就可以执行任务。

我们直接用t对象调用run方法,运行结果heloo Thread没问题。

但是,我们使用t.start();结果会是什么呢?

结果和run方法一样。

其实start方法是真正的创建了一个新的线程,线程在执行过程中执行了run任务,以此输出结果。

虽然run和start输出结果相同,但是内部逻辑有很大不同。

多线程

我前面说,main也是一个线程,如果这两个线程同时在执行一段很长的程序,在执行过程中,这两个线程会不会相互影响呢?

这段代码的运行结果很出乎预料,按照常理来说,似乎应该thread的循环一直执行才对,但是实际运行中,main和thread却是交替运行的。

由于对计算机来说,运行速度太快了,其实我运行这段代码时,输出窗口滑动飞快,我们可以使用一些办法来降低一下运行速度,以便更好地观察抢占过程。

sleep需要使用类名调用,是一个静态方法,它的作用是让线程休眠xxx毫秒。但是这个方法使用时会有编译期异常

知识回溯:

编译期异常== 受查异常

运行时异常== 非受查异常

我们处理异常时可以使用throws声明异常,或者try catch处理异常。

在main函数里,这两种方法都可以消除报错,但是Thread里面不可以用throws处理。因为:

Java 要求:子类重写父类方法时,声明抛出的 "检查型异常(Checked Exception)",不能比父类方法声明的更宽泛。

原因:

当调用者使用 "父类引用" 调用方法时(这是 Java 多态的常见场景),调用者只会根据 "父类方法的异常声明" 来处理异常。如果子类方法抛出了 "更宽泛" 的检查型异常,而调用者没处理,就会导致编译错误(因为检查型异常必须显式处理),破坏了代码的兼容性。

如果父类方法没声明任何检查型异常,说明 "调用者无需处理任何检查型异常"。此时子类重写方法也不能声明任何检查型异常(因为 "无异常" 是最狭窄的范围,任何检查型异常都比它宽泛)

此处main方法可以随便throw抛异常处理,MyThread类因为父类没有抛异常只能使用try catch处理

我们可以看到,hello thread和hello main并非按照严格先后顺序执行,因为线程的调度顺序在多线程时是随机的,本质上是一种"抢占式执行"的逻辑。

修改代码后,结果变化的原因:

t.run()只是单纯的调用run方法使用,没有产生新的线程,本质上只有一个main线程,所以不会发生抢占。

第三方工具可视化多线程

我们在jdk目录下的bin文件夹里面(每个人的jdk目录可能不一样)

找到这样一个程序:

进去之后点击线程:

在这之中大部分都是内置线程,启动任何线程都会同时启动这些线程。

这里只有我们手动启动的main线程一个。

通过这个可以看到线程运行的情况,到哪一步了。

下面我们注释掉run(),运行t.start():

就能看到我们启动创建的Thread-0了。


总结

本文围绕 Java 多线程展开,先明确进程是操作系统资源分配的基本单位、线程是 CPU 调度的基本单位,线程共享所属进程资源且创建销毁开销更小,同时对比了进程与线程在资源占用、开销、稳定性等方面的核心差异;接着介绍了 Java 通过标准库 Thread 类实现多线程的方式,强调重写 run 方法定义任务、调用 start 方法才是真正启动新线程(直接调用 run 仅为普通方法执行),并通过代码示例展示了多线程的抢占式执行特性;还补充了 API 的概念、线程休眠时的异常处理规则(子类重写父类方法不能抛更宽泛的受查异常),以及使用 jconsole 工具可视化查看线程的方法,完整呈现了多线程的基础核心知识。

相关推荐
Pluchon2 小时前
硅基计划6.0 JavaEE 叁 文件IO
java·学习·java-ee·文件操作·io流
程序员卷卷狗2 小时前
联合索引的最左前缀原则与失效场景
java·开发语言·数据库·mysql
纪莫2 小时前
技术面:SpringCloud(SpringCloud有哪些组件,SpringCloud与Dubbo的区别)
java·spring·java面试⑧股
会编程的吕洞宾3 小时前
Java中的“万物皆对象”:一场编程界的哲学革命
java·后端
会编程的吕洞宾3 小时前
Java封装:修仙界的"护体罡气"
java·后端
豆沙沙包?3 小时前
2025年--Lc231-350. 两个数组的交集 II-Java版
java·开发语言
好学且牛逼的马3 小时前
【SSM 框架 | day27 spring MVC】
java
是烟花哈3 小时前
后端开发CRUD实现
java·开发语言·spring boot·mybatis
爱分享的鱼鱼3 小时前
Java基础(六:线程、线程同步,线程池)
java·后端