经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Java » 查看文章
面试官让说出8种创建线程的方式,我只说了4种,然后挂了。。。
来源:cnblogs  作者:JavaBuild  时间:2024/3/7 10:36:26  对本文有异议

写在开头

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

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

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

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

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

创建线程的10种方式

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

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

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

【代码示例1】

  1. public class Test {
  2. public static void main(String[] args) {
  3. new ThreadTest().start();
  4. }
  5. }
  6. //继承 Thread,重写 run() 方法
  7. class ThreadTest extends Thread {
  8. @Override
  9. public void run() {
  10. for (int i = 0; i <3; i++) {
  11. System.out.println(Thread.currentThread().getName() + ":" + i);
  12. }
  13. }
  14. }

输出:

  1. Thread-0:0
  2. Thread-0:1
  3. Thread-0:2

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

【代码示例2】

  1. public class Test {
  2. public static void main(String[] args) {
  3. new Thread("t1"){
  4. @Override
  5. public void run() {
  6. System.out.println(Thread.currentThread().getName());
  7. }
  8. }.start();
  9. }
  10. }
  11. //打印结果:t1

② 实现Runnable接口

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

【代码示例3】

  1. public class Test implements Runnable{
  2. public static void main(String[] args) {
  3. Test test = new Test();
  4. new Thread(test).start();
  5. }
  6. @Override
  7. public void run() {
  8. System.out.println("我是Runnable线程");
  9. }
  10. }
  11. //打印结果:我是Runnable线程

③ 实现Callable接口

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

【代码示例4】

  1. public class Test implements Callable<String> {
  2. public static void main(String[] args) throws ExecutionException, InterruptedException {
  3. Test test = new Test();
  4. FutureTask<String> stringFutureTask = new FutureTask<>(test);
  5. new Thread(stringFutureTask).start();
  6. System.out.println(stringFutureTask.get());
  7. }
  8. @Override
  9. public String call() throws Exception {
  10. return "我是线程Callable";
  11. }
  12. }
  13. //打印结果:我是线程Callable

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

④ 使用ExecutorService线程池

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

【代码示例5】

  1. public class Test {
  2. public static void main(String[] args) {
  3. // 使用工具类 Executors 创建单线程线程池,其实还有其他几种创建方式
  4. ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
  5. //提交执行任务
  6. singleThreadExecutor.submit(() -> {System.out.println("单线程线程池执行任务");});
  7. //关闭线程池
  8. singleThreadExecutor.shutdown();
  9. }
  10. }
  11. //打印结果:单线程线程池执行任务

⑤ 使用CompletableFuture类

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

【代码示例6】

  1. public class Test {
  2. public static void main(String[] args) throws InterruptedException {
  3. CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
  4. System.out.println(Thread.currentThread().getName() + ":"+"CompletableFuture");
  5. return "CompletableFuture";
  6. });
  7. // 需要阻塞,否则看不到结果
  8. Thread.sleep(1000);
  9. }
  10. }
  11. //打印结果:ForkJoinPool.commonPool-worker-1:CompletableFuture

⑥ 基于ThreadGroup线程组

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

【代码示例7】

  1. public class Test {
  2. public static void main(String[] args) {
  3. ThreadGroup group = new ThreadGroup("groupName");
  4. new Thread(group, ()->{
  5. System.out.println("T1......");
  6. }, "T1").start();
  7. new Thread(group, ()->{
  8. System.out.println("T2......");
  9. }, "T2").start();
  10. new Thread(group, ()->{
  11. System.out.println("T3......");
  12. }, "T3").start();
  13. }
  14. }

输出:

  1. T1......
  2. T2......
  3. T3......

⑦ 使用FutureTask类

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

【代码示例8】

  1. public class Test {
  2. public static void main(String[] args) {
  3. FutureTask<String> futureTask = new FutureTask<>(() -> {
  4. System.out.println(Thread.currentThread().getName()+":"+"futureTask");
  5. return "futureTask";
  6. });
  7. new Thread(futureTask).start();
  8. }
  9. }
  10. //执行结果:Thread-0:futureTask

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

【源码解析1】

  1. public FutureTask(Callable<V> callable) {
  2. if (callable == null)
  3. throw new NullPointerException();
  4. this.callable = callable;
  5. this.state = NEW;
  6. }
  7. public FutureTask(Runnable runnable, V result) {
  8. // 通过适配器RunnableAdapter来将Runnable对象runnable转换成Callable对象
  9. this.callable = Executors.callable(runnable, result);
  10. this.state = NEW;
  11. }

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

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

【代码示例9】

  1. public class Test {
  2. public static void main(String[] args) {
  3. //new Runnable 对象,匿名重写 run() 方法
  4. new Thread(new Runnable() {
  5. @Override
  6. public void run() {
  7. System.out.println("匿名创建线程");
  8. }
  9. }).start();
  10. //JDK 1.8 开始支持 lambda 表达式
  11. new Thread(() ->
  12. System.out.println("lambda创建线程")
  13. ).start();
  14. }
  15. }

⑨ 使用Timer定时器类

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

【代码示例10】

  1. public class Test {
  2. public static void main(String[] args) {
  3. Timer timer = new Timer();
  4. timer.schedule(new TimerTask() {
  5. @Override
  6. public void run() {
  7. System.out.println("定时器线程");
  8. }
  9. }, 0, 1000);
  10. }
  11. }

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

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

【代码示例11】

  1. public class Test {
  2. public static void main(String[] args) {
  3. //ForkJoinPool线程池
  4. ForkJoinPool forkJoinPool = new ForkJoinPool();
  5. forkJoinPool.execute(()->{
  6. System.out.println(Thread.currentThread().getName()+":"+"ForkJoinPool线程池");
  7. });
  8. //parallelStream流
  9. List<String> list = Arrays.asList(Thread.currentThread().getName()+":"+"parallelStream流");
  10. list.parallelStream().forEach(System.out::println);
  11. }
  12. }

输出:

  1. ForkJoinPool-1-worker-1:ForkJoinPool线程池
  2. //并行流在主线程中被打印。
  3. main:parallelStream

总结

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

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

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

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!
image
如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!
image

原文链接:https://www.cnblogs.com/JavaBuild/p/18058345

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号