当前位置: 首页 > news >正文

从零开始的多线程生活

从这一期开始,我会持续更新一个系列,即多线程编程,我们之前学习JAVA的时候大多都是只有main函数一个线程,从这一期开始,我将带领大家进入多线程的世界。

首先,为什么引入多线程?之前只有一个线程的时候不是也能运行的好好的吗?我用生活中的例子向大家解释。

假设是这么一个场景,想要去吃掉100只鸡,这里安排了一个人,相当于是单进程且单线程,这时候想完成目标是相当困难的,按照我们以往的思路,想要减轻负担,更高效的完成任务,我们要引入另一个人。

也就是这种场景,现在多来了一个人,他们两个人一人桌子上放50只鸡,这样任务就被缓解了,这属于多进程,但每个进程中还是只有一个线程,不过我们说这种方式也是不太恰当的。可能在这张图上还看不出来,但引入一个房间、一张桌子开销是很大的,是远远超过你只引入一个人的开销,如果再多一些进程呢?要知道进程是要占据资源的,每创建一个新的进程对应的就要分配内存资源等资源,久而久之在这种大开销下效率会很低,所以我们要引入多线程,减少开销。

正如此图所示,房间还是原来房间,桌子也是原来桌子,在此基础上多加一个人一起吃这100只鸡,这就是多线程,它的开销是远小于多进程的。

这时候你可能会想,如果引入更多线程呢?效率肯定会进一步提高。

比如这种情况,100只鸡分给四个人吃,效率会进一步提升,但是如果再多,多到一定程度下反而会适得其反。

如果像这种情况,或者再多,甚至一个人都分不到一只鸡,有的人就开始摸鱼了,这样效率反而降低,所以说,效率和线程的数目并不是严格的线性增长关系,达到一定阈值后,再增加线程个数反而会因为调度消耗过多资源,导致性能变慢。

我就拿一张桌子四个人的例子来说,其实上边是一种理想化的方式,有没有可能,有两个人都想吃同一只鸡,形成了一个竞争关系,产生了线程之间的冲突,这就是极其重要的线程安全问题,在代码编写的层面,出现这种情况,可能会出bug。影响程序的正常运行。

线程安全问题是很严重的,理想情况下,两个线程产生冲突,顶多是他们两个都拒绝工作拖累一下运行效率而已,但是如果一个线程在和其他线程产生冲突后直接抛出异常,那程序就中断了,其余没有产生冲突的线程也停止了工作,所以线程安全问题一定是值得重视的。

接下来,我带大家学习一下在Java编程中怎么引入多线程。

package thread.csdn; class MyThread extends Thread { @Override public void run() { while (true) { try { Thread.sleep(1000); System.out.println("hello thread"); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Demo1 { public static void main(String[] args) throws InterruptedException { Thread t=new MyThread(); t.start(); while (true) { Thread.sleep(1000); System.out.println("hello main"); } } }

这是我实现的一个简单的多线程代码,它的效果是t线程循环打印"hello thread",main线程循环打印"hello main"在每次循环打印前休眠1秒,给大家看一下这个代码的执行效果。

这只是我截取的一部分执行结果,并不是完整的结果,可以看到,确实是两个线程都在执行。好了,效果给大家演示完了,接下来我简要分析一下代码的原理。

大家应该注意到了,在编写多线程代码时,用到的一个核心类是Thread类。

JAVA对操作系统提供的由C语言编写的原生线程api和不同操作系统的不同线程api进行统一封装到Thread类中供程序员使用。Thread类是在java.lang中的,这个包是默认导入的,所以我们不需要手动导包也可以运行代码。

其次,我们看到,我自己实现的MyThread类继承了Thread类并重写了run方法,这个run方法就是我们自己实现的线程要执行的逻辑,你也可以把run方法当成自己创建的线程的入口。

在main函数中,我编写了t.start()的代码,这行语句执行的效果是执行我们重写的run方法,实际则是创建新线程,并执行我们为这个新线程编写的逻辑。

或许你有疑问,既然我说了t.start()就是执行run方法,那为啥不直接在main中调用run呢?非得拐个弯,反正start也得调用run,我给你演示一下这么敲代码的结果。

package thread.csdn; class MyThread extends Thread { @Override public void run() { while (true) { try { Thread.sleep(1000); System.out.println("hello thread"); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Demo1 { public static void main(String[] args) throws InterruptedException { Thread t=new MyThread(); //t.start(); t.run(); while (true) { Thread.sleep(1000); System.out.println("hello main"); } } }

看到了吗?如果这么写代码,只会执行thread线程里的内容,main线程的逻辑不会执行,或者说,main线程的逻辑会等到run执行完了之后才会执行。你有没有觉得很眼熟呀,这不就是我们平常敲代码时的效果吗?等上一行代码执行完了在执行下一行,也就是说,这么写达不到多线程编程的目的。因此,start的作用并不仅仅是调用run方法,还有一个更重要的作用是多了一个执行流,可以实现多线程的效果。

核心的逻辑已经讲差不多了,我接下来给大家简单介绍一下代码中调用的sleep方法吧,显然这是属于Thread类的静态方法,而且会抛出InterruptedException异常,而我们需要针对这个异常进行处理,不知道你是否观察到,在上面的代码中,我调用了两次sleep但是采用的确是不同的处理异常方式,在run函数中,我是用try-catch处理异常,在main方法中,我直接在函数名处声明了异常,这也是暗藏玄机的,首先,我可以两个都用try-catch捕获异常,但不能两个都声明异常,不要忘了,run方法不是MyThread独有的,它是重写的父类方法,在父类中没有声明异常,子类重写当然也不能改变。

接着来跟大家分析一下执行结果,可以看到有时候是main线程先执行,有时候又是thread线程先执行,这是为什么?有什么规律吗?

这是因为操作系统中,线程调度是随机的,也就是抢占式执行,多线程之间如何调度,按照什么顺序执行,这完全取决于操作系统,和程序员没什么关系,你即使是想修改也改不了,不过如果你为线程设置优先级,理论上倒是可以指定他们的调度顺序,但是操作系统也就是参考一下,并不一定会严格执行。

那么,我咋查看多个线程的情况呢?这时候需要运行一个第三方工具。

在你安装jdk的bin目录下有这样一个exe文件

jconsole.exe点击后可以与正在执行的进程建立连接

随后点击线程,你就能看到正在运行的两个线程了。

main线程自不必多说,Thread-0是线程的默认命名规则,我们并没有指定线程名字,所以它就是默认的Thread-n第一个线程就是0第二个就是1,以此类推...

其他的线程,就是jvm自带的线程了,当执行代码时这些线程都会自动执行。

点击线程后,可以观察到具体调用的栈以及执行的位置,这就不多说了。

回过头来我们接着看Thread类。

点击源码后能看到,这个类实现了Runnable接口

Runnable接口中只有一个抽象方法,就是run方法,这时候你恍然大悟了吧,原来刚刚我们一直重写的run方法不是Thread所独有的,而是它实现的接口里边带的。

所以,这为我们提供了一个多线程编程的新思路,既然我们继承的Thread类中最核心的run方法是Runnable接口里边的抽象方法,那我们直接写一个新的类并实现Runnable接口不就行了吗?反正Thread类调用start方法的时候也要调用run,在哪重写还不是一样?从逻辑角度上来讲,是大致可行的,但Thread类毕竟是系统自带类,我们怎么把写的接口传给他呢?Thread类里边提供了这样的构造方法吗?我们接着翻源码。

看到了吧,Thread提供了这样的构造方法,也就是说我们刚刚的方案是完全可行的。接下来我为大家编写一下代码,实现的还是相同的逻辑。

package thread.csdn; class MyRunnable implements Runnable { @Override public void run() { while (true) { try { Thread.sleep(1000); System.out.println("hello Runnable"); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class Demo2 { public static void main(String[] args) throws InterruptedException { Runnable runnable = new MyRunnable(); Thread thread = new Thread(runnable); thread.start(); while (true) { Thread.sleep(1000); System.out.println("hello main"); } } }

显然,这种方法也是可行的。从逻辑上分析,其实这两种方式是差不多的,本质都是Thread类实现的对象调用start方法,只是start里边调用的run方法在哪里被重写的区别,那为什么引入第二种写法呢?假如,你要对run方法的内容修改,第一种方式你要跑到Thread里边去修改,而这个类也同时调用了执行逻辑,但是第二种方法,你只需要改一下外边使用Runnable接口的类就行了,根本不需要动你的Thread类,这是不是就达成了解耦的效果呀。会让你的逻辑更加清晰,修改起来也更加方便。

好的,那么第零期多线程编程就先写到这里,接下来我会持续更新,希望对你有所帮助。

http://www.jsqmd.com/news/765673/

相关文章:

  • 告别模拟器:实战派教你用真机+BurpSuite高效抓包安卓App(附最新绕过证书锁定技巧)
  • 3步完全掌控Alienware灯光与风扇:告别AWCC臃肿软件的高效方案
  • 初阶模板(C++)
  • 3个必学Xournal++数字笔记技巧:从PDF批注到专业绘图
  • 别再只盯着阻抗了!FR4板材的损耗角正切(Df)如何悄悄吃掉你的高速信号?
  • ColabFold:让蛋白质结构预测变得简单高效的神器
  • 手把手教你用Simulink搞定Boost PFC电流环:从扫频到PI参数整定(附避坑指南)
  • 独立开发者如何通过Taotoken管理多个项目的AI密钥与权限
  • WHEELTEC N100 AHRS模块调平校准避坑指南:告别姿态角漂移与数据偏差
  • GetQzonehistory:一站式自动化备份QQ空间历史说说的智能开源工具
  • todg6.ocx文件丢失无法启动程序解决
  • 从用量看板观测API调用延迟与token消耗的日常波动
  • 风电仿真避坑指南:Matlab画功率曲线时,你的Cp公式用对了吗?
  • 《龙虾OpenClaw系列:从嵌入式裸机到芯片级系统深度实战60课》013、ADC与DAC:模拟信号采集与转换的硬件细节
  • 2026年浙江成人高考培训机构口碑排行,哪家靠谱值得选? - 浙江教育测评
  • 互联网大厂 Java 求职面试实战:从基础到微服务的精彩对话
  • BetterNCM安装器:如何让你的网易云音乐变得更好用?
  • 五一假期最后一天,还要补作业
  • AI 英语伴学 APP 的开发费用
  • 宿舍6人用免费试用降AIGC率:拼单方案完整复盘记录! - 我要发一区
  • Fan Control终极指南:5步打造完美的Windows风扇控制系统
  • 从双非到985:避开CS保研材料关的3个隐形陷阱(附真实案例复盘)
  • 如何构建Windows任务栏图标居中解决方案的安全加固与自动化检测体系
  • QRazyBox:三步修复损坏二维码的终极免费工具
  • 用Cityscapes预训练模型搞定KITTI语义分割:DeepLabv3+ (PyTorch) 实战避坑指南
  • vue基于springboot的旅游信息分享管理平台 旅游门票酒店预订系统
  • 从Windows迁移到Linux?保姆级教程:在Ubuntu/CentOS上安装配置Serv-U 15.4管理后台
  • RAG高级技巧
  • 用了有机肉桂后,我家厨房彻底变了样
  • 通过用量看板清晰观测 Taotoken 上各模型的调用成本与消耗