Java 从入门到精通(十四):多线程入门,为什么程序一并发就开始变得“不听话”?

news2026/4/13 23:05:11
Java 从入门到精通十四多线程入门为什么程序一并发就开始变得“不听话”前一篇我们把 NIO 这条线讲清楚了为什么 Java 后来不满足于传统 IO为什么会引入 Path、Files、Buffer、Channel、Selector 这些更偏工程化的抽象。但当你继续往后学很快就会遇到另一个更容易让人头疼的话题多线程。很多人第一次接触多线程时都会有一种很强烈的落差感。前面学变量、分支、循环、方法、类、集合时代码基本都还符合一种“顺着往下执行”的直觉第 1 行执行完再执行第 2 行然后执行第 3 行程序逻辑像一条清晰的线但一旦进入并发世界这种直觉会迅速被打破。因为这时你要面对的不再只是“代码写没写对”而是为什么明明逻辑没问题结果却偶尔不对为什么有时候运行正常有时候结果乱掉为什么线程一多程序反而更慢为什么两个线程同时改一个变量就会出现莫名其妙的 bug为什么别人总说“线程安全”但新手很难直观理解它到底在说什么所以这篇文章不是要把并发包一次性讲完而是先把多线程最重要的入门骨架搭起来线程到底是什么它和进程是什么关系为什么程序需要并发而不是一直单线程Java 里创建线程有哪些方式start() 和 run() 到底差在哪什么叫线程安全为什么共享数据最容易出问题学多线程时初学者最容易踩哪些坑你先把这套基础认知搭稳后面再去学同步、锁、线程池、并发容器、JUC才不会一上来就被术语砸晕。一、先搞清楚什么是进程什么是线程很多教程上来就说线程是“程序执行的最小单位”这句话没错但太抽象。更直观一点的理解是1进程正在运行的一个程序实例比如你电脑上同时开着IDEA微信Chrome音乐播放器这些运行中的应用每一个都可以看作一个进程。进程有自己的资源空间比如内存文件句柄网络连接运行上下文所以进程更像是一个“独立的运行容器”。2线程进程内部真正执行任务的路径一个进程里不一定只有一条执行线。比如浏览器可以同时做这些事渲染页面加载图片执行 JavaScript处理用户点击发网络请求如果这些事情全都串行排队体验会非常差。所以一个进程内部通常会有多个线程分别负责不同工作。你可以先把它粗略理解成进程是房子线程是房子里活动的人房子提供空间和资源人真正去干活。二、为什么单线程很多时候不够用很多初学者一开始会想“一个程序顺着执行不是挺好吗为什么非要搞多线程”因为现实任务并不总适合排成一条线。1为了同时处理多个任务比如一个聊天程序可能要同时接收消息显示界面发送图片保存聊天记录如果全部都靠一条线程串着做只要其中某一步卡住其他事情就会被拖住。2为了避免界面卡死这是桌面程序和移动端里很常见的动机。比如主线程负责界面渲染如果你把一个耗时操作直接放进去读取大文件请求网络复杂计算那界面就会卡住用户会感觉“程序死了”。所以常见做法是主线程负责交互后台线程负责耗时任务3为了更好利用多核 CPU现代机器通常不是只有一个 CPU 核心。如果程序能把任务拆开并行执行就有机会把多核算力利用起来。当然这里要注意一个现实问题不是“开线程”就一定更快。如果任务本身拆不开或者线程切换成本太高线程越多反而越慢。所以多线程不是银弹它只是解决特定问题的一种手段。三、Java 程序其实从一开始就有线程很多人以为“写了多线程代码程序才有线程”。其实不是。Java 程序启动后至少就已经有一条主线程在执行 main() 方法。例如publicclassDemo{publicstaticvoidmain(String[]args){System.out.println(Thread.currentThread().getName());}}运行后通常会输出main这说明你的程序天然就在一个线程里运行。所谓“多线程”只是说除了主线程之外你又创建了新的执行路径。四、Java 创建线程的两种经典方式入门阶段最常见的是这两种继承 Thread实现 Runnable后面你会发现工程里更常用线程池和 ExecutorService但基础认知还是得从这里来。五、方式一继承 Thread 类先看最经典的写法publicclassMyThreadextendsThread{Overridepublicvoidrun(){for(inti1;i5;i){System.out.println(getName() 正在执行i);}}}然后在主方法里启动它publicclassDemo{publicstaticvoidmain(String[]args){MyThreadt1newMyThread();MyThreadt2newMyThread();t1.start();t2.start();}}你会看到输出顺序通常不是固定的。这很正常。因为两个线程是并发执行的调度顺序由 JVM 和操作系统共同决定。这种写法的优点直观好理解适合第一次接触线程时建立概念它的局限Java 只能单继承如果你的类已经继承了别的父类就不能再继承 Thread“任务逻辑”和“线程对象”耦合得太紧所以在真实开发里更推荐第二种方式。六、方式二实现 Runnable 接口publicclassMyTaskimplementsRunnable{Overridepublicvoidrun(){for(inti1;i5;i){System.out.println(Thread.currentThread().getName() 执行任务i);}}}使用时这样写publicclassDemo{publicstaticvoidmain(String[]args){MyTasktasknewMyTask();Threadt1newThread(task,线程A);Threadt2newThread(task,线程B);t1.start();t2.start();}}这里要注意一个关键变化Runnable 负责描述“要做什么事”Thread 负责把这个任务放到线程里执行这就比直接继承 Thread 更灵活。为什么 Runnable 更常用因为它把任务执行载体拆开了。这是一种更合理的设计。后面学线程池时你会更明显感受到这一点线程池管理的是线程而你提交进去的是任务。七、run() 和 start() 到底有什么区别这是多线程入门最经典、也最容易被面试反复问的一个点。很多初学者第一次写线程时会这样MyThreadtnewMyThread();t.run();然后以为“线程启动了”。其实没有。1直接调用 run()本质上只是把一个普通方法当普通方法执行。也就是说代码还是跑在当前线程里。2调用 start()才是真正告诉 JVM请启动一个新线程并在这个新线程里执行 run()。所以一定要记住run()普通方法调用start()启动新线程看一个对比publicclassDemo{publicstaticvoidmain(String[]args){ThreadtnewThread(()-{System.out.println(子线程Thread.currentThread().getName());});t.run();System.out.println(主线程Thread.currentThread().getName());}}这里很可能两行都输出 main。但如果改成t.start();那么子任务就会在另一个线程里执行。这个区别必须吃透。八、为什么线程的输出顺序经常“乱”很多人第一次运行多线程程序都会困惑“我代码明明写得很顺为什么输出不按顺序来”因为多个线程并发执行时谁先拿到 CPU 时间片谁先输出并不是你代码书写顺序决定的。例如publicclassDemo{publicstaticvoidmain(String[]args){Threadt1newThread(()-{for(inti0;i5;i){System.out.println(A-i);}});Threadt2newThread(()-{for(inti0;i5;i){System.out.println(B-i);}});t1.start();t2.start();}}输出可能是A-0A-1B-0A-2B-1B-2A-3B-3A-4B-4也可能完全是另一种顺序。这不是 bug而是并发的正常表现。所以多线程世界里一个基本认知是不要依赖“看起来刚好是这样”的执行顺序。如果逻辑必须有顺序保障那你需要显式控制而不是碰运气。九、什么叫共享数据为什么它最危险真正让多线程变复杂的不是“同时执行”本身而是多个线程同时操作同一份数据。比如这样一个例子publicclassCounter{intcount0;publicvoidincrement(){count;}}如果只有一个线程调用 increment()基本没问题。但如果多个线程同时调它就可能出错。为什么 count 不安全很多初学者会觉得“加 1 不是一个动作吗”其实从底层看它往往不是单一步骤而更像先读出 count在原值上加 1再把结果写回去如果两个线程同时做这三步就可能发生覆盖。比如线程 A 读到 5线程 B 也读到 5A 写回 6B 也写回 6结果两次加一最后却只变成了 6。这就是典型的并发问题。十、什么是线程安全你现在不用追求特别严格的定义先抓住最实用的理解如果一段代码在多个线程同时使用时结果仍然正确、不会出现数据错乱那它就是线程安全的。对应地不安全的常见信号包括结果偶尔不对同样代码重复跑每次结果不一样明明逻辑正确却会“随机出 bug”小规模测试没问题一并发就出事这也是为什么并发 bug 特别烦人。因为它们常常不是“必现”的而是“偶发”的。偶发 bug 比稳定 bug 更难查。十一、先别急着记锁先记住三类并发风险初学多线程时不要一上来就背 synchronized、Lock、volatile 一堆术语。先把问题本身认出来更重要。1竞争条件多个线程争抢同一份资源导致结果依赖执行时机。例如多个线程同时修改一个计数器。2可见性问题一个线程改了变量另一个线程未必立刻看得到。这类问题初学时比较抽象但后面学 volatile 就会更清楚。3有序性问题某些操作在底层执行时顺序未必和你表面看到的一模一样。这一点在深入并发模型时很关键不过入门阶段先知道它存在就够了。先记住并发问题不是只有“抢变量”这么简单。它至少还涉及“看不看得见”和“执行顺序会不会变”。十二、线程常见的几个基础方法先建立直觉入门阶段你不需要一口气全背完但下面这些很常见1sleep()让当前线程暂停一段时间。try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}常用于演示线程交替执行模拟耗时任务但要注意sleep() 不会释放你已经拿到的锁。2join()让一个线程等待另一个线程执行完。例如ThreadtnewThread(()-{System.out.println(子线程开始);});t.start();t.join();System.out.println(主线程继续);这表示主线程要等 t 执行结束再往下走。3currentThread()获取当前正在执行的线程对象。它对调试非常有用。System.out.println(Thread.currentThread().getName());4setName() / 线程名给线程起个名字调试时会清楚很多。比如ThreadtnewThread(task,订单处理线程);比一堆默认的 Thread-0、Thread-1 更容易看懂。十三、为什么说“线程越多不一定越快”这是一个特别值得尽早建立的观念。很多人初学并发时会天然觉得一个线程干活不如十个线程一起干十个线程不如一百个线程一起干听起来很合理但现实并不是线性增长。因为线程本身也有成本创建成本销毁成本上下文切换成本内存占用成本共享数据同步成本如果任务很小、线程很多程序可能大部分时间都浪费在线程调度上。所以多线程的目标不是“开更多线程”而是用合适的并发方式提高吞吐、响应性或资源利用率。这也是后面为什么要学线程池。线程池的核心思想本质上就是别频繁手搓线程统一管理更合理。十四、初学多线程最容易踩的 6 个坑1把 run() 当成启动线程的方法这是最经典的坑。再次强调run() 只是普通方法start() 才会开启新线程2看到输出顺序乱就以为程序错了并发本来就不保证自然顺序。不能拿单线程直觉去要求多线程。3多个线程共享一个变量却没意识到会出问题这会直接导致数据错乱。4以为加了线程性能就一定提升实际还要看任务是否适合并行线程切换是否过多有没有锁竞争CPU 是否真能吃满5用 sleep() 解决同步问题很多新手喜欢这样写“我先 sleep(100)另一个线程应该就跑完了吧。”这非常不稳。sleep() 只能“拖时间”不能保证逻辑同步正确。真正需要顺序控制时应考虑 join()、锁、条件变量等更可靠手段。6一上来就猛学高级并发包基础却没稳如果你连线程是什么共享数据为什么危险start() / run() 区别为什么结果会乱序这些基础都没吃透直接上 AQS、线程池源码、CAS通常只会越学越乱。十五、给你一个更稳的学习顺序如果你现在刚接触多线程我建议按这个顺序学第一步先建立线程直觉先彻底理解进程和线程的关系为什么要并发如何创建线程start() 和 run() 的区别第二步再理解共享数据问题重点搞清楚什么是线程安全为什么 count 会出错什么叫竞争条件第三步再学同步手段包括synchronized锁对象同步代码块volatile原子类第四步最后进入工程化并发包括线程池并发容器FutureCompletableFutureJUC 工具类这样推进比一上来就硬啃高级并发原理稳得多。十六、最后总结多线程难不是因为 API 多而是因为“时间”进来了如果说前面学集合、泛型、异常、IO本质上还是在处理“单条执行线里的正确性”那么多线程带来的最大变化是程序不再只和数据打交道而是开始和“时间顺序”打交道。也正因为这样问题会突然变复杂同样的代码先后顺序不同结果就可能不同多个线程同时执行输出天然可能交错共享变量一旦出现逻辑就不再只是“写对语法”那么简单所以你这篇真正要带走的不是会写两个线程打印数字而是这几个核心认识1线程是进程内部的执行路径Java 程序启动后本身就有主线程。2多线程是为了解决并发任务、响应性和资源利用率问题但它不是越多越好。3创建线程的经典方式有两种继承 Thread实现 Runnable工程里通常更偏向 Runnable 这种任务与线程分离的方式。4start() 和 run() 必须分清这是多线程入门的第一道门槛。5真正危险的地方在共享数据线程安全问题本质上多数都和共享状态有关。从这里开始Java 学习就正式进入一个更像“真实工程”的阶段了。因为并发不是可选装饰而是现代程序几乎绕不开的一块地基。下一篇最自然的继续方向就是线程同步与 synchronized为什么多个线程改同一个变量时结果总会乱

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2514528.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…