Java并发01:实现多线程的正确姿势

233 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

Java并发01:实现多线程的正确姿势

学习MOOC视频记录的笔记

1.网上的说法

2、4...

2.正确的说法

实现多线程的官方正确方法:2种

Oracle官网的文档是如何写的?

 有两种方法可以创建新的执行线程。一种是将类声明为Thread的子类。该子类应该重写类的run方法,然后可以分配和启动子类的实例。
 创建线程的另一种方法是声明一个实现Runnable接口的类。该类实现run方法,然后可以分配类的实例,在创建时作为参数传递给Thread并启动。
  • 方法一:实现 Runnable 接口
  • 方法二:继承 Thread

两种方法的对比:方法 1 (实现 Runnable 接口)更好

  • 代码架构角度:run方法中的内容对应任务,应该与线程解耦,将 run() 方法这个行为独立出来,而不是放在 Thread 内部 [策略模式???]
  • 节约资源,通过Thread类新建一个独立线程的损耗较大(创建,执行,销毁)
  • 无法继承其他类了
 两种方法的本质对比
 方法一:最终调用 target.run();
 方法二:run()整个都被重写

先查看 Thread 类中的 run() 方法:

 private Runnable target;
  
 ......
  
 @Override
 public void run() {
     if (target != null) {
         target.run();
     }
 }

再查看Runnable 接口里面的 run() 方法

 public interface Runnable {
     public abstract void run();
 }

思考题:同时用两种方法会怎么样?

由于 target != null,因此会调用 Runnable 接口里面的方法。

Thread 直接把 run() 方法给完全覆盖重写

从面向对象的思想去考虑

 public class BothRunnableThread {
     public static void main(String[] args) {
  
         // 创建了一个匿名内部类,传入了一个Runnable对象,并重写了run()方法
         // 最后输出 ---> 我来自Thread
         new Thread(new Runnable() {
             @Override
             public void run() {
                 System.out.println("我来自Runnable");
             }
         }){
             @Override
             public void run() {
                 System.out.println("我来自Thread");
             }
         }.start();
     }
 }

总结:最精准的描述

  1. 通常我们可以分为两类, Oracle官方文档 也是这么说的(第一类是继承Thread类,第二类是实现Runnable接口)

  2. 准确的讲,创建线程只有一种方式,那就是构造 Thread 类,而实现线程的执行单元有两种方式

    • 方法一:实现 Runnable 接口的 run 方法,并把 Runnable 实例传给 Thread
    • 方法二:重写 Threadrun 方法(继承 Thread 类)

3.经典错误观点

  • 线程池创建线程也算是一种新建线程的方式

    事实上最后会有一个 ThreadFactory 来创建线程,而默认的线程池里面的线程也是通过上述方式创建的,还是使用的new Thread,可以通过源码看出来

 public Thread newThread(Runnable r) {
     Thread t = new Thread(group, r,
             namePrefix + threadNumber.getAndIncrement(),
             0);
     if (t.isDaemon())
         t.setDaemon(false);
     if (t.getPriority() != Thread.NORM_PRIORITY)
         t.setPriority(Thread.NORM_PRIORITY);
     return t;
 }
  • 通过 CallableFutureTask 创建线程,也算是一种新建线程的方式

    通过类的结构图可以看出 FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 也实现了 Runnable 接口,本质上也离不开 Runnable 接口

image-20210227180516432

image-20210227180550408

  • 无返回值是实现 runnable 接口,有返回值是实现 callable 接口,所以 callable 是新的实现线程的方式

    同上,本质上还是实现的 runnable 接口

  • 定时器

  • 匿名内部类

  • Lambda 表达式

多线程的实现方式,在代码中写法干变万化,但其本质万变不离其宗。

4.彩蛋:如何从宏观和微观两个方面来提高技术?如何了解技术领域的前沿动态?如何在业务开发中成长?

经典知识

宏观上

  1. 并不是靠工作年限,有的人工作了 5 年技术却还是只懂皮毛。
  2. 要有强大的责任心,不放过任何 bug,找到原因并去解决,这就是提高。
  3. 主动:永远不会觉得自己的时间多余,重构、优化、学习、总结等。
  4. 敢于承担:虽然这个技术难题以前没碰到过,但是在一定的了解调研后,敢于承担技术难题,让工作充满挑战,这一次次攻克难关的过程中,进步是飞速的。
  5. 关心产品,关心业务,而不只是写代码。

微观上

  1. 看经典书籍(指外国人写的经典的中国译本,比如说 Java并发编程实战、自顶向下计算机网络)
  2. 看官方文档

如果前两点不能解决问题:

  1. 英文搜 googlestackoverflow
  2. 自己动手写,实践写 demo,尝试用到项目里
  3. 不理解的参考该领域的多个书本,综合判断
  4. 学习开源项目,分析源码(学习 synchronized 原理,反编译看 cpp 代码)

技术领域的最新动态

  • 高质量固定途径: ohmyrss.com(信息源筛选,为我所用)
  • 订阅技术网址的邮件: InfoQ(每周都看)

彩蛋:如何在业务开发中成长

  • 业务方向:eg: 电商、搜索、出行;了解业务的核心模型(整理,关键表,关键字段,积累),复杂业务合理抽象;考虑远一些,预留处理,快速扩展
  • 技术方向:eg: 中间件,存储系统,广泛的适用场景,通用性强
  • 两个 25% 理论

5.常见面试问题

有多少种实现线程的方法?思路有 5

  1. 不同的角度看,会有不同的答案
  2. 典型答案是两种 (哪个好哪个不好,3点优势)
  3. 我们看原理,两种本质都是一样的 (run方法区别,重写run方法,target执行run方法)
  4. 具体展开说其他方式 (线程池,计时器…)
  5. 结论

实现 Runnable接口和继承 Thread 类哪种方式更好?

  1. 从代码架构角度 【解耦,具体的任务】
  2. 新建线程的损耗 【可以反复利用这同一个线程,eg: 线程池】
  3. Java 不支持双继承