导言
今日来聊一个比较有意思的话题,这是一道Java
陈腔滥调文中的陈腔滥调文,简称陈腔滥调文Plus
!
这个疑问在很久之前便在我心中产生了,久远到什么程度呢?大约能够从盘古开天地开端算起,哈哈哈。
先来看看本次的主角,便是一个问题:“Java
中有几种创立线程的办法?”
我们能够先试着答复一下,答复完之后再往下看。
PS:个人编写的《技能人求职指南》小册已结束,其间从技能总结开端,到制定期望、技能突击、简历优化、面试预备、面试技巧、谈薪技巧、面试复盘、选
Offer
办法、新人入职、进阶提升、职业规划、技能管理、涨薪跳槽、裁定补偿、副业兼职……,为我们打造了一套“从求职到跳槽”的一条龙服务,一起也为诸位预备了七折优惠码:3DoleNaE
,感兴趣的小伙伴能够点击:s./ds/USoa2R3/了解概况!
一、浅谈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类
CompletableFuture
是JDK1.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并行流
ForkJoin
是JDK1.7
引进的新线程池,根据分治思想完成。而后续JDK1.8
的parallelStream
并行流,默许就根据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();
}
先new
出Runnable
目标,接着再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
》的第二十一章,也有重复提及到。于是,国内阅读过这两本书本的人,在写文章、写面试题、写书本、授课、录视频……时,把这个概念越传越泛,按照“三人成虎”准则,Java
有3、4
种创立线程的办法,这个观念变成了事实,从此刻在了每个Java
开发者的DNA
里。
好了,搞清楚问题的缘由,咱们回到前面提出的问题,已然完成Runnable、Callable
接口,不是创立线程的办法,那它们究竟是什么?准确来说,这是两种创立“线程体”的办法,包含承继Thread
类重写run()
办法也是。
三、线程与线程体的联系
前面或许提出了一个我们没触摸过的新概念:线程体,这是个啥?来看看ChatGPT
的解说:
看完这个答复,信任我们就能理解“线程体”是怎样一回事了,说简单点,线程是一个独立的履行单元,能够被操作系统调度;而线程体仅仅只是一个使命,就相似于一段一般的代码,需求线程作为载体才干运转,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
时,那不便是手到拈来嘛~
最后的最后,假如看完本文对你有所启示,记得点赞、关注、收藏支撑一下,假如平时想更方便看技能文章的小伙伴,也欢迎关注我的同名公众号:竹子爱熊猫。