Advertisement

Java—线程,多线程,线程池研究之旅 three【奇葩之旅,come】(附源码解析)

阅读量:

今天研究多线程,研究各种小弟,开始正片了

  • 一. 引言
    • 二. 多线程概述

      • 1. 核心组件承担了哪些主要任务
    • 2. 线程实现的主要方法(主要分为三类)

    • 三. 多线程源码(直接上干货)

      • 1. Thread

        • 详细流程
            1. 步骤一
        1. 步骤二
        • 例子
      • 1. Runnable

        • 详细流程
        • 例子
      • 3. Callable

        • 详细流程
        • 例子
    • 四. 总结

一. 前言

两个与第三个连续发布导致耗时较长。由于包含源码解析等内容这篇文章较为冗长对于无法及时回复的问题请多包涵。文章总共有8966个汉字请各位包涵。第一篇文章链接:Java线程与多线程研究之旅(一)第二篇文章链接:Java线程与多线 threading研究之旅(二)

二. 多线程概念

1. 头号小弟干了什么

当大哥的最得力助手工作过于缓慢时,
事情往往都需要他自己独自完成,
这样一来工作效率就会明显降低,
这时就需要多找几个得力的小弟来协助工作,
如何指挥这些小弟工作呢?
当然就是"通过电话联系"啦!

专业点就是:让主线程运行之后,创建多个线程帮自己干活

2. 线程的实现方法(有三种类型的小弟)

实现线程的方式,常用的就三种,一类二接(一个类,俩个接口)

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口

三. 多线程源码(直接上干货)

不废话直接扒光小弟让我们参观一下:

依次为步骤一、后续涉及的步骤二及后续部分则按照代码书写顺序展开。每一步骤后可能紧跟一个或多个具体实现细节,请建议首先集中精力理解各部分之间的关系,在熟悉整体架构后再逐一深入查看与之对应的实现细节!重点考察的是对图像处理流程的理解,并非仅仅局限于对现有算法的知识储备。当然如果有不同的见解或思考路径也可以尝试探索!

1. Thread

详细流程

Thread 这个组件的东西确实不少哦!建议大家花些时间仔细研究一下里面的功能模块!
请注意!这些代码图片与下面的文字部分是高度关联的,请不要搞错了!
来干货!这一部分的内容非常重要,请务必严格按照指定的观看顺序来操作!

1) 步骤一

步骤一:

  • 第一种方式:
    创建一个匿名线程实例;

步骤二:

  • 第一种方式:
    String name = "John Doe";
  • 第二种方式:
    String name = "Mary Ann";

其他参数自己研究,上述是我比较常用的。

  1. 代码片段一

图1:

请添加图片描述

registerNatives 是一个 native 方法,用于注册本地方法(native methods)的映射。在 Java 中,native 方法是使用本地代码(通常是 C 或 C++ 编写)实现的方法,它们在 Java 中被声明为 native,并且在 Java 虚拟机之外执行。

registerNatives 是一个 native 方法...

其中 registerNatives 是一个 native 方法,在 Java 中用于注册本地方法(native methods)与Java语言之间的映射关系。这些本地方法通常由C/C++编写并通过 JNI(Java Native Interface)与Java虚拟机通信。

  1. 代码片段二

图2:我粘出来的,所以有错误提示,不用管

请添加图片描述

考点:

  1. 当调用Thread类时(即调用其相关的方法),会自动调用初始化方法init(即实现init())。

  2. 如果是继承自Thread类,则该线程将采用public static synchronized Thread(){}的无参数构造函数。

    • 如果希望设置线程名称,则需编写如public TestThread(String name)这样的带有参数的构造函数,并在后续代码中调用setName(name)方法。
  3. 另一个常用的格式是public class Thread implements Runnable { ... }。

  4. public Thread(Runnable target)这个也常用。

  5. 代码片段三
    图3:

请添加图片描述

图4:

请添加图片描述

图5:

请添加图片描述

考点:

  1. init方法,我们就不细看了,只需要关注一点,注意第二张图里代码:this.target = target; 如果使用实现Runable的方法,可以看到将传进来的Runable对象,赋值到类变量里面,第三张图片就是类私有变量,类型是Runable。
  2. 当然如果你使用的不是实现RUnable接口类这种方式,那你传进来的参数就是null值,这一步也就不用管了
2) 步骤二

步骤二:

  • 继承Thread类。重写run方法
  1. 代码片段四
    图6:
在这里插入图片描述

考点:

  1. 如果基于Thread继承的方法,则需要在自定义类中重定义该方法。
  2. 如果实现了Runable接口,并且其指定的目标不为空,则会通过调用该对象的run()方法来执行。实际上也是在原有基础上对public boolean run(){}方法进行了重新定义。

步骤三:

  • thread.start()
  1. 代码片段五
    图7:
在这里插入图片描述

考点:

  1. 如果上面操作都做完了,那么会调用start()方法
  2. 可以看到此方法是上锁 synchronized 的
  3. 当你调用start()方法时,会调用一个start0()的方法,可以看到时native修饰的,是一个C写的代码,剩下的就会交由C来实现。

例子

正常用法:

在这里插入图片描述

重写用法:

在这里插入图片描述

1. Runnable

详细流程

看这一部分需要结合上面的Thread来看哦!

步骤一:

  • 实现Runable接口,重写run()方法
  1. 代码片段一
    这部分主要先讲解一下Runable接口
    图1:
请添加图片s述

您别误解了哦!这个对象长得挺正常的,并不是什么特别之处。

想了解它是如何运行的吗?实际上就是实现了 run 方法而已。

说实话我也不清楚这是怎么弄出来的(去问神仙也问不出来),抱歉啦!

只有一个抽象成员函数 named run ,除此之外没有其他可能性。

如果要使用该接口,则必须实现该成员函数 named run.

我已借助"有道词典"完成了对图片中三个段落的翻译工作。这三个段落分别探讨了该类的基本构成要素及其重要性,请收看。

第一段:任何类型的一般可运行性(Generic Runnability)接口应由任一支持其功能的对象类别来实现;这些对象实例将由相应的线程执行。该接口要求所有实现者必须定义一个名为run的无参数方法。

第二段:此通用协议设计旨在为那些希望在其运行状态中执行代码的对象提供统一规范;例如,在实际应用中可使用Thread类来实现Runnable接口;其中"处于活动状态"仅仅意味着该线程已启动但尚未停止。

第三段:此外,在无需继承Thread的情况下,在不子类化BaseClass的前提下仍可通过创建特定类型的一般可运行性(Generic Runnability)对象并将其设置为目标来保持对象活跃状态;这使得在大多数编程场景下若只需修改现有对象的行为而不愿对其基础功能进行增强,则应考虑使用Runnable接口而非继承式设计;这种做法至关重要;因为它确保了除非程序员有意对其进行修改或增强其基本行为模式否则系统架构将维持现状

这段文字的核心是@FunctionalInterface这一概念,在经过大量资料查阅后已经对其有了基本了解。如果对相关内容不感兴趣,则可以直接跳过后续部分

这段文字的核心是@FunctionalInterface这一概念,在经过大量资料查阅后已经对其有了基本了解。如果对相关内容不感兴趣,则可以直接跳过后续部分

在Java中,@FunctionalInterface是一个注解,在声明该接口类型时表明其为函数式接口,并规定其仅包含一个抽象方法。作为Java函数式编程范式的中心概念之一,函数式接口使得lambda表达式和方法引用的使用成为可能。当一个接口被标记上@FunctionalInterface注解时,在编译器层面会确保该接口仅包含一个抽象方法。尽管这是一个可选的注解,在实际开发中将其用于明确声明一个接口为函数式 interface 仍然是一种良好的编程实践。

请添加图片描述

该代码采用了lambda表达式来描述jjy()方法的行为,并运用了函数式接口

步骤2:

  • 第一种方式:
    Thread thread = new Thread(new Runnable() {@Override public void run() {}})
  • 第二种方式:
    TestRunable testRunable = new TestRunable (); 先获取自己写的类对象,这个类实现了Runable接口
    Thread thread = new Thread(testRunable); testRunable是实现Runable接口的对象

采用的是Thread类的方法,在此不再提供相关图片,在Thread类中:代码前段一的图片。

考点:

  1. 由于new了对象,会执行静态代码块的内容。

因为采用的是Thread类的方法,在此不再提供相关图片,而对应于该处的图像是:代码前段二

考点:

  1. 根据参数的不同,执行不同的有参构造方法
  2. 主要用public Thread(Runnable target) 和Thread(Runnable target, AccessControlContext acc)俩个方法,将实现RUnable接口的类对象放入其中即可

由于采用了Thread类的方法,在对应于Thread类的代码部分(即代码前段三)中不再提供相关图片说明

考点:
1.执行init()的方法,将实现Runable接口的类对象赋值给类私有变量。

基于Thread类的方法无需展示相关图片

  1. 在运行该对象的 run 方法的过程中, 发现目标变量 target 不为 null, 于是会自动触发并执行此对象内部的 run 操作, 也就是我们所重写的那个。

这也反映了 Thread 与 Runnable 之间的区别, 在它们各自运行各自的 run 方法时。

(重点)

步骤三:

  • thread.start()
  1. 代码部分六
    基于Thread类的方法运行时,默认不会提供图片说明。
    在其中关联到的是:代码部分五 的图像

考点:

  1. 调用start()方法

例子

在这里插入图片描述

3. Callable

详细流程

先说明Callable小弟和Runable小弟的不同点:

  • Callable 接口的 call 方法包含返回值
  • Callable 接口的 call 方法能够抛出异常,并支持获取异常信息
  • 除此之外,在后续内容中我们将按照顺序进行详细分析

步骤一:

  • 实现Runable接口,重写call()方法
  1. 代码前段一
    图一:
在这里插入图片描述

考点:

  1. 从图中可以看出Callable与Runable具有相同的形式。值得注意的是,在这边将run方法重命名为call,并通过深入分析这两种实现的本质特征发现:在Runable类中定义的run()函数是一个抽象函数且不带返回值;而在Callable类中定义的call()函数则是一个普通函数且支持自定义返回类型(其特点是可以自定义返回类型,在接口实现时可指定具体类型)。那么为何call函数能够支持带有明确类型的返回值呢?我们将在下一节中进行详细探讨。
  2. 同样采用了@FunctionalInterface这一工具,在上文的Runable部分已经进行了相关介绍。

步骤二:

  • 生成TestCallable对象testCallable;

  • 创建FutureTask实例futureTask并传入testCallable。

    1. 代码前段二 (重点!!!)

图二:

在这里插入图片描述

图三:

在这里插入图片描述

图四:

在这里插入图片描述

图五:

在这里插入图片描述

重点:

  • 这边是Callable的重点,我们先说为什么没有直接将testCallable对象直接放到Thread中,像new Thread(testCallable)这样。因为Thread()里面只能放Runable类的对象,而callable是不可以的,具体可以看Thread第二部分的图片。
  • 那么我们就需要转换一下,所以有了FutureTask,而FutureTask又是实现RunnableFuture接口,而RunnableFuture接口继承Runnable和Future的接口(注意只有接口之间才可以继承,类只能单继承,多实现)。通过图二、三、四、五可以看到几个类和接口相对应的关系
  • 所以FutureTask最终实现的还是Runable里面的Run方法,详细的下一部分讲。而FutureTask其他方法都是Future提供的。

目前我们还需进一步探究FutureTask内部的具体操作。这正是我们关注的重点所在:Callable为何需要返回值?

图六:

在这里插入图片描述

图七:

在这里插入图片描述

图八:Executors类的执行方式有很多参数设置,默认情况下已经优化得很好了。这里不做详细讲解,请欢迎自行查阅相关资料。

在这里插入图片描述

图九:Executors类的方法

在这里插入图片描述

考点:

  • 通过图六和图七,可以发现,当 new FutureTask(testCallable); 时,如果传入的是Callable接口类对象,初始化FutureTask类的私有变量Callable< V >和初始化状态,也就是图七。如果传入的是Runable接口类对象,那么就会调用Executors类里的callable方法,详细我们看图八,此时会创建一个RunableAdapter类对象,根据图十我们也知道这是一个静态内部类,这个类实现Callable接口,重写了call()方法。
  • 这个RunableAdapter内部类做了什么呢?答案就是,他将Runable里的run()方法执行了,并且将result结果进行返回,其实就是一个转换器,负责将实现Runable接口的类也同时返回一个结果,也算是弥补Runable没有返回结果的一个补救措施吧。

图十:

在这里插入图片描述

考点:

  • 我们回到正题,现在传入实现Callable接口的对象,然后run()方法里干了什么?
  • 可以看到,当c不是null时,且状态为NEW时,可以调用call()方法,也就是用户写的逻辑代码,然后将结果传入set()方法中
  • 这个set方法你们就自己看吧,一些结果逻辑判断然后赋值到类变量上,给后面使用,就不过多叙述了。

步骤三:

  • Thread thread = new Thread(futureTask);

在上一步之后,我们将FutureTask对象放入Thread中。由于它仍然实现了Runable接口,并且属于其所属类。

因为基于Thread类的方法而使用,在此不做图示展示,在Thread类中:代码前段一 的图片。

考点:

  1. 由于new了对象,会执行静态代码块的内容。

由于用的是Thread类的方法,则不再提供相关图片;对应于其所属的Thread类中的代码前段二部分

考点:

  1. 根据参数的不同,执行不同的有参构造方法
  2. 主要用public Thread(Runnable target) 和Thread(Runnable target, AccessControlContext acc)俩个方法,将实现Runable接口的类对象放入其中即可

因为采用了Thread类的方法,在此不再提供相关图片说明;具体到Thread类中对应的代码片段三的位置。

考点:
1.执行init()的方法,将实现Runable接口的类对象赋值给类私有变量。

基于Thread类的方法运行时,默认情况下会生成相应的日志信息。建议您直接查看完整的源代码以获取更多信息。

考点:

  1. 当执行run()方法时,发现target变量并不为null,则调用此对象里的run()方法,也就是我们重写后的方法。
    这也是Thread和Runable的不同点,在调用run()方法是,调用的对象不同。!!!!(重点)

步骤三:

  • thread.start()

采用 Thread 类的方法进行操作,则此处将不再提供相关图片。请参考 Thread 类中 代码前段五 所示的内容

考点:

  1. 调用start()方法

重点:实际上发现Callable的整体流程在前后环节上具有相似性,在FutureTask环节则有所不同。这也正是Callable为何能够返回结果值并捕获异常的根本原因。当然更深层次的原因则需要自行探索与理解

例子

实现Callable接口

在这里插入图片描述

实现Runable接口,同时也可以获取结果值。

在这里插入图片描述

四. 总结

先总结一下这篇文章的内容。看完之后你会发现其实多线程的三种实现方式非常简单它们之间的差别也不大但文章的主要目的是梳理整个流程图并帮助大家更好地理解这些线程创建方式的本质以及它们之间的区别与联系

关于这三者的差异或共同特征,请自行总结或查阅资料。我认为以实现Runable接口的方式更为常见。这是因为作为Java中的一个特点,在处理多线程场景时会带来诸多限制性因素;因此这种设计使得Runable更适合扩展。此外,在需要处理返回结果和异常的情况下,默认使用Callable接口这种方式也是比较常用的;不同场景下可以根据具体需求选择合适的实现策略;不过无论如何,请记住这三种机制最终都会通过Thread类来触发start()方法这一底层操作(毕竟这些机制本质上都是为了在虚拟机层面模拟多线程运行效果)。

这篇文章将会持续更新!!!

知识并非一天之内的造诣而成(...),而是需要长期积累的过程;因此掌握这一层面的知识即可满足需求。循序渐进地学习吧,在接下来的文章中我们将深入探讨"线程池"这一主题(涵盖基本概念、具体操作流程以及开发者实践中的注意事项)。其中重点分析了如何调用该功能模块(深入探讨大牛的做法)!

如果写的有误或者理解得不好,请一定告知,我会改正~~

全部评论 (0)

还没有任何评论哟~