今天研究多线程,研究各种小弟,开始正片了
一. 前言
two和threed是连着发的,所以时间较长,这篇文章包含源码解析,所以比较长,又不懂得可以直接留言,有错误地方也多多提出。
文章共计8966个字,有错误地方望大家海涵。
第一篇连接Java—线程,多线程,线程池研究之旅 one【奇葩之旅,come】
第二篇链接Java—线程,多线程,线程池研究之旅 two【奇葩之旅,come】
二. 多线程概念
1. 头号小弟干了什么
当大哥的头号小弟干活太慢,什么事都需要他一个人做,那么就会特别慢,同时效率很低,这时候可以多叫几个小弟来干活,那么如何叫小弟干活呢,当然是"打电话"!!!
专业点就是:让主线程运行之后,创建多个线程帮自己干活
2. 线程的实现方法(有三种类型的小弟)
实现线程的方式,常用的就三种,一类二接(一个类,俩个接口)
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
三. 多线程源码(直接上干货)
不废话直接扒光小弟让我们参观一下:
以下步骤一,步骤二…是代码的书写顺序,而每个步骤后面可能会跟一个或者多个代码片段,是此步骤代码所执行的部分源码片段,所以正确的阅读方式是:先看步骤一,然后看步骤一后面的1个或者多个代码片段,然后看步骤二,再看步骤二后面1个或者多个代码片段,以此类推!!!考点是对图片的理解,也是对代码的理解,需要重点看,当然你有更好的理解就按照你自己的理解去想就可以,考点也只是一个参考
1. Thread
详细流程
Thread这类型的小弟,东西还挺多,得慢慢研究~~~~~
一部分代码图片对应下面一段话,切勿看错,上干货,一定要注意观看顺序!!!
1) 步骤一
步骤一:
- 第一种方式:
Thread thread = new Thread();- 第二种方式:
Thread thread = new Thread(“jjy”);其他参数自己研究,上述是我比较常用的。
- 代码片段一
图1:
考点:
- 可以看到Thread类是实现Runnable接口的
- 当编译运行,类被加载时会优先执行static静态代码块里的内容,它只会被执行一次,可以看到最优先执行的是一个native的registerNatives方法【registerNatives 是一个 native 方法,用于注册本地方法(native methods)的映射。在 Java 中,native 方法是使用本地代码(通常是 C 或 C++ 编写)实现的方法,它们在 Java 中被声明为 native,并且在 Java 虚拟机之外执行。】
- 代码片段二
图2:我粘出来的,所以有错误提示,不用管
考点:
- 当你使用Thread时,根据不同的参数执行有参或者无参构造方法,执行init初始化方法
- 如果是继承Thread类,那么此处使用的是public Thread(){} ,如果给定线程名字,需要使用setName的方法。因为此处调用的是无参构造,如果你想给定默认名字,需要重写有参构造方法public TestThread(String name),一会看例子就明白了。
4.public Thread(Runnable target) 这个也常用,图片里没有截取到,抱歉抱歉
- 代码片段三
图3:
图4:
图5:
考点:
- init方法,我们就不细看了,只需要关注一点,注意第二张图里代码:this.target = target; 如果使用实现Runable的方法,可以看到将传进来的Runable对象,赋值到类变量里面,第三张图片就是类私有变量,类型是Runable。
- 当然如果你使用的不是实现RUnable接口类这种方式,那你传进来的参数就是null值,这一步也就不用管了
2) 步骤二
步骤二:
- 继承Thread类。重写run方法
- 代码片段四
图6:
考点:
- 如果使用继承Thread方法,那么target为null,你在你自己的类里会重写run方法
- 如果是实现Runable接口,那么target不为null,则此方法会调用Runable对象里的run()方法,实际上,也是你重写过后的run()方法
步骤三:
- thread.start()
- 代码片段五
图7:
考点:
- 如果上面操作都做完了,那么会调用start()方法
- 可以看到此方法是上锁 synchronized 的
- 当你调用start()方法时,会调用一个start0()的方法,可以看到时native修饰的,是一个C写的代码,剩下的就会交由C来实现。
例子
正常用法:
重写用法:
1. Runnable
详细流程
看这一部分需要结合上面的Thread来看哦!
步骤一:
- 实现Runable接口,重写run()方法
- 代码片段一
这部分主要先讲解一下Runable接口
图1:
你没看错,小弟就长这样,但是你要问那到底是怎么实现的的呀,实现类是怎么运行的呀,不好意思去问神仙吧,至少我不知道!!!
有且只有一个抽象的run方法,无它。使用该接口必须实现run方法。
图片中的三段话我已经用“有道词典”帮各位翻译了,这三段话分别讲了:阐述这个类、作用、和使用方式,请欣赏:
第一段:Runnable接口应该由任何类实现,这些类的实例打算由线程执行。类必须定义一个名为run的无参数方法。
第二段:该接口旨在为希望在活动状态下执行代码的对象提供公共协议。例如,Runnable是由Thread类实现的。处于活动状态仅仅意味着线程已经启动并且尚未停止。
第三段:此外,Runnable提供了在不子类化Thread的情况下使类处于活动状态的方法。实现Runnable的类可以通过实例化Thread实例并将其自身作为目标传入,而无需子类化Thread即可运行。在大多数情况下,如果您只是计划重写run()方法而不重写其他Thread方法,则应该使用Runnable接口。这一点很重要,因为除非程序员打算修改或增强类的基本行为,否则类不应该被子类化。
仔细看其实里面的核心是 @FunctionalInterface 这个我也是查了好多资料,大概了解一下。如果不感兴趣的可以直接跳到下一块。
@FunctionalInterface 是 Java 中的一个注解,声明该接口类型是函数式接口,这意味着它只有一个抽象方法。函数式接口是 Java 函数式编程范式的一个关键概念,它们使得 lambda 表达式和方法引用的使用成为可能。
当一个接口标记了 @FunctionalInterface 注解时,Java 编译器会确保它只有一个抽象方法。如果接口有多个抽象方法,则编译器会报错。虽然这个注解是可选的,但是使用它来明确声明一个接口为函数式接口是一种良好的实践
这段代码演示了如何使用 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类中:代码前段一的图片
考点:
- 由于new了对象,会执行静态代码块的内容。
- 代码前段三
由于用的是Thread类的方法,此处不再附上图片,对应Thread类中:代码前段二的图片
考点:
- 根据参数的不同,执行不同的有参构造方法
- 主要用public Thread(Runnable target) 和Thread(Runnable target, AccessControlContext acc)俩个方法,将实现RUnable接口的类对象放入其中即可
- 代码前段四
由于用的是Thread类的方法,此处不再附上图片,对应Thread类中:代码前段三的图片
考点:
1.执行init()的方法,将实现Runable接口的类对象赋值给类私有变量。
- 代码前段五
由于用的是Thread类的方法,此处不再附上图片,对应Thread类中:代码前段四的图片
考点:
- 当执行run()方法时,发现target变量并不为null,则调用此对象里的run()方法,也就是我们重写后的方法。
这也是Thread和Runable的不同点,在调用run()方法是,调用的对象不同。!!!!(重点)
步骤三:
- thread.start()
- 代码前段六
由于用的是Thread类的方法,此处不再附上图片,对应Thread类中:代码片段五的图片
考点:
- 调用start()方法
例子
3. Callable
详细流程
先说明Callable小弟和Runable小弟的不同点:
- Callable 接口 call 方法有返回值
- Callable 接口 call 方 法允许抛出异常,可以获取异常信息
- 其他无异,接下来我们按照顺序分析一下
步骤一:
- 实现Runable接口,重写call()方法
- 代码前段一
图一:
考点:
- 通过图片可以看到Callable和Runable一样,都是接口形式,同时Runable里的run方法在这边改成了call方法,仔细看俩种方法会发现,Runable里的run()方法是抽象方法,无返回值,而Callable里的call()方法是普通方法,返回值是V泛型(可以在实现接口时指定),那为啥call方法可以有返回值,我们在下一部分细细分解研究。
- 同样使用了@FunctionalInterface 这一快去上面的Runable第一部分看就可以。
步骤二:
- TestCallable testCallable = new TestCallable();
- FutureTask futureTask = new FutureTask(testCallable);
- 代码前段二(重点!!!)
图二:
图三:
图四:
图五:
重点:
- 这边是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中,因为FutureTask还是实现Runable接口的,是它的实现类。
- 代码前段三
由于用的是Thread类的方法,此处不再附上图片,对应Thread类中:代码前段一的图片
考点:
- 由于new了对象,会执行静态代码块的内容。
- 代码前段四
由于用的是Thread类的方法,此处不再附上图片,对应Thread类中:代码前段二的图片
考点:
- 根据参数的不同,执行不同的有参构造方法
- 主要用public Thread(Runnable target) 和Thread(Runnable target, AccessControlContext acc)俩个方法,将实现Runable接口的类对象放入其中即可
- 代码前段五
由于用的是Thread类的方法,此处不再附上图片,对应Thread类中:代码片段三的图片
考点:
1.执行init()的方法,将实现Runable接口的类对象赋值给类私有变量。
- 代码片段六
由于用的是Thread类的方法,此处不再附上图片,对应Thread类中:代码片段四的图片
考点:
- 当执行run()方法时,发现target变量并不为null,则调用此对象里的run()方法,也就是我们重写后的方法。
这也是Thread和Runable的不同点,在调用run()方法是,调用的对象不同。!!!!(重点)
步骤三:
- thread.start()
- 代码前段七
由于用的是Thread类的方法,此处不再附上图片,对应Thread类中:代码前段五的图片
考点:
- 调用start()方法
重点:
- 其实你会发现Callable这一整套流程前面和后面都是一样的,就是在FutureTask这不一样,这也是为什么Callable可以返回结果值和获取异常的原因,当然更深层次的还是需要你们自己挖掘,但是入门这些也就够了。
例子
实现Callable接口
实现Runable接口,同时也可以获取结果值。
四. 总结
先说这篇文章的总结,如果你全部看完,你就会发现,原来多线程三种实现方式如此简单,相互之间没有太大区别,其实这篇文章只是其中一小点,主要是为了梳理整个流程,也是为了让大家更好的理解这些小弟(线程创建方式)到底是怎么来的,每个人又有什么区别。
至于三种区别或者共同点,自己总结,或者自己查。
我个人认为,实现Runable接口的方式更常用一些,毕竟是接口形式,java里单继承限制太多了,所以Runable显得更容易扩展,其次Callable接口这种方式也是比较常用的,这就看你对结果和异常有没有需求了,不同的需求用不同的实现方式即可,但是一定要知道,三种方式都是通过Thread类来调用,至于执行start()方法那必然是底层的C或者C++实现。
这篇文章将会持续更新!!!
知识并非一日之寒,而需日积月累,所以学习或者了解到此就可。我们由浅入深,下一篇开始研究“线程池”,包括概念,如何调用(研究研究大哥,看看他们是如何变通的,嘿嘿)!!!
如果写的有误或者理解得不好,请一定告知,我会改正~~