Java创建线程的方式只有一种:Thread+Runnable
Java创建线程的方式其实只有一种
- 👨🎓一、继承Thread
- 👨🎓二、实现Runnable接口
- 👨🎓三、实现Callable接口
- 👨🎓四、通过线程池创建
- 👨🎓五、总结
一般我们会认为创建线程的方式是三到四种,其实 本质上这四种没有任何区别,都是利用Thread+Runnable来进行实现的多线程,其实java自始至终创建多线程的方式都只有一种,下面一起看下他们是怎么对Runnable进行改造成多种多样的。
👨🎓一、继承Thread
继承Thread下面是常见的写法
public class Test1 extends Thread{@Overridepublic void run() {while(true){System.out.println("线程1");}}public static void main(String[] args) {new Test1().start();}}
利用这种方式实现的线程,就是继承Thread类然后重写run方法,我们在run方法内部进行线程逻辑的编写。java的start方法会调用start0方法,start0是一个native方法,底层会调用run方法进行执行线程,所以我们是重写run方法。那run方法是怎么来的呢?

上面是run方法在Thread中的实现,我们可以看到他被注解Override修饰了,说明这个方法是父类的方法,我们点击左侧红色向上的箭头,就会发现跳到了Runnable接口中,如下

这样就很简单明了了,所以这样就简单明了了,我们在继承Thread重写run方法时其实重写的是Runnable的run方法。这这种已经证明了我们开始说的java是利用Thread+Runnable实现的多线程了。
👨🎓二、实现Runnable接口
这种方式也是需要依赖Thread才能去创建线程,如下所示
public class TestThread {public static void main(String[] args) {new Thread(() -> {while(true)System.out.println("Runnable多线程1");}).start();new Thread(() -> {while(true)System.out.println("Runnable多线程2");}).start();}}
那我们来看下这个实现方式到底是如何进行多线程创建的吧,我们还是从run方法开始,因为Thread启动线程时调用的是自己的run方法,那我们就先看下他自己的run方法实现:

可以看到Thread的run方法调用的是target.run方法,那target是什么呢?ctrl+左会发现,他就是一个Runnable,那这个Runnable怎么来的呢?我们创建过程中只在Thread的构造方法中传入了Runnable,那是不是在这里咱们一起看下:

这里没有做什么实质的操作,所以继续看,最后调用到了这里:
private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name.toCharArray();Thread parent = currentThread();SecurityManager security = System.getSecurityManager();if (g == null) {/* Determine if it's an applet or not *//* If there is a security manager, ask the security managerwhat to do. */if (security != null) {g = security.getThreadGroup();}/* If the security doesn't have a strong opinion of the matteruse the parent thread group. */if (g == null) {g = parent.getThreadGroup();}}/* checkAccess regardless of whether or not threadgroup isexplicitly passed in. */g.checkAccess();/** Do we have the required permissions?*/if (security != null) {if (isCCLOverridden(getClass())) {security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);}}g.addUnstarted();this.group = g;this.daemon = parent.isDaemon();this.priority = parent.getPriority();if (security == null || isCCLOverridden(parent.getClass()))this.contextClassLoader = parent.getContextClassLoader();elsethis.contextClassLoader = parent.contextClassLoader;this.inheritedAccessControlContext =acc != null ? acc : AccessController.getContext();this.target = target; // 这里是关键点setPriority(priority);if (parent.inheritableThreadLocals != null)this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);/* Stash the specified stack size in case the VM cares */this.stackSize = stackSize;/* Set thread ID */tid = nextThreadID();}
我们会发现这个方法内部将我们从构造器中传入的Runnable对象放到了this.target中,所以可以看到Thread中的run方法调用的就是我们重写的run方法。所以这个也证明了java中是通过Thread+Runnable实现的多线程方式。
👨🎓三、实现Callable接口
这种方式我们需要依赖FutrureTask,其实我们可以接受返回值也得归功FutureTask,下面是常见的实现代码:
public class TestThread {public static void main(String[] args) throws Exception{FutureTask<String> futureTask = new FutureTask<>(()->{int i =0 ;while(i<100)System.out.println("Callable线程1在执行:"+i++);return "线程1执行完了";});FutureTask<String> futureTask2 = new FutureTask<>(()->{int i =0 ;while(i<100)System.out.println("Callable线程2在执行:"+i++);return "线程2执行完了";});new Thread(futureTask).start();new Thread(futureTask2).start();System.out.println(futureTask.get());System.out.println(futureTask2.get());}}
run方法才是多线程的执行地方,还是一样我们还是从run方法开始看,我们点击下new Thread会进入Thread的构造器,发现进入的构造器和第二种是一样的:

这就说明FutureTask一定实现了或者继承了Runnable接口,其实FutureTask实现了RunnableFuture接口,而RunnableFuture继承了Runnable接口,因为继承和实现的特性,相当于FutureTask实现了Runnable接口。那走到这个构造器就是理所当然那的了。所以与第二种方式相同的是Thread调用的target.run就是FutureTask的run了。我们来看下FutureTask的run方法吧:
public void run() {if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))return;try {Callable<V> c = callable;if (c != null && state == NEW) {V result;boolean ran;try {result = c.call();ran = true;} catch (Throwable ex) {result = null;ran = false;setException(ex);}if (ran)set(result);}} finally {// runner must be non-null until state is settled to// prevent concurrent calls to run()runner = null;// state must be re-read after nulling runner to prevent// leaked interruptsint s = state;if (s >= INTERRUPTING)handlePossibleCancellationInterrupt(s);}}
上面是FutureTask的run方法,里面真正调用的是c.call方法,看到call方法应该就明白了,不错这个call就是我们传入到FutureTask中的Callable实例的call方法,所以FutureTask的调用路线也就清晰了:Thread.start–>Thread.run–>FutureTask.run–>Callable.call。而FutureTask则是Runnable的子类,所以也证明了我们一开始说的java是通过Thread+Runnable来实现的多线程。此外通过这里我们还可以清洗的看到FutureTask是怎么实现参数返回接收的,就是因为call方法有返回,然后FutureTask的run方法接收到返回后将他存放到自身的泛型V中,然后我们就可以直接通过FutureTask.get方法进行获取了。其实这种思想java里有很多,比如常见的http请求的包装类也是通过这种思想来处理流数据的。
👨🎓四、通过线程池创建
线程池这里咱们采用自己实现的线程池来进行解释说明,其实没啥区别,只不过是我们一般使用线程池都是禁止直接使用jdk自带线程池的所以才有用自己实现的线程池来看这个问题
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,1, 60,TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy() );threadPoolExecutor.execute(()->{while(true){System.out.println("线程4执行中");}});
关于各个参数的意思,这里就不解释了,需要的看这里:4万字爆肝总结java多线程知识点,言归正传,我们看下线程池是怎么创建线程的,线程池提交任务有submit和execute还有一个定时的schedule,不过schedule他的线程池类不是这个,不过原理一样这里只介绍ThreadPoolExecutor的。我们看下execute的实现方法
public void execute(Runnable command) {if (command == null)throw new NullPointerException();// 此处省略原文注释int c = ctl.get();if (workerCountOf(c) < corePoolSize) {//判断线程是否小于核心线程数,很明显第一次会进入这里if (addWorker(command, true))return;c = ctl.get();}if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();if (! isRunning(recheck) && remove(command))reject(command);else if (workerCountOf(recheck) == 0)addWorker(null, false);}else if (!addWorker(command, false))reject(command);}
很明显第一次执行会执行:addWorker(command, true)这个方法,我们再看下这个方法做了什么
private boolean addWorker(Runnable firstTask, boolean core) {retry:for (;;) {int c = ctl.get();int rs = runStateOf(c);// Check if queue empty only if necessary.if (rs >= SHUTDOWN &&! (rs == SHUTDOWN &&firstTask == null &&! workQueue.isEmpty()))return false;for (;;) {int wc = workerCountOf(c);if (wc >= CAPACITY ||wc >= (core ? corePoolSize : maximumPoolSize))return false;if (compareAndIncrementWorkerCount(c))break retry;c = ctl.get(); // Re-read ctlif (runStateOf(c) != rs)continue retry;// else CAS failed due to workerCount change; retry inner loop}}boolean workerStarted = false;boolean workerAdded = false;Worker w = null;try {w = new Worker(firstTask);//这个firstTask实际就是我们从execute传入的Runnable实现类final Thread t = w.thread;//获取thread,因为线程开启必须利用Thread的start方法,这两步就是最关键的地方if (t != null) {final ReentrantLock mainLock = this.mainLock;mainLock.lock();try {// Recheck while holding lock.// Back out on ThreadFactory failure or if// shut down before lock acquired.int rs = runStateOf(ctl.get());if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {if (t.isAlive()) // precheck that t is startablethrow new IllegalThreadStateException();workers.add(w);int s = workers.size();if (s > largestPoolSize)largestPoolSize = s;workerAdded = true;}} finally {mainLock.unlock();}if (workerAdded) {t.start();//这里真正调用了线程启动workerStarted = true;}}} finally {if (! workerStarted)addWorkerFailed(w);}return workerStarted;}
最关键的两步,笔者在代码中加了注释了,可以看到会将我们传入的Runnable的实现类交给Worker的构造器,那我们看看这个构造器又干了什么:

可以看到在这个构造器里面,将我们传入的Runnable给到了自己的Runnable,对他的私有变量进行了初始化,然后又对thread进行了初始化,而初始化Thread时传入了this,注意传入的是this。因为Worker实现了Runable,所以当我们真正执行start方法时,调用的应该是Worker的run方法,所以我们到这里应该去看run方法了,run方法很简单,他直接调用了runWorer方法,那看下runWorker的实现:
final void runWorker(Worker w) {Thread wt = Thread.currentThread();Runnable task = w.firstTask;//将Runnable的对象指向一个新的引用w.firstTask = null;//失效原引用w.unlock(); // allow interrupts//加锁,worker实现了AQS,支持锁boolean completedAbruptly = true;try {while (task != null || (task = getTask()) != null) {w.lock();// If pool is stopping, ensure thread is interrupted;// if not, ensure thread is not interrupted. This// requires a recheck in second case to deal with// shutdownNow race while clearing interruptif ((runStateAtLeast(ctl.get(), STOP) ||(Thread.interrupted() &&runStateAtLeast(ctl.get(), STOP))) &&!wt.isInterrupted())wt.interrupt();try {beforeExecute(wt, task);Throwable thrown = null;try {task.run();//这里相当于运行了我们在execute中传入的Runnable对象的run方法了。} catch (RuntimeException x) {thrown = x; throw x;} catch (Error x) {thrown = x; throw x;} catch (Throwable x) {thrown = x; throw new Error(x);} finally {afterExecute(task, thrown);}} finally {task = null;w.completedTasks++;w.unlock();}}completedAbruptly = false;} finally {processWorkerExit(w, completedAbruptly);}}
看笔者在代码中加的注释很清晰就看到最终调用的是我们从execute中传入的Runnable的对象的run方法。所以我们看到这里也是可以得出一个解决线程池还是利用Thread+Runnable接口一起实现的多线程。那么来从新梳理下线程池的调用过程:
execute(Runnable)–>
addWorker(command, true)–>内部对Worker(Runnale)进行初始化,Worker该类实现了Runnable,初始化Worker时生产线程Thread传入Worker自己,之后获取上一步生产的thread调用start方法。执行start方法,实际执行的是Worker的run方法,worker.run方法调用了runWorker方法,该方法执行时是将Worker的Runnable的对象的run方法进行调用执行。而Worker的Runnable的对象就是在addWorker中由execute方法传入的Runnable实现类。这样整个流程就很清晰了,我们也是可以佐证一开始说的观点了。
👨🎓五、总结
看了以上四种分析,我们可以清晰的发现了java中其实创建线程的方式就只有一种就是利用Thread+Runnable来实现多线程。其他多有方式都是对这个实现方式的变种。如有不对欢迎路过的朋友指正,也希望能帮到路过的朋友

相关文章:
Java创建线程的方式只有一种:Thread+Runnable
Java创建线程的方式其实只有一种👨🎓一、继承Thread👨🎓二、实现Runnable接口👨🎓三、实现Callable接口👨🎓四、通过线程池创建👨🎓五、总结一般我…...
数据加密--课后程序(Python程序开发案例教程-黑马程序员编著-第3章-课后作业)
实例6:数据加密 数据加密是保存数据的一种方法,它通过加密算法和密钥将数据从明文转换为密文。 假设当前开发的程序中需要对用户的密码进行加密处理,已知用户的密码均为6位数字,其加密规则如下: 获取每个数字的ASCI…...
【GO】K8s 管理系统项目33[前端部分–登录和登出]
K8s 管理系统项目[前端部分–登录和登出] 1. 登录登出流程 1.1 登录流程 登入流程总的分为5步: 账号密码验证token生成token验证验证成功进行跳转验证失败返回/login 1.2 登出流程 登出流程就相对简单,分为2步 删除Token跳转/login 2. 登录代码 src/views/login/Login.v…...
Vue 计算属性基础知识 监听属性watch
计算属性的概念 在{{}}模板中放入太多的逻辑会让模板内容过重且难以维护。例如以下代码: <div id"app">{{msg.split().reverse().join()}}</div><script>const vm new Vue({el: "#app",data: {msg:我想把vue学的细一点}})&…...
PAT:L1-004 计算摄氏温度、L1-005 考试座位号、L1-006 连续因子(C++)
目录 L1-004 计算摄氏温度 问题描述: 实现代码: L1-005 考试座位号 问题描述: 实现代码: 原理思路: L1-006 连续因子 问题描述: 实现代码: 原理思路: 过于简单的就不再写…...
Redis集群方案应该怎么做?
今天我们来跟大家唠一唠JAVA核心技术-RedisRedis是一款流行的内存数据库,适用于高性能的数据缓存和实时数据处理。当需要处理大量数据时,可以使用Redis集群来提高性能和可用性。Redis在单节点模式下,虽然可以支持高并发、快速读写、丰富的数据…...
连续点击返回键退出Android 应用
问题 业务需要,在主界面连续点击返回键退出应用,记录一下。 解决方案 先说结论,在主界面Activity中添加如下代码 /*** 记录上次点击返回键时间*/private long lastClickTime 0;/*** 两次回退点击时间间隔设置不小于2s*/public static fi…...
【PyTorch】教程:torch.nn.Hardswish
torch.nn.Hardswish 原型 CLASS torch.nn.Hardswish(inplaceFalse) 参数 inplace (bool) – 内部运算,默认为 False 定义 Hardswish(x){0if x≤−3,xif x≥3,x⋅(x3)/6otherwise\text{Hardswish}(x) \begin{cases} 0 & \text{if~} x \le -3, \\ x & \te…...
nacos源码入门
nacos官方文档地址:nacos官方文档 Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 简单来说,nacos就是一个注册中心、配置中心࿰…...
【记录】Samba|Windows 11的Samba连接切换用户
Samba是一个用于共享文件和打印机的网络协议,可以使不同的操作系统之间共享文件和资源变得容易。在Windows 11上,可以使用Samba来连接到网络共享。 如果您想在Windows 11上切换用户并连接到另一个Samba共享,可以按照以下步骤操作。 文章目录…...
vue hiprint vue使用hiprint打印控件VUE HiPrint HiPrint简单使用
vue hiprint vue使用hiprint打印控件VUE HiPrint HiPrint简单使用安装相关依赖安装Hi PrintJQuery引入依赖简单使用官方所有 打印示例安装相关依赖 安装Hi Print npm install vue-plugin-hiprintJQuery 因为 hi print 使用到了 JQuery 所以需要安装对应依赖 npm i jquery -…...
HBase常用Shell命令
HBase提供了一个非常方便的命令行交互工具HBase Shell。通过HBase Shell,HBase可以与MySQL命令行一样创建表、索引,也可以增加、删除和修改数据,同时集群的管理、状态查看等也可以通过HBase Shell实现。 一、数据定义语言 数据定义语言&…...
【阿里云】Apsara Clouder云计算专项技能认证-云服务器ECS入门,考试真题分享
以下是阿里云Apsara Clouder云计算专项技能认证-云服务器ECS入门真题汇总篇分享: 1.下列哪一个不是重置ECS密码的步骤? A. 查看实例详情 B.进入控制台 C.远程连接ECS D.点击控制台“概览” 2.针对云服务器ECS安全组说法正确的是 A.是一种物理防火墙 B.仅用于控制…...
怎样编写java程序
搭建好了Java开发环境之后,下面就来学习一下如何开发Java程序。为了让初学者更好地完成第一个Java程序,接下来通过几个步骤进行逐一讲解。 1.编写Java源文件 在D盘根目录下新建一个test文件夹,并在该文件夹中新建文本文档&#…...
面向对象设计模式:结构型模式之适配器模式
一、引入 Object Oriented Adapters 二、XX 模式 aka:Wrapper (包装器) 2.1 Intent 意图 Convert the interface of a class into another interface clients expect. 将一个类的接口转换成客户希望的另外一个接口. 作为两个不兼容的接口之间的桥梁 适配器模式使…...
Unity3D Shader系列之模板测试
一、 模板测试原理模板测试位于GPU渲染流水线的逐片元操作阶段,片元着色器完成之后就会进入模板测试,模板测试通过后再进入深度测试。我们的GPU中有一个模板缓冲区(Stencil Buffer)(Stencil即是模板的意思),其大小为整个屏幕大小*8位…...
机器学习中的数学——精确率与召回率
在Yolov5训练完之后会有很多图片,它们的具体含义是什么呢? 通过这篇博客,你将清晰的明白什么是精确率、召回率。这个专栏名为白话机器学习中数学学习笔记,主要是用来分享一下我在 机器学习中的学习笔记及一些感悟,也希…...
Oracle启动数据库报ORA-01102解决办法
1.机器启动之后登录服务器使用sqlplus / as sysdba 登录数据库发现数据库并没有启动之前把数据库服务添加过开机自启动 2.使用startup命令启动数据库报错了 SYSorcl>startup; ORACLE 例程已经启动。 Total System Global Area 2471931904 bytes Fixed Size 2255752 byt…...
Go 语言面向对象编程及实践
面向对象编程是计算机科学中的一种重要的编程方法,它将数据和处理它的代码组合成对象,并将这些对象组合成更大的程序。在 Go 语言中,我们同样可以使用面向对象编程的方式进行开发。本篇文章将介绍 Go 语言面向对象编程的概念、特性、使用方法以及实践技巧。 面向对象编程概…...
0102 MySQL05
1.约束 1.约束(constraint):在创建表时,可以给表中的字段加上一些约束,保证表中数据的完整性,有效性 常见的约束? 非空约束:not null 唯一性约束:unique 主键约束&am…...
eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)
说明: 想象一下,你正在用eNSP搭建一个虚拟的网络世界,里面有虚拟的路由器、交换机、电脑(PC)等等。这些设备都在你的电脑里面“运行”,它们之间可以互相通信,就像一个封闭的小王国。 但是&#…...
(二)TensorRT-LLM | 模型导出(v0.20.0rc3)
0. 概述 上一节 对安装和使用有个基本介绍。根据这个 issue 的描述,后续 TensorRT-LLM 团队可能更专注于更新和维护 pytorch backend。但 tensorrt backend 作为先前一直开发的工作,其中包含了大量可以学习的地方。本文主要看看它导出模型的部分&#x…...
五年级数学知识边界总结思考-下册
目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解:由来、作用与意义**一、知识点核心内容****二、知识点的由来:从生活实践到数学抽象****三、知识的作用:解决实际问题的工具****四、学习的意义:培养核心素养…...
pikachu靶场通关笔记22-1 SQL注入05-1-insert注入(报错法)
目录 一、SQL注入 二、insert注入 三、报错型注入 四、updatexml函数 五、源码审计 六、insert渗透实战 1、渗透准备 2、获取数据库名database 3、获取表名table 4、获取列名column 5、获取字段 本系列为通过《pikachu靶场通关笔记》的SQL注入关卡(共10关࿰…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...
多模态图像修复系统:基于深度学习的图片修复实现
多模态图像修复系统:基于深度学习的图片修复实现 1. 系统概述 本系统使用多模态大模型(Stable Diffusion Inpainting)实现图像修复功能,结合文本描述和图片输入,对指定区域进行内容修复。系统包含完整的数据处理、模型训练、推理部署流程。 import torch import numpy …...
GO协程(Goroutine)问题总结
在使用Go语言来编写代码时,遇到的一些问题总结一下 [参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现: 今天在看到这个教程的时候,在自己的电…...
篇章二 论坛系统——系统设计
目录 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 1. 数据库设计 1.1 数据库名: forum db 1.2 表的设计 1.3 编写SQL 2.系统设计 2.1 技术选型 2.2 设计数据库结构 2.2.1 数据库实体 通过需求分析获得概念类并结合业务实现过程中的技术需要&#x…...
32单片机——基本定时器
STM32F103有众多的定时器,其中包括2个基本定时器(TIM6和TIM7)、4个通用定时器(TIM2~TIM5)、2个高级控制定时器(TIM1和TIM8),这些定时器彼此完全独立,不共享任何资源 1、定…...
【实施指南】Android客户端HTTPS双向认证实施指南
🔐 一、所需准备材料 证书文件(6类核心文件) 类型 格式 作用 Android端要求 CA根证书 .crt/.pem 验证服务器/客户端证书合法性 需预置到Android信任库 服务器证书 .crt 服务器身份证明 客户端需持有以验证服务器 客户端证书 .crt 客户端身份…...
