第20章 多线程入门

发布时间:2026/8/28 6:08:51

第20章 多线程入门
第20章 多线程入门让程序同时做多件事就是多线程。本节学习线程的创建方式、生命周期和简单同步。多线程是并发编程的基础也是面试重点。一、进程与线程进程正在运行的程序是资源分配的基本单位。一个进程内有独立的内存空间。线程进程内的执行单元是 CPU 调度的基本单位。一个进程至少有一个线程主线程可以有多个线程共享进程的内存。进程和线程的区别对比项进程线程资源独立内存空间共享所属进程的内存切换开销大要切换内存空间小通信跨进程麻烦直接读写共享变量崩溃影响进程间互不影响一个线程出问题可能带崩整个进程多线程的好处充分利用多核 CPU4 核 CPU 上 4 个线程可以真并行耗时任务下载、查询不阻塞主流程提升程序响应速度界面不卡为什么需要多线程想象单线程下载大文件下载的几秒里整条程序都卡住用户点哪都没反应。把下载扔进子线程主线程继续响应点击、刷新界面——等待和干活并行互不阻塞。二、创建线程的两种方式方式一继承 Thread 类classMyThreadextendsThread{Overridepublicvoidrun(){for(inti1;i5;i){System.out.println(getName() 运行中i);}}}MyThreadt1newMyThread();t1.start();// 启动线程不要直接调用 run()两个线程同时 start输出会交替出现顺序不固定——这就是并发的直观感受A、B 的打印穿插在一起谁先谁后由 CPU 调度决定。方式二实现 Runnable 接口推荐classMyRunnableimplementsRunnable{Overridepublicvoidrun(){for(inti1;i5;i){System.out.println(Thread.currentThread().getName()i);}}}ThreadtnewThread(newMyRunnable(),工作线程);t.start();为什么推荐 RunnableJava 单继承实现接口不影响继承其他类任务run 逻辑和线程Thread分离更符合把任务交给线程执行的思想——同一份任务可以交给多个线程执行而继承 Thread 做不到后面学的线程池就基于 Runnable/Callable方式三Lambda 简化JDK 8ThreadtnewThread(()-{System.out.println(Lambda 线程);});t.start();Runnable只有一个抽象方法是函数式接口所以能用 Lambda 简化。这个写法从 JDK 8 引入后JDK 11、17 里用法完全一样放心用。三、start() 与 run() 的区别高频考点MyThreadtnewMyThread();t.start();// 开启新线程run() 在新线程中异步执行t.run();// 直接调用 run()只是普通方法调用不新开线程start()才创建新线程run()只是普通方法。忘了 start 直接调 run代码会在主线程顺序执行看不到并发效果。用输出顺序一眼看出区别t.run();// run()main 线程顺序执行输出一定是先 A 后 Bt.start();// start()新线程与主线程抢 CPU输出顺序不固定另外start() 只能调用一次重复 start 会抛IllegalThreadStateException易错点再展开。四、线程的生命周期新建(New) → 就绪(Runnable) → 运行(Running) → 死亡(Terminated) ↑ ↓ 阻塞/等待(Blocked/Waiting) → 就绪新建new Thread() 创建就绪start() 后等待 CPU 调度运行获得 CPU 时间片执行阻塞/等待sleep、wait、IO 等待等条件满足后回到就绪死亡run() 执行完或异常终止用t.getState()可以亲眼观察状态流转start 前是 NEWstart 后是 RUNNABLE大概率执行完是 TERMINATEDsleep 期间是 TIMED_WAITING。常用方法Thread.sleep(1000);// 静态方法当前线程休眠 1000 毫秒t.join();// 等待 t 线程执行完当前线程再继续主线程等子线程收尾的典型用法Thread.currentThread();// 获取当前线程对象t.getName();// 线程名t.setName(名字);// 设置线程名也可以构造器传五、线程安全问题多个线程同时操作共享数据会产生数据不一致classCounter{intcount0;voidincrement(){count;// 问题所在count 不是原子操作}}count在 CPU 层面其实是三步读 count → 加 1 → 写回。两个线程同时执行可能都读到 0都写成 1导致结果少了。这属于并发三大问题里的原子性问题另外两个是可见性、有序性。CountercnewCounter();// 开 10 个线程每个自增 1000 次for(inti0;i10;i){newThread(()-{for(intj0;j1000;j){c.increment();}}).start();}// 主线程等待所有线程执行完简化写法Thread.sleep(2000);System.out.println(c.count);// 期望 10000实际可能不到// 输出9387参考值每次运行都不同多数小于 10000为什么会丢假设 count 现在是 5线程 A 读到 5 还没写回线程 B 也读到 5两个都写 6——加了两次结果只加了一次。线程越多、竞争越激烈丢得越厉害。六、解决线程安全synchronized用synchronized给方法加锁同一时刻只有一个线程能执行classCounter{intcount0;synchronizedvoidincrement(){count;}}加锁后再跑上一节的 10×1000 测试结果永远是 10000。synchronized两种用法// 1. 修饰方法锁住整个方法publicsynchronizedvoidmethod(){}// 2. 修饰代码块只锁关键部分粒度更细、性能更好publicvoidmethod(){// 非共享代码不用锁synchronized(this){// 操作共享数据的代码}}锁的本质线程进入同步代码前必须先获得对象锁没拿到就阻塞等待。注意锁的对象是实例而不是代码——两个线程必须锁同一个对象才互斥各自 new 一个对象再锁等于没锁。七、线程安全的集合开发中常直接用线程安全容器省去手动加锁// 老式的线程安全集合VectorStringvnewVector();HashtableString,StringhtnewHashtable();// 并发包 java.util.concurrent推荐高性能线程安全集合// 从 JDK 8 起就是标准库标配JDK 11、17 里同样内置ConcurrentHashMapString,StringmapnewConcurrentHashMap();CopyOnWriteArrayListStringlistnewCopyOnWriteArrayList();并发容器帮你把锁写好了直接用10 个线程各往 CopyOnWriteArrayList 加 100 个元素结果稳定是 1000换成普通 ArrayList 会小于 1000甚至抛并发修改异常。老式 Vector/Hashtable 给每个方法都加了锁并发高时性能差是历史遗留现代项目优先用 concurrent 包。八、线程池进阶预告频繁 new Thread 开销大正式开发用线程池复用线程importjava.util.concurrent.ExecutorService;importjava.util.concurrent.Executors;ExecutorServicepoolExecutors.newFixedThreadPool(4);// 4 个线程的池for(inti0;i10;i){inttaskIdi;pool.execute(()-System.out.println(任务 taskId 执行中));}pool.shutdown();// 关闭线程池Runnable 没有返回值想让线程算个结果回来用 Callable Futurepool.submit(() - 42)返回 Futurefuture.get()阻塞取结果。线程池核心参数核心线程数、最大线程数、任务队列、拒绝策略——并发进阶再详讲。九、扩展知识9.1 sleep vs wait 初识两者都能让线程歇一会儿但差别很大对比项Thread.sleep(ms)Object.wait()谁的静态方法Thread 的静态方法Object 的实例方法要不要锁不需要任何地方能调必须在 synchronized 内是否释放锁不释放抱着锁睡释放锁让别人进来怎么醒来到时间自动醒等 notify/notifyAll 唤醒典型配合生产者/消费者。厨师做好菜就 wait 等通知服务员取走菜就 notifyclassKitchen{synchronizedvoidcook()throwsInterruptedException{System.out.println(厨师做好菜等待取走);wait();// 释放锁并等待通知System.out.println(菜被取走继续做下一道);}synchronizedvoidtake(){System.out.println(服务员取走菜);notify();// 唤醒一个等待该锁的线程}}publicstaticvoidmain(String[]args)throwsInterruptedException{KitchenknewKitchen();newThread(()-{try{k.cook();}catch(InterruptedExceptione){}}).start();Thread.sleep(100);// 保证厨师先拿到锁k.take();}// 输出厨师做好菜等待取走// 服务员取走菜// 菜被取走继续做下一道9.2 守护线程用户线程主线程结束不影响它会自己跑完如业务线程守护线程所有用户线程结束后守护线程自动结束如 GC 垃圾回收线程、后台日志用法daemon.setDaemon(true)必须在start()之前设置之后主线程一结束守护线程自动退出不会无限跑下去。适合后台心跳、日志清理这类陪跑任务。9.3 线程优先级setPriority(1~10)给线程设置优先级默认 5Thread.MIN_PRIORITY1、MAX_PRIORITY10。注意这只是给调度器的建议具体由操作系统决定不能依赖它保证执行顺序——了解即可别在业务里依赖优先级。9.4 死锁初识两个线程各自持有一把锁又都想拿对方的锁互相等对方释放——谁都不让程序卡死ObjectlockAnewObject();ObjectlockBnewObject();newThread(()-{synchronized(lockA){System.out.println(线程1拿到锁A);try{Thread.sleep(100);}catch(InterruptedExceptione){}synchronized(lockB){// 等锁B但锁B在线程2手里System.out.println(线程1拿到锁B);}}}).start();newThread(()-{synchronized(lockB){System.out.println(线程2拿到锁B);try{Thread.sleep(100);}catch(InterruptedExceptione){}synchronized(lockA){// 等锁A但锁A在线程1手里System.out.println(线程2拿到锁A);}}}).start();// 输出线程1拿到锁A// 线程2拿到锁B// 之后互相等程序卡死这就是死锁避免死锁的入门准则所有线程按同样的顺序拿锁都先 A 后 B就永远不会互相等。十、易错点1. 直接调用 run() 而不是 start()// ❌ run() 只是普通方法调用没有并发效果t.run();// ✅ start() 才创建新线程t.start();2. start() 重复调用t.start();t.start();// ❌ 抛 IllegalThreadStateException// ✅ 一个线程只能 start 一次要再跑就 new 一个新线程3. 共享变量不加同步数据错乱// ❌ count 不是原子操作多线程下结果变少// ✅ 加 synchronized或直接用原子类JDK 8 自带importjava.util.concurrent.atomic.AtomicInteger;AtomicIntegercountnewAtomicInteger(0);count.incrementAndGet();// 原子自增无需加锁4. sleep 抛 InterruptedException 未处理编译报错// ❌ Thread.sleep(1000); // 编译错误Unhandled exception type InterruptedException// ✅ 捕获或声明抛出try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}5. Lambda 里直接使用循环变量// ❌ 编译报错i 不是 effectively final循环中会变for(inti0;i5;i){newThread(()-System.out.println(i)).start();}// ✅ 先复制到局部变量for(inti0;i5;i){inttaskIdi;newThread(()-System.out.println(taskId)).start();}6. 主线程不等子线程就结束// ❌ main 跑完就退出子线程可能没跑完// ✅ 用 join() 等子线程完成再收尾worker.join();十一、小结进程是资源分配的基本单位线程是 CPU 调度的基本单位线程共享进程内存创建线程继承 Thread / 实现 Runnable推荐/ LambdaJDK 811、17 用法一致start()开新线程且只能调一次run()只是普通方法调用生命周期新建 → 就绪 → 运行 → 阻塞/等待 → 死亡多线程操作共享数据有安全风险count 不是原子操作synchronized 加锁解决或用原子类/并发容器sleep 不释放锁wait 释放锁必须在 synchronized 内靠 notify 唤醒生产环境用线程池和并发容器不手写裸线程多把锁注意按相同顺序拿避免死锁下一节最后一节用综合项目串起前面所有知识。下一篇第21章 实战学生信息管理系统综合应用

相关新闻

MySQL 性能优化・诗意化记忆

MySQL 性能优化・诗意化记忆

2026/8/28 6:08:51

覆盖索引、explain、SQL、表设计、锁、配置,古风短句,面试背诵向总起库中万绪藏千行,慢索翻寻耗时光。 优化不从一处取,表、索、SQL、锁与章。一、表结构设计优化plaintext建表先把范式量,适度冗余莫过僵。 字段类型宜…

蓝桥杯单片机国赛实战:模块化设计与状态机编程精解

蓝桥杯单片机国赛实战:模块化设计与状态机编程精解

2026/8/28 6:08:51

1. 项目概述:第九届蓝桥杯单片机国赛深度复盘最近在整理资料时,翻到了当年参加第九届蓝桥杯单片机设计与开发大学组国赛的完整工程文件和笔记。作为一项在国内高校电子类学生中极具影响力的赛事,蓝桥杯单片机赛道的国赛题目,向来是…

AI Agent在物流行业的落地陷阱与架构设计实战

AI Agent在物流行业的落地陷阱与架构设计实战

2026/8/28 6:08:51

从 2023 年大模型爆发开始,几乎每隔一段时间就会出现一个“让 AI 替我做点正事”的讨论热点。到了 2025 年前后,这个热点落到了 AI Agent 身上:不再是简单的问答,而是让模型自己去查数据、做判断、调工具、完成任务。物流行业天然…

从蓝桥杯真题“玛雅神庙”解析Scratch坐标思维与碰撞检测

从蓝桥杯真题“玛雅神庙”解析Scratch坐标思维与碰撞检测

2026/8/28 7:18:54

1. 项目概述:从一道国赛真题看Scratch编程思维最近在整理历年蓝桥杯青少年创意编程组的国赛真题时,我又仔细研究了一下“玛雅神庙”这道题。这道题在圈内老师中讨论度一直不低,它远不止是让角色在舞台上移动那么简单,而是非常典型…

蓝桥杯平面切分问题解析:从数学归纳到增量算法实现

蓝桥杯平面切分问题解析:从数学归纳到增量算法实现

2026/8/28 7:18:54

1. 项目概述:从一道真题看平面几何的思维跃迁最近在整理蓝桥杯的历年真题时,我又翻出了那道经典的“平面切分”问题。这道题乍一看,像是初中数学里的找规律,但真正动手去解,尤其是想用程序高效、准确地求解时&#xff…

STM32 ADC单通道电压测量:从原理到精度提升的完整实践指南

STM32 ADC单通道电压测量:从原理到精度提升的完整实践指南

2026/8/28 7:18:54

1. 项目概述:从“测电压”到“感知世界”的桥梁在嵌入式开发的世界里,STM32系列微控制器因其强大的性能和丰富的生态,成为了无数工程师和爱好者的首选。而ADC(模数转换器)功能,则是连接数字芯片与模拟物理世…

超声波ToF传感器5m量程实战:原理、驱动与温度补偿

超声波ToF传感器5m量程实战:原理、驱动与温度补偿

2026/8/28 7:18:54

这几周我一直在调一款超声波ToF传感器,板子上的丝印写着标称量程5m,刚拿到手的时候我心里是打鼓的——超声波这东西,做到2m、3m很常见,真跑到5m的消费级模块不多。但实测下来,这一代传感器确实把量程做上去了&#xff…

前端面试题:如何控制批量接口请求的最大并发数

前端面试题:如何控制批量接口请求的最大并发数

2026/8/28 7:18:54

前端面试题:如何控制批量接口请求的最大并发数📌 题目一次性需要发送 100 条接口请求,不能全部并发(会压垮后端),也不能完全串行(太慢),如何控制最大并发数?请…

Loop Engineering与原生模型切换:构建Codex与Claude多模型工作流

Loop Engineering与原生模型切换:构建Codex与Claude多模型工作流

2026/8/28 7:08:53

做 AI 编程落地时,很多人都会遇到一个尴尬的局面:同一个模型并不能包打天下。写脚本、改配置时,Codex 可能一轮就完成任务;做架构评审、改测试时,Claude 的理解往往更细腻;而当你希望批处理多个仓库任务时&…

[光学原理与应用-521]:对光的错误理解与纠偏

[光学原理与应用-521]:对光的错误理解与纠偏

2026/8/27 11:10:02

首先光是一种能量的载体和形态,宏观上观察到的光是由无数个微观的光量子组成的,每个光子在产生的瞬间,其在真空的空间中以确定不变的速度沿着一个初始的方向一直向前,在微观层面,每个光量子的运动轨迹是以波函数所展现…

SIP通话转接原理与REFER方法实战解析

SIP通话转接原理与REFER方法实战解析

2026/8/27 7:25:23

1. 通话转接不是“挂断再拨号”,而是SIP会话的动态重定向你有没有遇到过这样的场景:客服坐席A正在和客户通电话,突然需要把这通对话无缝转给专家坐席B,客户完全感知不到中间的断连——既没听到忙音,也没被要求重新拨号…

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

2026/8/26 17:50:58

1. 为什么选择Kolla-ansible来部署单节点OpenStack?如果你正在寻找一种能把OpenStack从“概念”快速变成“可用的实验环境”的方法,那么Kolla-ansible几乎是当前最主流、最省心的选择。我见过太多人卡在手动编译依赖、配置服务、处理版本冲突的泥潭里&am…

基于Claude Code的开源AI求职框架:从职位搜索到Offer的全自动化闭环

基于Claude Code的开源AI求职框架:从职位搜索到Offer的全自动化闭环

2026/8/28 0:08:32

当AI助手能够独立完成从职位匹配、简历定制到面试准备的全链路求职流程时,求职不再是一场信息战,而是一场工程化战役。框架概述:本地运行的AI求职引擎这是一个构建在Claude Code之上的开源AI求职框架,核心理念是"在工作者的机…

Godot 4 仿 agar.io:相机缩放被 max_zoom 卡死,窗口越大球越小的根因与修复

Godot 4 仿 agar.io:相机缩放被 max_zoom 卡死,窗口越大球越小的根因与修复

2026/8/28 0:08:32

1. 问题现象 在 Godot 4 仿 agar.io 的 2D 项目中,相机缩放设计为「由球组整体尺寸决定」,世界可见高度恒定,窗口只作为视口裁剪。默认小窗口 1280x720 时相机高度正常;但窗口最大化到 2940x1912 后,视角被明显拉远、…

从软件测试大赛到实战:Java+Selenium自动化测试进阶指南

从软件测试大赛到实战:Java+Selenium自动化测试进阶指南

2026/8/28 0:08:32

1. 缘起:从校园到赛场,我的软件测试之路几年前,我还是一个在校园里对着Java课本和“Hello World”程序挠头的普通学生。软件测试对我来说,只是一个在开发流程末尾、用鼠标点点按钮的模糊概念。直到我偶然在学校的公告栏上看到了“…

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

2026/8/22 2:02:26

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…

导师推荐!2026最新AI论文工具测评与实用推荐

导师推荐!2026最新AI论文工具测评与实用推荐

2026/8/26 18:07:30

2026年真正好用的AI论文工具,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

告别游戏崩溃:XCOM 2模组管理器的智能革命

告别游戏崩溃:XCOM 2模组管理器的智能革命

2026/8/26 17:57:52

告别游戏崩溃:XCOM 2模组管理器的智能革命 【免费下载链接】xcom2-launcher The Alternative Mod Launcher (AML) is a replacement for the default game launchers from XCOM 2 and XCOM Chimera Squad. 项目地址: https://gitcode.com/gh_mirrors/xc/xcom2-lau…