第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 性能优化・诗意化记忆

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

2026/10/21 6:28:04 阅读更多 →
AI参与创造时任务意义感为何下降?机制拆解与工程应对

AI参与创造时任务意义感为何下降?机制拆解与工程应对

当 AI 做了“创意”,人为什么突然不想努力了?任务意义感流失的机制拆解与工程应对在最近的 AI 协作实践中,我反复遇到一个很有意思的现象:同一份需求文档,如果由我手动梳理并写出初版方案,我会很投入地反复…

2026/10/21 1:44:49 阅读更多 →
ATK-RK3568 Atmel 触摸屏适配记录

ATK-RK3568 Atmel 触摸屏适配记录

I2C电容触摸屏(Atmel maXTouch)故障排查与解决 硬件平台:ATK-DLRK3568(Rockchip RK3568) 触摸芯片:Atmel maXTouch(型号系列,Family: 164, Variant: 54) 驱动&#xff1a…

2026/10/21 15:13:48 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/2 0:25:10 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/2 10:46:11 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/2 10:47:40 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/2 1:16:51 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/2 10:46:31 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/2 10:46:26 阅读更多 →

日新闻

周新闻

月新闻