面试官让说出8种创建线程的方式,我只说了4种,然后挂了。。。

简介: 面试官让说出8种创建线程的方式,我只说了4种,然后挂了。。。

写在开头

昨天有个小伙伴私信说自己面试挂在了“Java有几种创建线程的方式”上,我问他怎么回答的,他说自己有背过八股文,回答了:继承Thread类、实现Runnable接口、实现Callable接口、使用线程池这四种,但是面试官让说出8种创建方式,他没说出来,面试就挂了,面试官给的理由是:只关注八股文背诵,对线程的理解不够深刻!

在这里想问一下大家,这位小伙伴回答的这四种有问题吗?看过《Java核心技术卷》和《Java编程思想》的朋友应该都知道,在这两本书中对于多线程编程都有详细的介绍,并且也都提到了线程创建的方式:

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

鉴于这两本书的权威性,以及在国内的广泛传播,让很多学习者,写书者,教学者都以此为标准,长此以往,这种回答似乎就成了一种看似完美的标准答案了。

因此,这位小伙伴的回答在大部分面试官那里都是正确的,没有什么大问题,但既然这位面试官抛出了8种的提问,很明显他要的回答并不是八股文参考答案。那应该怎么回答才能征服这位面试官呢?请接着往下看!

创建线程的10种方式

既然面试官想看线程创建的方式,我们就往上整,不仅仅他要的8种,我们还可以说出10种,甚至更多,今天花了点时间,梳理了一下之前用到过得以及网上看到的线程创建的办法,我们通过一个个小demo去感受一下。🥰
image.png

① 继承Thread类,并重写run()方法

这是最基本的一个线程创建的方式,闲话少叙,直接上代码!

【代码示例1】
public class Test {
   
    public static void main(String[] args) {
   
        new ThreadTest().start();
    }
}

//继承 Thread,重写 run() 方法
class ThreadTest extends Thread {
   
    @Override
    public void run() {
   
        for (int i = 0; i <3; i++) {
   
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
AI 代码解读
输出:
Thread-0:0
Thread-0:1
Thread-0:2
AI 代码解读

创建一个ThreadTest 并继承Thread类,重写run方法,来创建一个线程,当然我们还可以采用匿名内部类去重写run方法来创建线程,这其实也可以算所一种方式

【代码示例2】
public class Test {
   
    public static void main(String[] args) {
   
        new Thread("t1"){
   
            @Override
            public void run() {
   
                System.out.println(Thread.currentThread().getName());
            }
        }.start();
    }
}
//打印结果:t1
AI 代码解读

② 实现Runnable接口

这也是常用的四个方式之一,实现Runnable接口并重写run方法。

【代码示例3】
public class Test implements Runnable{
   

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

    @Override
    public void run() {
   
        System.out.println("我是Runnable线程");
    }
}
//打印结果:我是Runnable线程
AI 代码解读

③ 实现Callable接口

这种方式实现Callable接口,可以创建有返回值的线程。

【代码示例4】
public class Test implements Callable<String> {
   

    public static void main(String[] args) throws ExecutionException, InterruptedException {
   
        Test test = new Test();
        FutureTask<String> stringFutureTask = new FutureTask<>(test);
        new Thread(stringFutureTask).start();
        System.out.println(stringFutureTask.get());
    }

    @Override
    public String call() throws Exception {
   
        return "我是线程Callable";
    }
}
//打印结果:我是线程Callable
AI 代码解读

这个示例里使用了FutureTask,这个类可用于异步获取执行结果或取消执行任务的场景。通过传入Runnable或者Callable的任务给FutureTask,直接调用其run方法或者放入线程池执行,之后可以在外部通过FutureTask的get方法异步获取执行结果。

④ 使用ExecutorService线程池

通过Executors创建线程池,Executors 类是从 JDK 1.5 开始就新增的线程池创建的静态工厂类,它就是创建线程池的,但是很多的大厂已经不建议使用该类去创建线程池。原因在于,该类创建的很多线程池的内部使用了无界任务队列,在并发量很大的情况下会导致 JVM 抛出 OutOfMemoryError,直接让 JVM 崩溃,影响严重。因此,在这里我们只将它作为一个案例参考,真实开发中不建议使用!

【代码示例5】
public class Test  {
   
    public static void main(String[] args) {
   
        // 使用工具类 Executors 创建单线程线程池,其实还有其他几种创建方式
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        //提交执行任务
        singleThreadExecutor.submit(() -> {
   System.out.println("单线程线程池执行任务");});
        //关闭线程池
        singleThreadExecutor.shutdown();
    }
}
//打印结果:单线程线程池执行任务
AI 代码解读

⑤ 使用CompletableFuture类

CompletableFuture是JDK1.8引入的新类,CompletableFuture 除了提供了更为好用和强大的 Future 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。后面的文章更新中会详说,现在先上代码!

【代码示例6】
public class Test  {
   

    public static void main(String[] args) throws InterruptedException {
   
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
   
            System.out.println(Thread.currentThread().getName() + ":"+"CompletableFuture");
            return "CompletableFuture";
        });
        // 需要阻塞,否则看不到结果
        Thread.sleep(1000);
    }
}
//打印结果:ForkJoinPool.commonPool-worker-1:CompletableFuture
AI 代码解读

⑥ 基于ThreadGroup线程组

在Java的线程中同样有组的概念,可以通过ThreadGroup创建一个线程组,在线程组中创建多个线程。

【代码示例7】
public class Test  {
   

    public static void main(String[] args) {
   

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

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

        new Thread(group, ()->{
   
            System.out.println("T3......");
        }, "T3").start();
    }
}
AI 代码解读
输出:
T1......
T2......
T3......
AI 代码解读

⑦ 使用FutureTask类

看到这个FutureTask类是不是很熟悉,对喽!咱们在第三种方式,实现Callable接口,重写call方法中也用到了它,他们的实现方式几乎都万变不离其宗,只不过我们在这里采用了lambda 表达式调用。

【代码示例8】
public class Test  {
   
    public static void main(String[] args) {
   
        FutureTask<String> futureTask = new FutureTask<>(() -> {
   
            System.out.println(Thread.currentThread().getName()+":"+"futureTask");
            return "futureTask";
        });
        new Thread(futureTask).start();
    }
}
//执行结果:Thread-0:futureTask
AI 代码解读

其实虽然是匿名方式,它的底部仍然调用了callable。我们来看一下FutureTask底层的构造方法,都是通过传参或者调用callable。

【源码解析1】
public FutureTask(Callable<V> callable) {
   
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;
}
public FutureTask(Runnable runnable, V result) {
   
    // 通过适配器RunnableAdapter来将Runnable对象runnable转换成Callable对象
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;
}
AI 代码解读

⑧ 使用匿名内部类或Lambda表达式

这种方式其实在上面的实现中多少都有提到,匿名方式创建,lambda 表达式创建。

【代码示例9】
public class Test  {
   

    public static void main(String[] args) {
   
        //new Runnable 对象,匿名重写 run() 方法
        new Thread(new Runnable() {
   
            @Override
            public void run() {
   
                System.out.println("匿名创建线程");
            }
        }).start();
        //JDK 1.8 开始支持 lambda 表达式
        new Thread(() ->
                System.out.println("lambda创建线程")
        ).start();
    }
}
AI 代码解读

⑨ 使用Timer定时器类

Timer类在JDK1.3时被引入,用来执行定时任务,里面需要传入两个数字,第一个代表启动后多久开始执行,第二个代表每间隔多久执行一次,单位是ms毫秒。

【代码示例10】
public class Test  {
   
    public static void main(String[] args) {
   
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
   
            @Override
            public void run() {
   
                System.out.println("定时器线程");
            }
        }, 0, 1000);
    }
}
AI 代码解读

⑩ 使用ForkJoin线程池或Stream并行流

ForkJoin是JDK1.7引入的新线程池,基于分治思想实现。而后续JDK1.8的parallelStream并行流,默认就基于ForkJoin实现,我们直接上代码感受一下。

【代码示例11】
public class Test  {
   

    public static void main(String[] args) {
   
        //ForkJoinPool线程池
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        forkJoinPool.execute(()->{
   
            System.out.println(Thread.currentThread().getName()+":"+"ForkJoinPool线程池");
        });
        //parallelStream流
        List<String> list = Arrays.asList(Thread.currentThread().getName()+":"+"parallelStream流");
        list.parallelStream().forEach(System.out::println);
    }
}
AI 代码解读
输出:
ForkJoinPool-1-worker-1:ForkJoinPool线程池
//并行流在主线程中被打印。
main:parallelStream流
AI 代码解读

总结

OK,我们根据面试官的需求,写出了10种创建线程的方式,如果再细分,甚至还可以更多,毕竟线程池的工具类还有没往上写的呢。

那么,我们一起静默3分钟,好好思考一下,在Java中创建一个线程的本质,真的是八股文中所说的3种、4种、8种,甚至更多吗?Build哥认为,真正创建线程的方式只有1种,其他的衍生品都算套壳!

考虑到本篇已经六七千字了,所以我们在下一篇文章中来分析一下为什么“真正创建线程的方式只有1种!”

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

目录
打赏
0
1
1
0
66
分享
相关文章
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
面试必看:如何设计一个可以优雅停止的线程?
嘿,大家好!我是小米。今天分享一篇关于“如何停止一个正在运行的线程”的面试干货。通过一次Java面试经历,我明白了停止线程不仅仅是技术问题,更是设计问题。Thread.stop()已被弃用,推荐使用Thread.interrupt()、标志位或ExecutorService来优雅地停止线程,避免资源泄漏和数据不一致。希望这篇文章能帮助你更好地理解Java多线程机制,面试顺利! 我是小米,喜欢分享技术的29岁程序员。欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
114 53
面试大神教你:如何巧妙回答线程优先级这个经典考题?
大家好,我是小米。本文通过故事讲解Java面试中常见的线程优先级问题。小明和小华的故事帮助理解线程优先级:高优先级线程更可能被调度执行,但并非越高越好。实际开发需权衡业务需求,合理设置优先级。掌握线程优先级不仅能写出高效代码,还能在面试中脱颖而出。最后,小张因深入分析成功拿下Offer。希望这篇文章能助你在面试中游刃有余!
48 4
面试大神教你:如何巧妙回答线程优先级这个经典考题?
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
147 14
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
66 13
面试中的难题:线程异步执行后如何共享数据?
本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
106 6
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
115 11
面试必问的多线程优化技巧与实战
多线程编程是现代软件开发中不可或缺的一部分,特别是在处理高并发场景和优化程序性能时。作为Java开发者,掌握多线程优化技巧不仅能够提升程序的执行效率,还能在面试中脱颖而出。本文将从多线程基础、线程与进程的区别、多线程的优势出发,深入探讨如何避免死锁与竞态条件、线程间的通信机制、线程池的使用优势、线程优化算法与数据结构的选择,以及硬件加速技术。通过多个Java示例,我们将揭示这些技术的底层原理与实现方法。
178 3
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC

热门文章

最新文章