JavaEE 初阶大师之路之*线程,多线程编程,Thread类,变量捕获,中断线程* 一文全部搞懂!!

文章目录

  • [*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. 结语

以上就是本文主要的内容,我们正式开始学习多线程的知识,本文内容特别丰富,需要大家费点功夫好好消化,下面我们会继续多线程的学习,路还是很长滴!有不明白的地方可以留言小编会回复,希望读者们多提建议,小编会改正,共同进步!谢谢大家。 🌹🌹🌹

相关推荐
逻辑驱动的ken1 小时前
Java高频面试考点场景题16
java·开发语言·面试·职场和发展·求职招聘
DukeMr.Lee2 小时前
有声书实现
java·开发语言
SamDeepThinking2 小时前
秒杀系统的幂等,只做一层Redis判重远远不够
java·后端·架构
csdn2015_2 小时前
lambdaQuery 加 or
java·linux·服务器
天涯海风2 小时前
写一个录音并保存到手机的工具 安卓工具类
android·java·智能手机
sjsjsbbsbsn2 小时前
RAG 基础学习总结
java·数据库·学习
今天又在写代码2 小时前
Docker部署
java·阿里云·docker
_Evan_Yao2 小时前
技术成长周记07|复盘中看清方向,多Agent开启新挑战
java·后端
人道领域2 小时前
【黑马点评日记】Redis分布式锁终极方案:Redisson全面解析(含源码解析)
java·数据库·redis·分布式·缓存