大家都说Java有三种创建线程的方式!并发编程中的惊天骗局!

简介: 今天来聊一个比较有意思的话题,这是一道Java八股文中的八股文,简称八股文Plus!

引言

今天来聊一个比较有意思的话题,这是一道Java八股文中的八股文,简称八股文Plus

这个疑惑在很久之前便在我心中产生了,久远到什么程度呢?大概可以从盘古开天地开始算起,哈哈哈。

先来看看本次的主角,就是一个问题:“Java中有几种创建线程的方式?”

大家可以先试着回答一下,回答完之后再往下看。

一、浅谈Java线程的创建方式

回到前面的那个问题,如果是个普通Java程序员,应该会回答“三种”,分别为:

  • ①继承Thread类;
  • ②实现Runnable接口;
  • ③实现Callable接口。

如果是Pro版的Java程序员,应该会回答“四种”,分别为:

  • ①继承Thread类;
  • ②实现Runnable接口;
  • ③实现Callable接口;
  • ④使用ExecutorService线程池。

如果是Plus版的Java程序员,应该会回答“五种”,分别为:

  • ①继承Thread类;
  • ②实现Runnable接口;
  • ③实现Callable接口;
  • ④使用ExecutorService线程池;
  • ⑤使用CompletableFuture类。

如果是ProMax版的Java程序员,应该会回答“七种”,分别为:

  • ①继承Thread类;
  • ②实现Runnable接口;
  • ③实现Callable接口;
  • ④使用ExecutorService线程池;
  • ⑤使用CompletableFuture类;
  • ⑥基于ThreadGroup线程组;
  • ⑦使用FutureTask类。

如果是超级至尊版的Java程序员,可能还会回答“十种”,分别为:

  • ①继承Thread类;
  • ②实现Runnable接口;
  • ③实现Callable接口;
  • ④使用ExecutorService线程池;
  • ⑤使用CompletableFuture类;
  • ⑥基于ThreadGroup线程组;
  • ⑦使用FutureTask类;
  • ⑧使用匿名内部类或Lambda表达式;
  • ⑨使用Timer定时器类;
  • ⑩使用ForkJoin线程池或Stream并行流。

如果是……版的Java程序员,或许还会整出十二种、十三种……,但我就不继续往下罗列了,先简单将上述提到的十种方式,编写出相应的代码。

1.1、继承Thread类

这是最普通的方式,继承Thread类,重写run方法,如下:

public class ExtendsThread extends Thread {
   
   
    @Override
    public void run() {
   
   
        System.out.println("1......");
    }

    public static void main(String[] args) {
   
   
        new ExtendsThread().start();
    }
}

1.2、实现Runnable接口

这也是一种常见的方式,实现Runnable接口并重写run方法,如下:

public class ImplementsRunnable implements Runnable {
   
   
    @Override
    public void run() {
   
   
        System.out.println("2......");
    }

    public static void main(String[] args) {
   
   
        ImplementsRunnable runnable = new ImplementsRunnable();
        new Thread(runnable).start();
    }
}

想深入研究可以参考之前《Runnable分析》的文章。

1.3、实现Callable接口

和上一种方式类似,只不过这种方式可以拿到线程执行完的返回值,如下:

public class ImplementsCallable implements Callable<String> {
   
   
    @Override
    public String call() throws Exception {
   
   
        System.out.println("3......");
        return "zhuZi";
    }

    public static void main(String[] args) throws Exception {
   
   
        ImplementsCallable callable = new ImplementsCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

想深入研究可以参考之前《Callable分析》的文章。

1.4、使用ExecutorService线程池

这种属于进阶方式,可以通过Executors创建线程池,也可以自定义线程池,如下:

public class UseExecutorService {
   
   
    public static void main(String[] args) {
   
   
        ExecutorService poolA = Executors.newFixedThreadPool(2);
        poolA.execute(()->{
   
   
            System.out.println("4A......");
        });
        poolA.shutdown();

        // 又或者自定义线程池
        ThreadPoolExecutor poolB = new ThreadPoolExecutor(2, 3, 0,
                TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3),
                Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        poolB.submit(()->{
   
   
            System.out.println("4B......");
        });
        poolB.shutdown();
    }
}

具体可以参考之前《剖析ThreadPoolExecutor线程池》的文章。

1.5、使用CompletableFuture类

CompletableFutureJDK1.8引入的新类,可以用来执行异步任务,如下:

public class UseCompletableFuture {
   
   
    public static void main(String[] args) throws InterruptedException {
   
   
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
   
   
            System.out.println("5......");
            return "zhuZi";
        });
        // 需要阻塞,否则看不到结果
        Thread.sleep(1000);
    }
}

具体可以参考之前《详解CompletableFuture》的文章。

1.6、基于ThreadGroup线程组

Java线程可以分组,可以创建多条线程作为一个组,如下:

public class UseThreadGroup {
   
   
    public static void main(String[] args) {
   
   
        ThreadGroup group = new ThreadGroup("groupName");

        new Thread(group, ()->{
   
   
            System.out.println("6-T1......");
        }, "T1").start();

        new Thread(group, ()->{
   
   
            System.out.println("6-T2......");
        }, "T2").start();

        new Thread(group, ()->{
   
   
            System.out.println("6-T3......");
        }, "T3").start();
    }
}

1.7、使用FutureTask类

这个和之前实现Callable接口的方式差不多,只不过用匿名形式创建Callable,如下:

public class UseFutureTask {
   
   
    public static void main(String[] args) {
   
   
        FutureTask<String> futureTask = new FutureTask<>(() -> {
   
   
            System.out.println("7......");
            return "zhuZi";
        });
        new Thread(futureTask).start();
    }
}

想深入研究可以参考之前《剖析FutureTask类》的文章。

1.8、使用匿名内部类或Lambda

这种方式属于硬扯,就是直接new前面所说的Runnable接口,或者通过Lambda表达式书写,如下:

public class UseAnonymousClass {
   
   
    public static void main(String[] args) {
   
   
        new Thread(new Runnable() {
   
   
            @Override
            public void run() {
   
   
                System.out.println("8A......");
            }
        }).start();

        new Thread(() -> 
                System.out.println("8B......")
        ).start();
    }
}

1.9、使用Timer定时器类

JDK1.3时,曾引入了一个Timer类,用来执行定时任务,如下:

public class UseTimer {
   
   
    public static void main(String[] args) {
   
   
        Timer timer = new Timer();

        timer.schedule(new TimerTask() {
   
   
            @Override
            public void run() {
   
   
                System.out.println("9......");
            }
        }, 0, 1000);
    }
}

里面需要传入两个数字,第一个代表启动后多久开始执行,第二个代表每间隔多久执行一次,单位是ms毫秒。

1.10、使用ForkJoin或Stream并行流

ForkJoinJDK1.7引入的新线程池,基于分治思想实现。而后续JDK1.8parallelStream并行流,默认就基于ForkJoin实现,如下:

public class UseForkJoinPool {
   
   
    public static void main(String[] args) {
   
   
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        forkJoinPool.execute(()->{
   
   
            System.out.println("10A......");
        });

        List<String> list = Arrays.asList("10B......");
        list.parallelStream().forEach(System.out::println);
    }
}

想要深入研究可以参考《全解ForkJoinPool》的上下两篇文章。

二、八股文中的惊天骗局

看完前面第一阶段,是不是说的头头是道?如果你也这样认为,恭喜你被带偏了!

不知道从何时起,Java并发编程的八股文,在“Java有几种创建线程的方式”这道题上,开始以“数量”为荣,写的越多,显得越专业,越牛X,大家去百度搜个关键词:

Java有几种方式创建线程?”

出现的答案,最少都有四种,那这真的对吗?可以说对,但严格意义上来说,又不对。

抛开后面一些先不谈,咱们就聊最开始的三种:“继承Thread类、实现Runnable接口、实现Callable接口”,这应该是广为人知的答案,不管是刚入行的小白,还是在业内深耕已久的老鸟,相信都背过这一道八股文。

那么此时来看个例子:

public class ImplementsRunnable implements Runnable {
   
   
    @Override
    public void run() {
   
   
        System.out.println(Thread.currentThread().getName()
                + ":竹子爱熊猫");
    }
}

这里定义了一个类,实现了Runnable接口并重写了run方法,按前面的说法,这种方式是不是创建了一条线程?答案是Yes,可问题来了,请你告诉我,该如何启动这条所谓的“线程”呢?

public static void main(String[] args) {
   
   
    ImplementsRunnable runnable = new ImplementsRunnable();
    runnable.run();
}

难道像上面这样嘛?来看看运行结果:

main:竹子爱熊猫

结果很显然,打印出的线程名字为:main,代表目前是主线程在运行,和调用普通方法没任何区别,那究竟该如何创建一条线程呀?要这样做:

public static void main(String[] args) {
   
   
    ImplementsRunnable runnable = new ImplementsRunnable();
    new Thread(runnable).start();
}

newRunnable对象,接着再new一个Thread对象,然后把Runnable丢给Thread,接着调用start()方法,此时才能真正意义上创建一条线程,运行结果如下:

Thread-0:竹子爱熊猫

此时线程名字变成了Thread-0,这意味着输出“竹子爱熊猫”这句话的代码,并不是main线程在执行了,所以聊到这里,大家明白我想表达的含义了嘛?实现了Runnable接口的ImplementsRunnable类,并不能被称为一条线程,包括所谓的Callable、FutureTask……,都不能创建出真正的线程。

换到前面所提出的三种方式中,只有继承Thread类,才能真正创建一条线程,如下:

public class ExtendsThread extends Thread {
   
   
    @Override
    public void run() {
   
   
        System.out.println(Thread.currentThread().getName()
                + ":竹子爱熊猫");
    }

    public static void main(String[] args) {
   
   
        new ExtendsThread().start();
    }
}

// 运行结果:
//      Thread-0:竹子爱熊猫

因为当你用一个类,继承Thread类时,它内部所有的方法,都会被继承过来,所以当前类可以直接调用start()方法启动,更具体点来说,Java中,创建线程的方式就只有一种:调用Thread.start()方法!只有这种形式,才能在真正意义上创建一条线程!

而例如ExecutorService线程池、ForkJoin线程池、CompletableFuture类、Timer定时器类、parallelStream并行流……,如果有去看过它们源码的小伙伴应该清楚,它们最终都依赖于Thread.start()方法创建线程。

好了,搞清楚这点之后,再回头来看Runnable、Callable,这俩既然不是创建线程的方式,那它们究竟是什么?这点咱们放到后面去讨论,先来聊聊“Java有三种创建线程的方式”,这个以讹传讹的八股文,到底是怎么来的呢?

究根结底,这个错误观念的源头,来自于《Java编程思想》(《Thinking In Java》)和《Java核心技术》(《Core Java》)这两本书。在《Core Java》这本书的第12、13章,专门对多线程编程进行了讲解,提到了四种创建线程的方式:

  • ①继承Thread类,并重写run()方法;
  • ②实现Runnable接口,并传递给Thread构造器;
  • ③实现Callable接口,创建有返回值的线程;
  • ④使用Executor框架创建线程池。

同样的内容,在《Thinking In Java》的第二十一章,也有重复提及到。于是,国内阅读过这两本书籍的人,在写文章、写面试题、写书籍、授课、录视频……时,把这个概念越传越泛,按照“三人成虎”原则,Java3、4种创建线程的方式,这个观念变成了事实,从此刻在了每个Java开发者的DNA里。

好了,搞清楚问题的缘由,咱们回到前面提出的问题,既然实现Runnable、Callable接口,不是创建线程的方式,那它们究竟是什么?准确来说,这是两种创建“线程体”的方式,包括继承Thread类重写run()方法也是。

三、线程与线程体的关系

前面可能提出了一个大家没接触过的新概念:线程体,这是个啥?来看看ChatGPT的解释:

001.png

看完这个回答,相信大家就能明白“线程体”是怎么一回事了,说简单点,线程是一个独立的执行单元,可以被操作系统调度;而线程体仅仅只是一个任务,就类似于一段普通的代码,需要线程作为载体才能运行,ChatGPT给出的总结特别对:线程是执行线程体的容器,线程体是一个可运行的任务

不过Java中创建线程体的方式,可以基于Runnable创建,也可以靠Callable创建带返回的、也可以通过Timer创建支持定时的……,但不管是哪种方式,到最后都是依赖于Runnable这个类实现的,如果大家有去研究过Callable的原理,大家就会发现:Callable实际上就是Runnable的封装体

到这里,搞清线程与线程体的关系后,相信大家就一定明白了我为何说:Java中创建线程只有Thread.start()这一种方式的原因了!而最开始给出的其他方式,要么是在封装Thread.start(),要么是在创建线程体,而这个所谓的线程体,更接地气的说,应该是“多线程任务”。

new Runnable(...);
new Callable(...);

这并不是在创建线程,而是创建了两个可以提供给线程执行的“多线程任务”。

不过还有个问题,任务和线程,到底是怎么产生绑定关系的呢?大家可以去看Thread类提供的构造器,应该会发现这个构造函数:

public Thread(Runnable target) {
   
   
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

new``Thread对象并传入一个任务时,内部会调用init()方法,把传入的任务target传进去,同时还会给线程起个默认名字,即Thread-x,这个x会从0开始(线程名字也可以自定义)。

而当大家去尝试继续跟进init()方法时,会发现它在做一系列准备工作,如安全检测、设定名称、绑定线程组、设置守护线程……,当init()方法执行完成后,就可以调用Thread.start()方法启动线程啦。

启动线程时,最终会调用到start0()这个JNI方法,转而会去调用JVM的本地方法,即C/C++所编写的方法,源码我就不带着大家去跟了,感兴趣的可以去down一下OpenJDK的源码,或者去搜一下Thread.start()的实现原理,我这里就大致总结一下大体过程。

Thread在类加载阶段,就会通过静态代码块去绑定Thread类方法与JVM本地方法的关系:

private static native void registerNatives();
static {
   
   
    registerNatives();
}

执行完这个registerNatives()本地方法后,Java的线程方法,就和JVM方法绑定了,如start0()这个方法,会对应着JVM_StartThread()这个C++函数等(具体代码位于openjdk\jdk\src\share\native\java\lang\Thread.c这个文件)。

②当调用Thread.start()方法后,会先调用Java中定义的start0(),接着会找到与之绑定的JVM_StartThread()这个JVM函数执行(具体实现位于openjdk\hotspot\src\share\vm\prims\jvm.cpp这个文件)。

JVM_StartThread()函数最终会调用os::create_thread(...)这个函数,这个函数依旧是JVM函数,毕竟Java要实现跨平台特性,而不同操作系统创建线程的内核函数,也有所差异,如Linux操作系统中,创建线程最终会调用到pthread_create(...)这个内核函数。

④创建出一条内核线程后,接着会去执行Thread::start(...)函数,接着会去执行os::start_thread(thread)这个函数,这一步的作用,主要是让Java线程,和内核线程产生映射关系,也会在这一步,把Runnable线程体,顺势传递给OS的内核线程(具体实现位于openjdk\hotspot\src\share\vm\runtime\Thread.cpp这个文件)。

⑤当Java线程与内核线程产生映射后,接着就会执行载入的线程体(线程任务),也就是Java程序员所编写的那个run()方法。

四、总结

看到这里,这篇文章也就结束了,相较于以往的文章,篇幅方面略显短小,本文重在纠正大家的错误观念,讲述一种学习思维:看任何东西请保持质疑,不要无条件信任别人的说法,要锻炼自己的深度思考能力,而不是听风就是雨!学习时请记住这个原则,这才能让你真正发生质的成长。

最后,如果以后你的面试中,被问到“Java有几种创建线程的方式”这个问题时,也希望按照本文所说,在面试中聊出与别人不一样的看法,例如:

Java创建线程有很多种方式啊,像实现Runnable、Callable接口、继承Thread类、创建线程池等等,不过这些方式并没有真正创建出线程,严格来说,Java就只有一种方式可以创建线程,那就是通过new Thread().start()创建。\
而所谓的Runnable、Callable……对象,这仅仅只是线程体,也就是提供给线程执行的任务,并不属于真正的Java线程,它们的执行,最终还是需要依赖于new Thread()……

大家换位思考一下,面试官问别人时,答案都是千篇一律的那三种、四种、五种……,而你能聊出这样的看法,是不是特别能让他眼前一亮?因此面试的差异化就来了,别人都是八股文选手,而你拥有着自己的理解,这自然能让对方给你打上更高的评分,和别人竞争Offer时,那不就是手到拈来嘛~

最后的最后,如果看完本文对你有所启发,记得点赞、关注、收藏支持一下,如果平时想更方便看技术文章的小伙伴,也欢迎关注我的同名公众号:竹子爱熊猫

相关文章
|
4天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
21 9
|
5天前
|
并行计算 数据处理 调度
Python中的并发编程:探索多线程与多进程的奥秘####
本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
|
7天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
4天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
7天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
21 3
|
5天前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
6天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
17 1
|
7天前
|
Java UED
Java中的多线程编程基础与实践
【10月更文挑战第35天】在Java的世界中,多线程是提升应用性能和响应性的利器。本文将深入浅出地介绍如何在Java中创建和管理线程,以及如何利用同步机制确保数据一致性。我们将从简单的“Hello, World!”线程示例出发,逐步探索线程池的高效使用,并讨论常见的多线程问题。无论你是Java新手还是希望深化理解,这篇文章都将为你打开多线程的大门。
|
5月前
|
Java C++
关于《Java并发编程之线程池十八问》的补充内容
【6月更文挑战第6天】关于《Java并发编程之线程池十八问》的补充内容
49 5
|
2月前
|
缓存 监控 Java
Java中的并发编程:理解并应用线程池
在Java的并发编程中,线程池是提高应用程序性能的关键工具。本文将深入探讨如何有效利用线程池来管理资源、提升效率和简化代码结构。我们将从基础概念出发,逐步介绍线程池的配置、使用场景以及最佳实践,帮助开发者更好地掌握并发编程的核心技巧。