多线程:让你的 App 从“单行道”变成“立体交通枢纽”
把 Java 中的 多线程Multithreading想象成一家超大型 “智能中央厨房”里的“并行流水线作战系统”。你作为点外卖的用户软件使用者根本感受不到后厨有几个厨师、几口锅。你只在乎一件事我点完餐多久能吃到单线程单行道整个厨房只有 1 个厨师。他先做你的红烧肉做完才做隔壁桌的酸菜鱼。如果做红烧肉时发现没酱油了他得放下锅去采购全厨房都停摆。你用户看到的就是 App 卡死转圈圈什么按钮都点不动。多线程立体交通厨房里有 10 个厨师、5 个配菜员、3 个洗碗工所有人各司其职同时干活。你下单后A 厨师立刻开火做你的菜B 厨师同时在打包外送C 备菜员同时在切葱姜蒜。你感觉订单刚下手机就响了“您的餐已出锅”。多线程的核心作用就是让计算机的 CPU中央处理器这个“大厨”永不闲置同时处理多件事让你用户感觉软件永远“反应快、不卡顿、能同时干很多事”。一、多线程解决了哪些让你抓狂的痛点痛点 1界面冻结点哪哪死如果没有多线程你点“下载 1GB 的游戏安装包”时整个 App 画面会一动不动连“取消”按钮都点不了你以为手机死机了。多线程会派一个“下载专员”线程专门去下载主线程UI 界面继续陪你聊天、滚动页面。 你能边下载边刷评论爽不爽痛点 2一件事卡住全盘拖累你在淘宝付款时系统需要同时干三件事扣你余额、减库存、生成物流单。如果是单线程这三件事排队做任何一个环节慢比如银行接口卡 3 秒你就要等 3 秒才能看到“支付成功”。多线程让这三件事并行做你 1.5 秒就收到成功通知。痛点 3大量任务堆积活活饿死双 11 凌晨 0 点100 万用户同时点“提交订单”。单线程只处理 100 个/秒你排到第 9999 位得等 100 秒早就没库存了。多线程 线程池开启 500 个线程同时处理你 0.5 秒就收到“订单已锁定”让你感觉抢购毫不费力。二、多线程的五大“工种分类”类型及使用场景Java 里的多线程不是乱来的有 5 种典型的“线程角色”程序员根据你的操作场景选派不同工种目的只有一个让你用户的操作极速响应永不阻塞。类型 1继承 Thread 类 —— “专职单兵厨师”形态一个类直接继承 Thread重写 run() 方法。使用场景极简的独立后台任务且不需要返回结果比如定时清理临时文件、后台发心跳包。你的体验你在 App 里玩连连看每走一步后台都有一个“反作弊校验”线程在悄悄检查你的移动轨迹是否正常。你毫无感觉因为它是独立的专职兵绝不干扰你华丽的连击特效。// 专职单兵专门做日志归档的线程 class LogArchiveThread extends Thread { Override public void run() { while (true) { System.out.println(后台正在归档昨天的日志...你完全无感); try { Thread.sleep(60000); } catch (Exception e) {} // 每小时干一次 } } } // 系统启动时 new LogArchiveThread().start(); 它就在后台默默干活了类型 2实现 Runnable 接口 —— “灵活兼职小工”形态实现 Runnable 接口重写 run()然后丢给 Thread 去执行。使用场景任务本身与线程控制分离方便多个线程执行同一个任务比如多窗口卖票。你的体验你抢周杰伦演唱会门票App 告诉你“当前购票人数 2000 人”。系统背后开了 50 个 Runnable 线程同时处理这 2000 人的请求。你不会因为某个人付款慢而被活活等死因为每个线程都在独立处理不同的人。你感觉“抢票很公平不卡人”。// 兼职小工专门负责“减库存”这个动作可以被多个线程同时调用 class StockRunnable implements Runnable { private int stock 10; Override public void run() { if (stock 0) { System.out.println(Thread.currentThread().getName() 抢到一个库存剩余 (--stock)); } } } // 创建 5 个线程去执行这个任务模拟 5 个用户同时抢类型 3实现 Callable 接口 —— “带成绩单的工头”形态实现 Callable重写 call() 方法允许返回结果并配合 Future 或 FutureTask 获取返回值。使用场景需要计算结果的并行任务比如并发计算 12...1亿最后汇总结果。你的体验你在地图 App 里搜“从北京到上海的最佳路线”。后台同时派出 3 个 Callable 线程线程 1 算“高速优先”线程 2 算“省钱优先”线程 3 算“风景优先”。3 个线程同时算谁先出结果先展示谁。你感觉“路线推荐几乎是秒出的而且同时给了 3 条选择”。// 带成绩单的工头专门计算某一段路程的时间 class RouteCalculator implements CallableString { private String strategy; public RouteCalculator(String strategy) { this.strategy strategy; } Override public String call() throws Exception { // 模拟计算耗时 Thread.sleep(2000); return strategy 路线预计 5 小时 20 分; } } // 主线程同时派发 3 个任务用 Future 接收结果类型 4线程池ThreadPoolExecutor —— “全能作战总指挥部”最常用、最重要形态通过 Executors 或 ThreadPoolExecutor 创建固定数量、可缓存的线程池任务直接扔进去线程复用。使用场景高并发、任务量大、需要控制资源的业务比如 Web 服务器处理 HTTP 请求、批量发送短信。你的体验双 11 凌晨你疯狂刷新订单页。App 没有崩、没有卡。因为后端使用了线程池池子里有 200 个“快递员线程”专门处理“查询订单”这个请求。请求来了直接分配一个空闲线程处理完就回收绝不创建/销毁开销极大的线程。你感觉“再多人刷新App 都稳如老狗”。// 全能作战指挥部创建包含 10 个线程的池子 ExecutorService pool Executors.newFixedThreadPool(10); for (int i 0; i 100; i) { pool.execute(() - { System.out.println(Thread.currentThread().getName() 正在处理你的刷新请求...); // 模拟查库 }); } pool.shutdown();类型 5定时线程池ScheduledExecutorService —— “准点闹钟班”形态专门用来执行定时、周期性任务的线程池。使用场景定时刷新缓存、定时清理过期 Session、定时发送报表邮件。你的体验你每天早上 8 点打开学习 App发现首页的“每日推荐”已经更新了准时不差一秒。这是因为后台有一个定时线程每天凌晨 2 点准时去数据库抓取最新内容并刷新缓存。你打开时永远看到最新内容从不用手动拉取刷新。// 准点闹钟班每天凌晨 2 点执行清理任务 ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1); scheduler.scheduleAtFixedRate(() - { System.out.println(凌晨 2 点定时清理过期缓存你明早打开 App 时体验极速); }, 0, 24, TimeUnit.HOURS);三、一张表看清什么时候用哪种线程站在你的操作角度四、多线程的“翻车现场”你必须知道的隐患虽然多线程让你用户体验飞升但程序员如果玩不好你会遇到这些恶心场景数据错乱线程不安全你和女朋友同时用同一个账号修改收货地址结果系统把她的地址覆盖了你的你买的鞋子寄到了她家。因为两个线程同时写同一份数据导致混乱。解决方法加 synchronized同步锁。死锁互相等死线程 A 拿着“锅”等“铲子”线程 B 拿着“铲子”等“锅”两个互相不让。App 彻底卡死你只能杀进程重启。这是多线程最危险的事故。资源耗尽线程爆炸程序员没控制线程数量每来一个请求就 new Thread()来了 10 万个请求创建了 10 万个线程。手机/服务器内存爆掉App 直接闪退。解决方法必须用线程池。 终极一句话站在你的感受角度如果 CPU中央处理器是餐厅里那个无所不能的“超级主厨”那么多线程就是给他配的“N 个分身术”。没有多线程的软件 主厨一次只能做一道菜你做一道全餐厅的人饿着肚子排队等动不动还要放下锅去处理杂事你用户等到饭菜凉透、手机砸脸。有了多线程的软件 主厨分出 10 个分身一个炒菜、一个煲汤、一个蒸饭、一个处理投诉你用户刚点完单菜就嗖的一下上齐了。你感受到的所有“丝滑、秒开、多任务、不卡死”本质上都是多线程在底层把你一个人独占 CPU 的“特权”剥夺了换成所有用户共享 CPU 时间片但正因如此每个人反而都感觉自己在独占整个系统。这才是多线程最大的魔法。