文章目录
- [*1. 前言*](#1. 前言)
- [*2. 正文*](#2. 正文)
-
- [1. 线程](#1. 线程)
-
- [1.1 线程的引入](#1.1 线程的引入)
- [1.2 进程与线程的关系](#1.2 进程与线程的关系)
- [1.3 线程的调度](#1.3 线程的调度)
- [1.4 丝滑的🌰:解释线程和进程](#1.4 丝滑的🌰:解释线程和进程)
- [2. Thread类](#2. Thread类)
-
- [2.1 什么是Thread类](#2.1 什么是Thread类)
- [2.2 线程的创建(简) run和start](#2.2 线程的创建(简) run和start)
- [2.3 多线程执行案例](#2.3 多线程执行案例)
- [2.4 sleep()方法](#2.4 sleep()方法)
- [2.4 查看线程状态](#2.4 查看线程状态)
- [3. 线程的创建(繁)](#3. 线程的创建(繁))
-
- [3.1 利用子类](#3.1 利用子类)
- [3.2 利用Runnable接口](#3.2 利用Runnable接口)
- [3.3 利用匿名内部类替代MyThread子类](#3.3 利用匿名内部类替代MyThread子类)
- [3.4 利用匿名内部类替代Runnable接口](#3.4 利用匿名内部类替代Runnable接口)
- [3.5 使用lambda表达式(==重点==)](#3.5 使用lambda表达式(==重点==))
- [4. Thread类及常见方法](#4. Thread类及常见方法)
-
- [4.1 start()方法](#4.1 start()方法)
- [4.2 run()方法](#4.2 run()方法)
- [4.3 Thread类的构造方法](#4.3 Thread类的构造方法)
- [4.4 Thread中核心属性及获取方法](#4.4 Thread中核心属性及获取方法)
- [4.5 isDaemon() setDaemon()后台线程](#4.5 isDaemon() setDaemon()后台线程)
- [4.7 isAlive() 线程是否存活](#4.7 isAlive() 线程是否存活)
- [4.8 isTnterrupted()中断线程](#4.8 isTnterrupted()中断线程)
- [*3. 结语*](#3. 结语)
The minute you think of giving up, think of the reason why you held on so long!
1. 前言
本文我们正式开展关于线程的学习,这是个非常庞大的知识点,小编会写很多篇博客围绕线程以及多线程的知识来进行深入讲解,本篇博客也正式开展我们JavaEE阶段的关键内容,内含丰富代码,请小伙伴们''细嚼慢咽''
2. 正文
1. 线程
1.1 线程的引入
在上篇博客中我们主要介绍了操作系统以及进程的概念,那什么是线程?为什么要引入线程呢?
线程其实就是进程的一部分,就是进程的一个小弟🕶!引入线程的原因是由于进程整体是一个比较''重''的概念,操作系统创建进程销毁进程时的开销特别大(尤其是频繁的创建销毁),所以我们就不让大哥来办事了,主要让小弟替大哥办事
那这时候就有小伙伴有疑问了,我们的电脑怎么会频繁创建销毁进程呢?我也不会无缘无故打开程序;退出程序;打开程序;退出程序...; 这里的频繁创建销毁主要指的是服务器!我们学习Java以后的方向就是服务器开发,而对于一个服务器频繁创建进程和销毁进程是非常常见的。
那什么是服务器? 服务器分为硬件和软件

如图就是硬件的服务器,而软件服务器指的是一个程序,同样客户端也是一个程序,这两个程序配合完成一些工作,客户端发起请求给服务器,服务器被动接受请求,并返回响应!例如我们所学的MySQL数据库,MySQL数据库就分为MySQL客户端和MySQL服务器;一个MySQL服务器要给多个客户端提供服务 ,只不过我们把MySQL客户端和服务器都安装在了本机上,没有其余客户端访问服务器,所以感知不到这个过程。服务器就好比是一个餐馆,客户端就是来餐馆吃饭的客人,向餐馆发出请求自己要吃什么,餐馆为此进行响应!
再举一个简单的🌰,小编正在创作这篇博客,与此同时,世界上可能有数以万计的程序🐒也在创作博客,这些程序猿都是一个个客户端,并且同时在访问 CSDN 这个服务器,而 CSDN 服务器要为这些程序猿提供服务。
那为什么说服务器就会频繁的创建销毁进程呢? 这就要追溯到上个世纪了,我们知道进程是并发执行的,拿 CSDN 举🌰,当没有引入线程时,世界上每个程序猿登录 CSDN ,CSDN 服务器就要为这些程序员一一开启一个进程,当某个程序员关闭CSDN 网页时,CSDN 服务器就要销毁对应的进程,这样服务器就涉及到频繁的创建销毁进程,所面临的开销也就特别的大。
所以根据上面的🌰,我们就引出线程 这一概念,可以理解为轻量级进程!利用线程完美的优化了服务器的压力,之后的服务器只需要为客户端创建线程,并销毁线程即可
1.2 进程与线程的关系
1)进程是包含线程的,每一个创建出来的进程内部至少包含一个线程

2)同进程内所有线程共享进程全部公共资源;后续新建的线程仅需极少私有资源,创建、切换开销极小,远轻于进程。
在Windows电脑的任务管理器中是无法查看进程内部的线程的,只能借助第三方工具来实现
我们之前学习过进程中PCB的属性,进程内部管辖的多个线程之间会共享进程的内存资源,硬盘资源 等 (CPU资源另行讨论),进程的创建和销毁需要申请资源和释放资源,这是一件重量级的事情,而对于线程,只是第一个线程的创建(和进程一起创建)才需要申请资源,后续在创建线程不涉及到资源申请操作;并且只有所有线程都销毁(进程销毁)才会真正的释放资源,运行过程中销毁某一个线程不会释放资源!!!
那么由于线程之间是共享进程资源的关系,会不会出现一个线程挂了其余都挂了的情况呢 ?答案是会的!!这就涉及到线程最关键的知识点:线程安全问题!我们后续会反复围绕线程安全展开学习
1.3 线程的调度
大哥能被调度走,小弟能干看着吗??所以线程也可以被CPU调度!线程CPU被CPU调度的过程其实与进程调度是一样的! 我们知道进程调度过程中与PCB的四个属性息息相关(状态,优先级,上下文,记账信息),而线程的调度同样也与这四个属性有关!与进程一样,每个线程都有唯一且不共享的一份四个属性! 这就好比大哥出去和别人打架,数以百计的小弟只有一人带了🧱,这能行吗?? 但是多个线程共用一个文件描述符表和内存指针!!! 这就好比大哥租了一间豪华别墅,小弟不跟着大哥住,还要单独租一间豪华大别野??
CPU资源在线程调度这里很难用''共享''来形容,我们下面描述一下线程如何在CPU上被调度执行的
CPU的核心就像一个大舞台,而线程就是要上台表演的演员,每个演员都有上台表演的机会,现在有三个线程:线程1🐱 线程2 🐶 线程3 🐰;🐱🐶🐰三个人并没有商量好谁在哪个舞台上表演,所以就会出现很多种情况:
1)🐱去核心1;🐶去核心2;🐰去核心3
2)🐱和🐶去核心1;🐰去核心2
3)🐱和🐰去核心1;🐶去核心3
4) ... ... ... ...
通过以上线程的无序调度,我们不难看出线程的调度也是并发执行的 ,与进程调度过程一模一样!而具体这些线程怎么调度执行,这个过程程序猿也感知不到,并且干预不了!全是由操作系统内部的调度器自行完成的
打起仗来,将军(进程)能先冲吗?肯定卒(线程)先上!
线程比进程更轻,更高效,所以线程是 CPU 上调度执行的基本单位!!!
1.4 丝滑的🌰:解释线程和进程
派大星老铁接收到松鼠珊迪的一个任务,要吃完一百只鸡

派大星老铁对此感到很头疼,一百只🐔怎么吃得完? 为此想了几个办法
1)打电话给他的好哥们派小星,并邮寄给他五十只🐔,让他帮忙吃掉

但是派小星住在南极,邮寄的费用非常贵,所以这个方案付出的成本很高,派大星pass掉了
找派小星这一方案就拟化为创建多进程,效率虽得到提高,但开销很大!
2)利用海星的繁殖能力再次创造一个派大星,两个派大星一起吃

这次的效果很明显,两个派大星吃🐔的速度很快,也不需要付邮费了,整体效率大大提高,那么派大星就想繁殖更多的派大星一起吃🐔
繁殖一个派大星一起吃🐔这一操作,是引入了多线程的方案,效率得到大幅度提高,并且节约开销
3)引入多个派大星一起吃🐔

派大星又繁殖了两个派大星一起吃🐔,这样每个派大星只需要吃掉 25 只,整体的效率进一步提高,但是派大星对此表示不够,他要继续增添帮手!

派大星繁殖出很多很多个派大星,这使🐔不够分了,后面的派大星挤来挤去就为了吃口🐔
这就说明了,虽然提高线程的数目能够提升效率,但是不是''线性增长'',线程数目达到一定数量时,就算线程再多,也没办法起到效果,反而还会拖慢效率,线程数目过多,线程调度的开销也会非常明显!
4)情况超出派大星所预料的了,由于派大星的数量过多,开始出现两只派大星争抢一只鸡

这就体现了线程安全问题,如果出现两个线程共同调度 CPU 的同一块核心的同一个区域,就会出现冲突,可能会使代码出现 bug,这也是因为线程调度是并发的,抢占式执行(派大星抢🐔)
5)两个派大星开始抢同一只鸡,其中一个没抢过的派大星红温了化身为乌鸦哥了🐦⬛

这就说明,如果一个线程抛出了异常,可能就会带走整个进程,并且所有线程都将无法工作(桌子都掀了,还吃什么鸡)
2. Thread类
2.1 什么是Thread类
我们要先明白,线程是操作系统提供的一个概念,所以操作系统就会提供一些关于线程的 api 供程序员使用,相信有的小伙伴还不明白api是什么,下面小编解释一下:
api原名Application Programming Interface:应用程序接口 ;还是举一个🌰
小编以前喜欢过一个女神,下面讲述小编如何追求女神的故事!
刚认识女神时,小编只能:
1) 发qq
2)发短信
3) 聊wx
后面小编和女神越来越熟悉了,小编就能
1)打电话
2)打视频
再后面,小编和女神确定关系了
1) 吃饭
2)看电影
3)逛街
每个阶段,小编能对女神做什么事情,都属于女神为小编提供的api
那么基于女神为小编提供的api,小编就可以编程了 😜;到了周末,小编计划和女神如何度过难忘的一天:
1)早晨起来,先打个视频,看看女神的美貌
2)中午一起吃个饭,一起逛街
3)下午一起看电影
4)晚上 ...
以上小编的计划,就属于小编利用api去进行编程!
话题再扯到Java层面,在Java标准库里例如String,StringBuilder,List,ArrayList都是Java为程序员提供的api,相信小伙伴们对 api 有了更好的理解,小编在这里再强调一点:api 的目的是用于编程的,如果未能帮助我们去编程则不叫 api!
回到主题来,线程就是操作系统所提供的 api,而 Java 中的 JVM 对此进行了封装,封装成了 Thread 类,供程序员使用线程去完成编程,这里再注意一点,操作系统都是 C 语言写的,不同操作系统的线程 api 都不同,JVM 对此进行了总体的封装,不需要程序员操心,这也是 JVM 的🐂🍺之处!
2.2 线程的创建(简) run和start

这是我们接触编程时第一次写的代码,也是一个最直观的单线程例子,这个代码中只有main一个线程,所以打印完hello world线程结束了。
而多线程就与我们之前所写的代码大相径庭了,我们需要考虑的东西要变得非常非常多!那多线程的代码是怎样的呢? 首先在Java中利用Thread类来表示一个线程

我们发现一个问题,当使用Thread类时,IDEA并没有import任何包,这也说明Thread类在java.lang的默认包中! 但光凭上述代码我们是无法创建一个线程的,下面跟随小编的脚步一起来探寻如何正确的创建一个线程
1)首先我们创建一个子类MyThread继承父类Thread,并且重写父类中的run()方法

这里的run方法代表线程要执行的任务,父类Thread中只提供了run方法并没有任何内容,我们肯定不能在 Thread 类里给 run 加操作,而创建出一个新线程,需要程序员为线程分配任务,分配任务就需要在run方法里进行操作!所以我们需要一个子类继承 Thread 并重写 run 方法

此时线程的任务就是打印"hello thread"
2)利用 start()方法
start()方法是多线程编程中必不可少的方法!它代表启动一个线程并执行任务,start()方法与 run()方法是对应关系,start()方法是 Thread 类提供的启动线程的标准入口,它向操作系统容申请并创建一个线程;并且让新线程进入就绪状态等待 CPU 调度,调度之后就会执行 run 方法!

这样一个线程就创建成功了!多了一个派大星🧽来帮助我们干活!这里也利用了向上转型的知识点,忘记的小伙伴记得要复习一下🕶
通过以上两步我们就成功的创建出一个线程,创建线程的方法还有很多,我们之后会详细介绍!
这里我们在总结一下 run()和 start()方法:
run 是线程的入口方法,当新的线程启动了(调用 start()方法)就会自动执行 run 方法,run 不需要手动调用,所以 run 方法就相当于回调函数,回调函数意思大体为你写一个函数自己不去调用而是交给别人调用,例如 Comparable 中的 compareTo 方法就是典型的回调函数
start 是真正系统中创建线程,这里其实是 JVM 调用操作系统的 api 去完成创建线程的操作的
2.3 多线程执行案例
这时候就有小伙伴有疑问了,我利用 t.run()也可以执行 MyThread 里的任务呀,为什么非要用 start()?我们用代码举一个🌰大家就明白了

当在代码中加入 while 循环,让这个代码一直跑起来我们就会发现:如果使用 t.start(),则会出现一会打印 hello main 一会打印 hello thread,这就是多线程并发执行的体现,而如果用 t.run()呢?

使用 t.run() 则不会打印 hello main,这也说明当前只有一个线程在干活,只有一个派大星在吃一百只🐔
2.4 sleep()方法
如果有小伙伴试过上述多线程的代码就会发现,呀?我的电脑怎么越来越烫,这是因为线程执行的太快了,CPU在疯狂处理while循环一直打印,我们如果不想让线程执行得这么快,就可以使用 Thread 中的一个静态方法 sleep()方法! 相信有小伙伴是熟悉这个方法的,在Java中sleep()方法代表休眠,让当前线程放弃CPU,休息一会,过段时间再执行

好了小编我们听你的了,使用了sleep怎么还报错了?? 小编送出名言一句:报错遇到不要慌,看看信息再查查,

InterruptedException:编译时异常,这明显就是让我们处理一下异常罢了

上面是小编利用 IDEA自动生成的异常处理,但是这个异常处理得并不是很完美,什么小编你敢质疑 IDEA?
在我们实际开发中,异常处理的方式有三种
1 记录异常信息作为日志,后续程序员根据日志查询问题
2 进行重试(多指网络异常,例如网卡了游戏卡了,退出重进)
3 遇到特别严重的问题在处理中通过短信 邮件 电话 通知程序员(报警机制)
而 IDEA 这里只是向上抛异常,这个异常并没有处理,在真实业务中第三种方式是最常用的~
好了话题回来,我们就勉为其难使用 IDEA 的自动生成抛异常吧🤣,我们再观察这里的 sleep 是休眠,里面的 millis 代表什么呢?物理⚛️好的都知道是代表 毫秒,这里小编使用 1000ms 就相当于 1s,让该线程打印完 hello thread 休息1 秒再进入循环再进行打印
这里1s 对于计算机是非常非常长的时间,我们要知道CPU 执行指令的时间一般都是微秒级别的也就是百万分之一秒,那你有没有等一个女孩百万年呢?🤔
我们的目光再回到代码上,如果也在 main 线程上加上sleep()操作,会不会报错呢?"小编你在考验我智商呢,肯定会报错啊!"

无需质疑,必然报错,可这里 IDEA是如何处理异常呢?

IDEA 在方法中声明了抛出 InterruptedException,那么小编问题来了,为什么刚才的 run 方法中不能这么做???
相信聪明的小伙伴已经有答案了,run 方法是继承来的方法,哪里能 throws 异常,这就好比你借 daddy 的手机用,觉得不好玩,于是扔进垃圾桶了,那你等着吃七匹狼吧~
好!我们的目光再回到这个最终版代码,两个都加上了休眠操作的while 循环

此时通过结果我们发现,两个线程的执行确实慢了下来,电脑也不会喊救命了;并且两个两个一组,有先打印 hello thread 的情况,也有先打印 hello main 的情况,这也反应了线程并发执行!!!(小编会一直强调这六个字)
2.4 查看线程状态
线程不会像女神一样,连见面的机会都没有🐶,我们可以借助第三方工具来查看线程的详细情况,当然生活中我们也可以通过创造者工具和约女神逛街🧐!
JDK在安装好后就为我们提供了一个可以查看当前Java代码中的线程详细情况,注意:只是针对Java线程,不针对其他线程!! 我们可以从jdk目录中找到名为jconsole的程序

小编使用的是MacOS操作系统的电脑,文件如图所示,注意只有在程序运行时,也就是代码正在运行时才可以看到线程,如果代码运行结束则看不到线程了!!!

打开后会看到如图界面,勾选本地进程的Thread.Demo1 ,这是我们当前代码的进程,其余三个小编可以简单介绍一下:
com.intellij.idea.Main :这是 IntelliJ IDEA 主进程,也就是你正在使用的 IDEA 开发环境本身。
org.jetbrains.jps.cmdline.Launcher 这是 IDEA 的增量编译服务进程(JPS)
jdk.jconsole/sun.tools.jconsole.JConsole 这就是 你当前正在运行的 JConsole 进程本身。
以上三个进程了解即可,我们主要看当前代码的进程,勾选后点击连接🔗即可

进入连接后勾选"不安全连接"

接着勾选线程,这样就可以查看当前代码的线程了

我们发现当前线程名称显示在左边,个数显示在图中,那么又有疑问了,当前这个代码小编只创建了两个线程 ,为什么显示了 19 个线程?? 这里注意:除了我们创建的两个线程,另外的线程都属于 JVM 自带的线程,也叫后台线程,小编后面会专门讲解后台线程!

main 线程和 Thread---0 线程是小编创建的两个线程,main 线程就没什么好说的了,程序的主线程,Thread---0 是默认另一个线程的默认命名,如果再创建一个线程则会命名为 Thread-1,按顺序命名下去,当然创建线程时可以为其分配名字,我们放到后面说!

当我们点击 main 线程查看详细信息时会发现一个堆栈跟踪的字段,这里代表获取当前线程状态的同时线程的代码执行到哪里了!main线程被获取到时,代码执行到第 22 行

Jconsole 获取 main 线程状态时,代码跑到 22 行的sleep 操作,如果这里没有 sleep 休眠 1 秒的操作 ,而是直接打印的话,jconsole 则有可能捕获到 20 行或 21 行,这是由于 Jconsole 的捕获是采样式的,也就是 Jconsole 在没有 sleep 时线程飞速运转,采样捕获到哪一行就是哪一行!

这里的 Jconsole 像一只饥饿的老虎,老虎是会抓跑的慢的鹿还是跑得快的鹿?当然是跑得慢的!若所有鹿跑的一样快时,Jconsole 就随机抓个小🫎了!
同理我们也可以看看 Thread-0线程的堆栈跟踪


也是 sleep 休眠操作,可见 1 秒钟对于计算机来说是多么的慢!
3. 线程的创建(繁)
3.1 利用子类
第一种方法与上面创建线程的方法一模一样,小编就不再介绍了
java
class MyThread extends Thread{
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1{
public static void main(String[] args) throws InterruptedException {
Thread t=new MyThread();
t.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
3.2 利用Runnable接口
"小编我头都大了,一会Thread类,一会又扯到接口上了",不知道小伙伴们到这里有没有感到很懵,线程跟接口能有什么关系?Runnable接口又是何方神圣?
其实Runnable就表示一个任务,就是一段要执行的逻辑,相比于第一种方法,利用Runnable接口是一个更好的选择!

我们发现遇到老朋友run了,Runnable接口中只有run这一个方法,所以它就是被当做一个任务,代表线程要干什么,与子类继承Thread重写run方法的意图是一样的!

Runnable也需要一个子类实现这个接口,所以我们创建了MyRunnable

在创建线程时,我们利用多态的知识,实例化了子类 MyRunnable 对象,并赋值给 runnable 引用,并在 Thread类中调用 runnable 引用即可

利用 start 方法就成功创建出一个线程,这里自动执行 run 方法

相信小伙伴们对这部分知识没有什么问题了
但是小编的问题来了!为什么小编刚刚说使用 Runnable 接口是更好的选择??
这里就不得不扯到解耦和的问题了~~
小编!什么是耦合?? 耦合可以理解为关联度,两个代码的关联关系越大耦合就越大,我们在编写程序时不希望写出耦合太大的代码,这样就造成修改一个代码就会改坏一片!所以编写代码要尽量保证低耦合
回过头来什么叫解耦合??就是让代码之间降低关联度,使用 Runnable 就把任务和线程两个概念剥离开了,线程是线程,任务是任务,而不是使用 MyThread 继承 Thread 的方式重写 run,这样就造成线程和任务高耦合了,在未来修改代码就可能造成繁琐的影响
好了谈到耦合我们就不得不得谈到内聚了!(连锁反应)那么什么是内聚??
下面听小编举个🌰:
小编在生活中是个懒人🙂,总是把用过的东西随手丢,这就导致牙膏可能出现在🛏上,牙刷可能出现在饭桌上,导致小编每次找到牙刷找不到牙膏;找到牙膏找不到牙刷;这种情况我们就称之为 低内聚! 也就是很分散;
后来小编改掉了这些坏习惯,牙膏和牙刷都放在洗手台附近,想找牙膏或牙刷都变得很容易,这就称为 高内聚!
相信通过小编的🌰小伙伴能充分了解这部分知识,在我们代码层面则推崇高内聚低耦合,开发过程中我们要尽量考虑要代码之间变得低耦合高内聚,但不能矫枉过正!!!
3.3 利用匿名内部类替代MyThread子类
"小编,我怎么对匿名内部类有点印象",匿名内部类是我们JavaSE语法部分的重要知识,不会的小伙伴请自行跳转✈️,使用匿名内部类替代MyThread本质上就是使用方法一!

我们可以把匿名内部类当成一位临时工👷♀️,在公司上班作为临时工是没有工牌的,只需要来公司干活即可,正常的类可以当作正式员工,是有工牌的,知道这位员工叫什么,临时工无人在意叫什么名字,只在意你是否完成工作(类中的方法)
Thread t=new Thread(){ ...}
这里匿名内部类主要做了三件事情
1)创建了一个Thread的子类,不用管子类的名字叫什么
2){ } 里面编写子类中包含的方法,属性,或重写父类的方法
3)创建了这个匿名内部类的实例,并把实例的引用赋值给t(编译看左,运行看右)

利用匿名内部类的优点就是简化创建子类的过程,当然也有缺点(凡事都是有利有弊),如果某个代码是 ''一次性 ''的那么就推荐使用匿名内部类,而如果要利用很多次,还是定义子类的方式比较方便。
3.4 利用匿名内部类替代Runnable接口
在JavaSE语法阶段学习匿名内部类时,匿名内部类经常与接口组合使用,简化了创建子类继承接口的操作!

与方法二,方法三相比,使用Runnable接口+匿名内部类,不仅能简化代码,而且使代码变得低耦合!

3.5 使用lambda表达式(重点)
相信小伙伴对lambda表达式都不陌生,是数据结构中的知识,小编这部分知识还在创作中,未来会给小伙伴填补上的🫓!
我们先来看一下如何利用lambda表达式创建一个线程

引入lambda表达式是针对方法三和方法四的进一步改进,是对匿名内部类冗余写法的进一步简化与改进。Lambda 表达式本质上就是匿名函数 ,最核心用途就是充当回调函数 。不止 Java,主流编程语言都具备 Lambda 特性,只是命名与语法略有差异。Java 有一条严格语法规则:方法不能脱离类独立存在,所有方法必须依托类或对象。而其他语言允许直接定义独立的匿名函数,Java 无法直接实现。为了兼容语法限制、顺利使用匿名函数,Java 专门提出了函数式接口 的概念:有且仅有一个抽象方法的接口。 (这里的Runnable接口就是典型的函数式接口)
用这个唯一的抽象方法,来承接 Lambda 匿名函数的逻辑,
以此绕开「方法不能独立存在」的限制,让 Java 也能实现轻量级函数式编程。

这里的"() -> { }" 本质上完成了三步操作:创建一个匿名函数式接口的实现类;创建出对应的实例;重写里面的方法。 ()代表了 run ()方法,如果里面有参数就要在()中写入对应的参数;{ }代表了 run 方法中要写入的操作
相信小伙伴们通过以上内容能充分了解lambda 表达式的用法和概念!
以上就是线程在 Java 中创建的五种方法,随着学习的深入,我们会了解更多创建线程的方法,小编推荐大家多多使用方法五,在小编的示例中也会尽可能使用方法五去创建线程!
4. Thread类及常见方法
学习一个类,我们肯定要了解该类的属性以及方法!这也是本篇博客的重点内容
4.1 start()方法
start方法的作用是启动一个线程,它是Java标准库所提供的方法,本质上是调用操作系统的API


通过Thread类中start方法的源码,我们可以看到其中有一个start0()方法,是由native修饰,"小编,我对这个native好熟悉!" 没错,我们在之前的学习中经常会看到源码中由native所修饰的方法,也叫本地方法 ,也就是JVM内部通过由C++代码所实现的方法,(JVM也是C++写的),我们在IDEA中是看不到start()方法内部的主要实现,如果想要看则需要额外下载JVM的源码(C++写的),小编就不在这为大家展示了,可以大致讲解一下个关于start内部的方法逻辑,先判断当前操作系统是什么操作系统,再找到相匹配的操作系统并调用其api!
对于start()方法,我们只强调一点 :每个Thread对象只能start一次,不能重复利用!!!

如果我们写出了如图代码,一个 Thread 对象 start 两次会发生什么?

第一次 start 执行是没问题的,再次 start 则会报错,我们分析一下这个报错信息:
Illegal:非法的;Thread:线程;State:状态;Exception:错误
连起来就是:非法的线程状态,那么问题来了,为什么会抛出这个错误提示?
当第一次调用 start 方法时,线程从"刚出生"状态,被操作系统调度,进入"执行"状态;执行完打印日志后就会进入结束状态,当第二次调用 start 时,JVM 会检查当前线程的状态,如果发现该状态为结束状态或刚出生状态则不允许再次启动
我们在后面介绍线程状态时,会详细写明每个线程经过什么操作会进入什么状态,大家这里大致了解即可!
4.2 run()方法
run方法本质上就是入口方法,新的线程启动了,就要执行此方法,run也不需要程序员手动调用,线程创建好则会自动执行。
我们还可以把run方法理解为"回调函数";("小编,什么是回调函数,上面好几次都提到了你怎么也不解释一下")别急,小编现在就告诉你到底什么是回调函数:
回调函数本质上就是你写一个函数,自己不去调用,交给别人去调用。相信这段话对于理解回调函数还是很模糊的,所以🌰来了:

小编最近想喝奶茶🧋,于是小编去奶茶店点单,此时店员接收到小编的请求就要开始做奶茶,小编只需留下手机号,等奶茶做好了店员就会主动打电话给小编!
这里小编的手机号给店员就是把回调函数交给店员,而店员打电话通知小编这个动作就是回调函数
我们再用上述的例子结合代码来看

CallBack 接口相当于小编的手机号,里面有一个 finish()方法,相当于打电话给小编的动作;Worker 类相当于店员,店员 doWork(传入小编手机号)这杯奶茶,当Worker 干完活,就会回掉 finish方法,也就是打电话给小编,这就是回调函数!最后在 main 方法中,进行对 finish 方法的重写,也就是店员打电话告诉了小编哪些信息。
回过头来,run 方法就相当于回调函数,也就是任务,任务是什么就要通过回调函数来体现!
4.3 Thread类的构造方法
| 方法 | 说明 |
|---|---|
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
| 【了解】Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可 |
以上就是Thread类所有的构造方法,我们一一来介绍
1)Thread()
无参构造方法,注意使用这个方法时,必须重写Thread的run方法
2)Thread(Runnable target)
使用了Runnable对象创建线程,这里的Runnable相当于任务(再次强调,反复强调),这里注意:此时就不要重写 Thread 的 run 方法
3)Thread(String name)
前两个线程构造方法我们都见怪不怪了,而传入 String 类型的参数又开阔了我们的眼界,传入字符串就是创建一个线程对象并为其命名
4)Thread(Runnable target,String name)
与 3)同理,只不过多个任务

我们可以使用 Jconsole 查看一下当前线程

我们发现确实有一个名为 paxon1 的线程,这也是我们所创建的线程,如果没有为其命名则会以 Thread-0 的方式自动填充!
这有的小伙伴可能会抛出一个问题:main 线程哪去了?? 哪去了?哪去了?回家休息了! 由于代码中没有main 线程无死循环,所以 main 线程就会很快执行完代码并结束

再次查看上述代码的线程

main 就回来被迫工作了
5)Thread(ThreadGroup group, Runnable target)
通过英文单词的含义我们也不难猜到这里的 ThreadGroup 就是线程组,对于线程组我们后续会讲解,这里知道有这个方法即可
4.4 Thread中核心属性及获取方法
| 属性 | 获取方法 | 核心作用说明 |
|---|---|---|
| ID | getId() |
线程唯一标识,系统自动分配,不可修改 |
| 名称 | getName() |
线程可读名称,可自定义,便于调试识别 |
| 状态 | getState() |
获取线程当前状态(NEW/RUNNABLE/BLOCKED/WAITING/TERMINATED 等) |
| 优先级 | getPriority() |
获取线程优先级(1~10,默认5),影响CPU调度概率 |
| 是否后台线程 | isDaemon() |
判断是否为守护线程,守护线程会随用户线程全部结束而终止 |
| 是否存活 | isAlive() |
判断线程是否处于活动状态(已启动且未终止) |
| 是否被中断 | isInterrupted() |
判断线程是否收到中断请求 |
首先我们把目光聚焦于前四个方法:
getId():获取当前线程的Id,在Java中个会给每个运行的线程分配id,用于标识线程身份的效果,类似于进程PID

在实际开发中很少使用这个方法
getName()获取当前线程的名字,小编也不做代码演示了,与 getId 一样,在实际开发中很少使用这两个方法,况且我们使用 Jconsole 也可以查看线程的编号和名字
getState()与 getPriority()在我们讲解完线程优先级以及线程状态后再作讲解
4.5 isDaemon() setDaemon()后台线程
"Daemon:守护;是否为守护线程?小编,线程还有守护者啊?"
对的,线程是有守护者的,我们把这种线程称为后台线程,那到底什么算是后台线程,后台线程是什么意思?
🌰来了✈️:
不知道小伙伴们在上学时做没做过守护者,周董有一首歌"等你下课"

里面讲述一位沸羊羊🐑默默守护自己心爱的女神,为女神奉献自己的所有,最难绷的是女神不知道他的存在的~
这里的沸羊羊就算是后台线程,我们再把目光转向代码

我们创建三个线程t1,t2,t3;(代码太多,小编就截一部分了),此时我们打开Jconsole

我们会发现,除了t1,t2,t3和main线程之外还有很多个线程,这些线程都是JVM自带的线程,它们的存在不影响进程的结束,也就是说即使他们继续存在,如果进程要结束,它们也会随之结束,这些线程都称为后台线程 ;而t1,t2,t3,main 这些程序员自己创建的线程称之为前台线程
我们可以通过 isDaemon()判断是否为后台线程,通过 setDaemon()把该线程变成后台线程
当然如果有多个前台线程,必须等所有前台线程结束,进程才结束! 后台线程就是沸羊羊🐑,而前台线程则是男神级别的🤵♂️,如果沸羊羊的女神在和🤵♂️谈恋爱,沸羊羊干预不了他们的恋爱是否结束,能做的只有旁观守护;而🤵♂️可以选择与女神分手,也就是让该线程结束随之进程就结束了!
目光再回到我们的代码上

这是小编的示例代码,上述代码中 main 执行 3s 后显示 main线程结束

当 main 线程结束后,t 线程仍在继续执行,这就说明当前 t 线程也是个🤵♂️,如果利用 setDaemon 方法,把 t 线程变成沸羊羊🐑,我们看看结果会发生什么变化

注意,setDaemon 方法要在start 方法前使用,核心原因是:JVM 在线程启动时,会一次性读取并锁定该线程的守护线程标记,启动后不允许修改,否则会抛出 IllegalThreadStateException 异常。

此时的结果表明,main 线程终止了,t 就终止了,说明 t 被我们设置为后台线程(沸羊羊🐑)
4.7 isAlive() 线程是否存活
Java代码中创建的Thread对象,和操作系统中的线程是一一对应关系,但是,Thread对象的生命周期,与系统中线程的生命周期可是大不相同的! 这是怎么回事??Java中我们创建的是Thread类的对象,对象只会随着进程的销毁而被销毁,而操作系统的线程,随时随地都会被销毁

如图线程t执行3次或四次hello thread后就会被销毁,这里我们看看t线程是否存活


通过执行结果我们不难发现,虽然线程被销毁,打印确实为false,但是t对象仍然存在,如果不存在则不会打印了!并且此处打印三个 true,也有可能打印四个 true 这是由于线程的随机调度特性!
4.8 isTnterrupted()中断线程
比起中断线程,小编更想称之为终止线程,中断可能后面还会恢复?而终止是真的终止了
终止线程本质上就是让线程的入口方法(run方法)尽快执行完毕,线程就结束了 ,在我们之前的代码中,为了展示多线程编程和查看线程,总是利用死循环while(true)来执行线程的入口方法,这样就会使线程永远执行下去,那么如果想要终止线程该怎么做?聪明的小伙伴肯定已经想到把true换成别的不就行了! 小编下面介绍三种方式来终止线程(while的()中传入三种参数)
1)引入成员变量

上述代码我们引入一个成员变量isFinished,当isFinished为true线程终止,3s后isFinished赋值为true,t线程入口方法就结束了

这个方法相信小伙伴们都没有什么问题,既然大家都没有问题,小编的问题来了:能否把这里的成员变量isFinished改为局部变量??
答案可能超乎大家想象🕶
2)引入局部变量 (变量捕获)

还是同样的思路,3s 后将局部变量 isFinished 设置为 true,使线程跳出循环,但是这里我们发现,程序竟然报错了!??

这里写着在 lambda 表达式中这个变量应该为 final 或 effectively final ,什么意思??什么是 effectively final? 我们下面一一来分析!
在 lambda 里面使用了局部变量就会触发 变量捕获 这杨的语法,lambda 是回调函数,它的执行时机为操作系统创建线程之后才执行!!

这样就很有可能导致:后续线程创建好了,main 线程中的方法都执行完了,而main 线程中的局部变量 isFinished 就会被销毁...为了解决这个问题,Java 的做法:把被捕获的变量拷贝一份到 lambda 里面,也就是当前两个 isFinished 本质上不是一个东西了,而是拷贝!外面的局部变量 isFinished 是否销毁与 lambda 中 isFinished 就无关了!
当然拷贝也有缺点,拷贝意味着变量不适合进行修改了!修改一方,另一方不会因此发生变化,这种一边变,一边不变的状态会给程序员带来很多困扰,所以 Java 规定lambda 中如果采用局部变量就必须为 final,也就是不可变,不能修改!

所以想要解决报错必须把上述代码最后一行修改 isFinished 值的操作删除
我们再把话题扯远点,有的小伙伴可能还会有疑问,如果lambda 这里采用了一个引用类型,那么引用类型也不能修改吗??
答案是:引用本身不能修改,也就是这个引用不能指向其他对象,而引用指向的对象的本体是可以修改的!~ 这也是因为引用所指的对象生命周期是由 JVM 的垃圾回收(GC)管理的,而不是 main 线程结束,对象就销毁。。。

当前是小编所举的实例代码

如果这里让 p 指向一个新的对象,则还会出现变量捕获语法问题,如果仅仅是修改 Paxon 类中的属性,是不影响变量捕获的语法的!
我们的话题再回到初始代码中,为什么使用第一种方式,访问成员变量,不会出现这类错误呢? 访问成员变量的语法就变成了内部类访问外部类成员!并且成员变量的生命周期也是由 GC 管理的,不必担心会过早销毁,也就不必拷贝或限制!
3)使用 isInterrupted()
通过前两种终止线程的土办法,我们大致明白了这个流程是怎样的,紧接着我们介绍现代常用的方法:使用 Thread 类中的isInterrupted()方法

小编小编,我听你的了,使用了这个方法,为什么又报错了??小编你耍我呢?
小编可不敢耍各位小伙伴,这里报错的原因还是由于变量捕获规则引起的,"怎么还有变量捕获,这里哪有变量啊!?" 别急,这里创建线程Thread t=new Thread(...)在本质上是先创建 Thread 对象,后赋值给 t!你都没拿到 t,怎么能在 lambda 使用呢??

聪明的小伙伴肯定想到了一定要利用 Thread 类中...方法获取一下当前线程不就好了!这里获取当前线程的方法为 currentThread()方法,后续小编还会提及,相当于(this) 小编就继续操作了

这是小编操作出来的完整的代码,利用 Interrupt 方法终止线程;这里一定要注意 while 条件要带个!,因为 isInterrupt 是判断当前线程是否被终止,没被终止前就是 false!相信小伙伴们也能写出和小编大差不差的代码,那么问题来了

当我们一运行发现,没有打印 t 线程被终止这串日志,而是直接抛异常,这是为什么!?这是小编为大家留下的悬念,请见下篇播客详解🕶🕶
3. 结语
以上就是本文主要的内容,我们正式开始学习多线程的知识,本文内容特别丰富,需要大家费点功夫好好消化,下面我们会继续多线程的学习,路还是很长滴!有不明白的地方可以留言小编会回复,希望读者们多提建议,小编会改正,共同进步!谢谢大家。 🌹🌹🌹