尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Java并发编程进阶:进程线程、锁机制与线程池实战解析
1. 为什么把进程线程并行并发放在一起学我见过太多人在多线程这条路上栽跟头问题往往出在基础概念混淆上。比如面试被问进程和线程的区别张嘴就是进程是资源分配的最小单位线程是CPU调度的最小单位听着像是背过书但一问并行和并发到底差在哪立刻就含糊了。JavaCore-进程-线程-并行并发-多线程这套内容其实就是把这些最容易被绕晕的概念用代码和案例彻底掰扯清楚。先明确一个定位这不是给完全没写过Java的人看的入门读物更适合已经能写CRUD、想往并发方向进阶的同学。你不需要背下所有API但需要建立一套清晰的并发脑图——知道什么时候该用锁、什么时候该用线程池、什么时候要考虑通信。学完这套东西回头再看那些多线程安全高并发优化的帖子基本都能看懂了。这套知识解决的核心痛点很实在业务系统一旦涉及定时任务、批量处理、消息推送、异步调用就躲不开多线程。而多线程写不好轻则数据错乱重则死锁卡死整个服务。JavaCore里这块内容从线程的创建方式讲到锁机制再到线程通信正好是一条完整的从会用到底层原理的进阶路径。2. 先分清几个要命的概念进程、线程、并行、并发2.1 进程和线程的真正区别举一个生活化的例子你打开微信这是一个进程微信里同时处理文字聊天、接收图片、播放语音消息这些就是线程。进程是操作系统分配内存、文件句柄等资源的基本单位线程是进程内部的执行单元共享进程的资源。但有个细节很多人忽略线程共享进程的内存空间所以线程之间通信成本低可也正因为共享才会出现多线程同时改一个变量导致数据错乱的问题。进程之间内存隔离通信要走管道、消息队列、共享内存这类IPC机制成本高但安全。Java里写多线程不需要直接操作进程JVM帮我们屏蔽了操作系统进程的差异但这不代表你不需要理解进程——遇到线上CPU飙升、内存泄漏时你得会用系统命令看进程、看线程才能定位问题。2.2 并行与并发理解这两张图的差异简单说并发是看起来同时做实际是多个任务快速地交替执行并行是真正同时做需要多核CPU支持。单核CPU时代所谓的多线程其实是并发——操作系统用时间片轮转让每个线程轮流用CPU因为切换速度快用户感知不到。多核CPU出现后并行才成为可能两个线程分别跑在两个核心上互不干扰。有一个经典比喻一个人同时吃馒头和喝粥这是并发——一会儿咬一口馒头一会儿喝一口粥两个人一个吃馒头一个喝粥这是并行。并发关注的是任务调度和资源竞争并行关注的是物理上的同时执行。Java中Runtime.getRuntime().availableProcessors()可以查看CPU核心数如果你的多线程程序在双核机器上跑出了四倍速那大概率是逻辑设计有bug而不是并行带来的提升。2.3 为什么概念不清会写出Bug我最常看到的一个反例有人在单核服务器上测试多线程程序发现性能并没有提升于是得出多线程没用的结论。实际上单核CPU上多线程的主要价值不是提速而是提高资源利用率——比如一个线程在等数据库IO时另一个线程可以去处理CPU计算。只有当任务确实能拆分成独立片段、且运行在多个核心上时并行才能真正缩短总耗时。理解这些区别后你才能做出合理的技术选型IO密集型任务多线程效果明显因为线程等待IO时可以让出CPU计算密集型任务线程数一般设置为CPU核心数1多了反而因为上下文切换损耗性能。概念不通的人一上来就开两百个线程处理计算任务结果CPU全耗在线程切换上程序慢得像爬。3. Java多线程的创建方式从继承Thread到使用线程池3.1 三种基本创建方式回顾Java中创建线程有四种经典方式教科书里常写三种但实际开发中最常用的是第四种继承Thread类重写run()方法实现Runnable接口传给Thread构造器实现Callable接口配合FutureTask获取返回值使用ExecutorService线程池第一种方式早该淘汰了因为Java是单继承一旦继承Thread就再没法继承其他类而且Thread类本身是一个重量级对象直接创建线程的开销远大于复用线程池中的线程。第二种方式是最基础的只关注任务本身不关注线程管理。第三种方式能拿到线程执行结果适合需要返回值的任务但要配合FutureTask代码稍显啰嗦。第四种是生产环境的标配。原因很直接线程的创建和销毁是有成本的假设一个任务平均执行10ms而线程创建需要5ms那么直接创建线程的损耗占比高达33%。线程池预期创建一定数量的线程任务来了直接交给池中的线程执行用完归还避免反复创建销毁的开销。3.2 实际写代码时用哪种一个具体的例子说明第三种和第四种的对比。需要并发查询三个商品的库存并汇总// 使用Callable FutureTask CallableInteger task1 () - queryStock(sku001); FutureTaskInteger ft1 new FutureTask(task1); new Thread(ft1).start(); // 使用线程池 ExecutorService pool Executors.newFixedThreadPool(3); FutureInteger f1 pool.submit(() - queryStock(sku001)); FutureInteger f2 pool.submit(() - queryStock(sku002)); FutureInteger f3 pool.submit(() - queryStock(sku003)); int total f1.get() f2.get() f3.get(); pool.shutdown();第二种写法的好处是线程复用不需要手动管理线程生命周期。用new Thread()创建线程的方式在真实项目里基本看不到了因为你无法控制线程数量一但任务量大内存中会堆积成千上万个线程最终OOM。注意Executors.newFixedThreadPool()和newCachedThreadPool()在某些场景有隐患——前者任务队列无界后者最大线程数为Integer.MAX_VALUE都可能导致资源耗尽。生产环境推荐手动创建ThreadPoolExecutor明确核心线程数、最大线程数、队列容量、拒绝策略。3.3 创建一个规范线程池的参考配置一个常见的配置方案核心线程数设为CPU核心数的2倍IO密集型或核心数1计算密集型最大线程数设为核心线程数的2倍队列用有界ArrayBlockingQueue拒绝策略用CallerRunsPolicy——任务被拒绝时由调用线程自己执行避免任务无声丢失。int core Runtime.getRuntime().availableProcessors() * 2; ThreadPoolExecutor executor new ThreadPoolExecutor( core, core * 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(100), new ThreadFactory() { private final AtomicInteger count new AtomicInteger(1); Override public Thread newThread(Runnable r) { return new Thread(r, biz-thread- count.getAndIncrement()); } }, new ThreadPoolExecutor.CallerRunsPolicy() );这里手动设置线程名非常关键。线上排查问题时jstack输出的线程名能帮你迅速定位是哪个任务的线程卡住了。如果全部叫pool-1-thread-1排查效率会大打折扣。4. 线程优先级别迷信但有讲究4.1 优先级本质和跨平台问题Thread类提供了setPriority(int)方法范围是1到10默认是5。Java语法上允许你设置优先级但操作系统不一定会严格按这个值调度。不同平台对优先级的映射策略不同Windows上Java的10级映射到系统优先级后实际效果还算明显Linux上则可能被忽略或仅作参考。所以优先级只能作为软性建议绝不能依赖它来保证某个线程一定先执行。我在实际项目中见过有人把业务线程设为最高优先级结果该线程疯狂抢占CPU其他线程饿死最终整个服务响应变慢。4.2 什么场景值得调优先级有一个比较实用的场景守护线程执行一些后台清理任务比如定期清理内存中的过期缓存这种任务没必要跟用户请求线程抢CPU可以把优先级调低比如setPriority(Thread.MIN_PRIORITY)。另外要注意Thread.yield()和优先级是两个不同的东西。yield()只是让出当前CPU时间片线程仍然可调度优先级影响的是调度器在选择下一个线程时的偏好。实际开发中我很少主动调优先级更多是保持默认把精力放在锁和线程池配置上。5. 锁机制是并发编程的重头戏5.1 synchronized 的三种用法synchronized是Java内置锁用法有三种修饰实例方法、修饰静态方法、修饰代码块。修饰实例方法时锁对象是this修饰静态方法时锁对象是Class对象修饰代码块时可以自己指定任意对象作为锁。很多人搞混的问题是两个线程分别调用两个不同对象上的同步实例方法锁的是不同对象所以不会互斥。如果你想让同一类的所有实例都互斥应该用static synchronized。synchronized基于对象监视器Monitor实现底层与操作系统的互斥量有关重量级锁在线程竞争激烈时会涉及用户态和内核态切换成本较高。JDK 1.6之后引入了偏向锁、轻量级锁、重量级锁的升级过程减少了无竞争场景下的锁开销。这时的synchronized在大多数场景下性能并不比ReentrantLock差。5.2 Lock接口与ReentrantLock的关键方法ReentrantLock提供了synchronized没有的灵活能力lock()与unlock()必须成对出现一般放在try-finally中tryLock()可以非阻塞地尝试获取锁失败后返回false避免线程无限阻塞lockInterruptibly()允许在等待锁过程中响应中断支持公平锁和非公平锁构造时传true即为公平锁什么情况下用ReentrantLock呢当需要尝试获取锁拿不到就做其他事的时候。比如一个任务要更新用户余额另一个任务也在更新同一用户你可以用tryLock避免等待直接告诉调用方当前用户正在操作中请稍后重试。另外ReentrantLock能实现synchronized做不到的锁的多个条件等待这就引出了线程通信。5.3 锁的底层状态与可见性这里必须强调一个核心原理锁不仅仅是互斥它还能保证内存可见性。线程A在释放锁之前对共享变量的修改线程B在获得同一把锁之后是可见的。否则就算避免了多个线程同时修改数据也可能出现线程B读到的还是主存中的旧值。Java内存模型JMM规定所有变量存在主内存中线程工作内存保存变量副本。synchronized的加锁和释放会触发工作内存与主内存的同步。所以多线程访问共享变量时光靠volatile有时也不够必须结合锁来保证复合操作的原子性。举个例子private int count 0; public void increment() { synchronized (this) { count; } }count不是一个原子操作它包含读取-修改-写回三步。如果不加锁两个线程同时读到count5各自加1写回最终结果可能是6而不是7。锁的作用是让这段代码在同一时刻只有一个线程执行从而保证了结果正确。6. 线程通信的三种常见方式6.1 wait/notify/notifyAll 的注意事项线程通信最基础的方法是Object类的wait()、notify()、notifyAll()配合synchronized使用。关键点wait()必须在持有锁的代码块中调用否则抛IllegalMonitorStateException。调用wait()后线程释放锁并进入等待队列notify()唤醒一个等待线程notifyAll()唤醒所有等待线程。被唤醒的线程重新参与锁竞争获得锁后从wait()之后继续执行。一个经典的生产者消费者场景synchronized (queue) { while (queue.size() MAX_SIZE) { queue.wait(); } queue.add(item); queue.notifyAll(); }注意这里用while而不是if来检查条件。原因是被唤醒的线程不一定能立刻重新获得锁在等待期间其他线程可能已经把队列填满所以需要重新检查条件。用if会导致虚假唤醒下的越界或重复消费。6.2 Condition 实现更精确的等待通知ReentrantLock搭配Condition可以解决wait/notify无法指定唤醒某一类线程的问题。一个典型的场景队列满时生产者等待队列空时消费者等待。如果用notifyAll生产者唤醒的所有线程里可能包含生产者它们醒来发现还是满的又回去等待白白浪费一次调度。使用两个Condition可以做到精确通知ReentrantLock lock new ReentrantLock(); Condition notFull lock.newCondition(); Condition notEmpty lock.newCondition(); // 生产者 lock.lock(); try { while (queue.size() MAX_SIZE) { notFull.await(); } queue.add(newItem); notEmpty.signal(); } finally { lock.unlock(); } // 消费者 lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); } Item item queue.remove(); notFull.signal(); } finally { lock.unlock(); }这样一来生产者只等待notFull消费者只等待notEmpty唤醒时也只唤醒对方阵营的线程效率会高很多代码意图也更清晰。6.3 阻塞队列开发中最推荐的通信方式实际开发中手写wait/notify或Condition的场景非常少因为java.util.concurrent包提供了ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等阻塞队列它们内部已经封装好了锁和等待通知逻辑。生产者消费者只靠一个BlockingQueue就能实现BlockingQueueTask queue new ArrayBlockingQueue(100); // 生产者 queue.put(task); // 队列满时阻塞 // 消费者 Task task queue.take(); // 队列空时阻塞put和take方法在队列满或空时会阻塞线程这样天然实现了流量控制。使用阻塞队列比自己手写锁加wait/notify更安全因为减少了出错面。我们常见的线程池任务队列底层就是BlockingQueue。7. 常见问题与排查技巧实录7.1 线程死锁的产生与预防死锁的经典场景是两个线程持有一把锁的同时还想获得对方的锁。例如// 线程1 synchronized (lockA) { Thread.sleep(100); synchronized (lockB) { // 业务 } } // 线程2 synchronized (lockB) { Thread.sleep(100); synchronized (lockA) { // 业务 } }线程1持有lockA等待lockB线程2持有lockB等待lockA互相等待谁也拿不到对方手里的锁程序卡死。排查死锁的办法线上出问题时用jps找到进程ID再用jstack pid打印线程堆栈。如果存在死锁jstack末尾会直接提示Found one Java-level deadlock并列出涉及的两个线程和锁对象。看到这个信息后再结合代码定位。预防死锁的思路有三条按固定顺序获取锁让所有线程都先拿A再拿B就不会出现循环等待使用tryLock并设置超时获取不到锁就释放已有锁放弃本次操作避免无限等待避免在持有锁时调用外部接口因为外部接口耗时不可控容易放大锁的持有时间7.2 线程安全集合的使用误区很多人知道HashMap线程不安全就换成Hashtable但Hashtable把所有方法都用synchronized锁住并发度极低读操作也要竞争同一把锁。更好的选择是ConcurrentHashMap——它采用了分段锁或CAS加锁的机制读操作几乎不加锁写操作只锁住特定节点性能远高于Hashtable。还有一个常见误区以为用了CopyOnWriteArrayList就万无一失。这个集合读多写少的场景下很合适因为读操作直接读原数组写操作复制新数组再替换引用。但如果频繁写入每次写都要复制整个数组内存开销极大。所以线程安全不等于性能好得看具体场景。7.3 线程池使用不当导致的任务丢失有次我在模拟项目里遇到一个奇怪现象某个定时任务偶尔不执行日志里也没有异常。后来发现用的是newFixedThreadPool(1)任务队列无界。当某个任务执行时间过长卡住时后面的任务全部排队等候看起来就像丢失了。解决思路给线程池的任务加上超时控制或者用ScheduledThreadPoolExecutor配合Future.get(timeout)确保单个任务不会无限阻塞。还有线程池拒绝策略默认是AbortPolicy直接抛异常如果你没捕获任务就静默失败了。生产环境我建议用CallerRunsPolicy至少让提交任务的线程自己兜底执行保留任务不丢。7.4 volatile 容易误解的地方volatile保证了可见性但不保证原子性。一个典型的反例volatile int count 0; // 多线程执行 count多个线程同时读取count各自加1再写回仍然会发生丢失更新。因为count是读改写三步volatile管不了这个。要保证原子性得用AtomicInteger、synchronized或ReentrantLock。但volatile在一写多读的开关场景很常用volatile boolean running true; // 线程A while (running) { // 处理 } // 线程B running false;没有volatile时线程A的工作内存里可能一直缓存着runningtrue线程B改了主内存的值线程A也感知不到导致无法退出循环。加上volatile线程A每次读都会去主内存刷新才能及时停止。8. 多线程调优的实战心得8.1 线程数配比不是拍脑袋定的IO密集型任务和CPU密集型任务的线程数配置逻辑完全不同。IO密集型比如HTTP请求、数据库查询线程大部分时间在等待IO此时可以开较多的线程来充分利用CPU等待间隙。网上常见的公式是IO密集型线程数 CPU核心数 * 2 CPU密集型线程数 CPU核心数 1但这个公式只是起点真实环境最好用压测来验证。我在处理一个批量数据同步需求时最初按公式配置了16个线程但下游数据库连接池最大只有10个连接导致线程大量等待获取连接。后来把线程数调成与数据库连接池一致整体吞吐反而提升了。线程数不是越多越好需要考虑下游资源的整体链路。8.2 上下文切换是隐形杀手为什么线程太多性能反而下降因为每个线程在CPU上执行一段时间后操作系统要把它的运行状态保存下来再加载另一个线程的状态这个保存和加载就是上下文切换。切换是需要消耗CPU时间的。可以用thread dump分析线程状态或用系统工具统计上下文切换次数。如果发现切换次数极高而且线程大多是RUNNABLE但执行进展缓慢就该考虑减少线程数或者把任务拆分得更合理减少无意义的竞争。8.3 捕获多线程异常的正确姿势线程里的异常不会像普通方法那样直接抛给调用方。如果你在run()里没有捕获异常会传给线程的UncaughtExceptionHandler默认处理是打印堆栈然后终止线程。如果业务要求某个任务失败后不能影响其他任务就需要在每个任务内部自己捕获异常或者给线程池设置ThreadFactory时指定自定义UncaughtExceptionHandler。还有一个容易被坑的地方ExecutorService.submit()如果返回Future任务内部抛出的异常会被封装到Future.get()时再抛出。如果你提交任务后从不调用get()异常就被静默吞掉了。我自己就遇到过一个定时任务用submit提交异常信息丢了排查了很久才发现是数据库连接池配置错误。从那以后凡是用submit我一定会获取Future并调用get()或者在任务内部把异常记录到日志。多线程之所以难难在问题不容易稳定复现往往是概率性触发。但只要理解了锁、原子性、可见性、通信这几根支柱大部分问题都能在写代码时提前规避。这套JavaCore知识越早吃透后面剖析框架源码、排查线上故障就越是举重若轻。至少我在面对高并发场景时靠的就是这些基础一点点把问题抠出来的。
RELATED

相关推荐

Kafka接入AI实战:从Lag排查到Function Calling自动化运维

Kafka接入AI实战:从Lag排查到Function Calling自动化运维

1. 为什么要把Kafka接进AI:这个标题背后要解决的真实问题先聊点实在的。做Kafka运维和研发的朋友,应该都有过这种经历:半夜被ConsumerGroup的Lag告警叫起来,打开电脑连上跳板机,一条一条查kafka-consumer-groups.sh --…

📅 2026/10/17 18:40:16
harness 工程化实践:用 AGENTS.md 与 Makefile 把 TaoToken 接入 CI 流水线

harness 工程化实践:用 AGENTS.md 与 Makefile 把 TaoToken 接入 CI 流水线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/17 18:40:16
大模型推理-基于prefill和decode阶段特性,PD分离,势在必行!TaoToken统一Key下的PD分离部署大纲

大模型推理-基于prefill和decode阶段特性,PD分离,势在必行!TaoToken统一Key下的PD分离部署大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/17 18:40:16
MORE NEWS

更多资讯

📰

实时换脸技术实战:Live Face Swap 2.0原理与性能优化解析

市面上换脸工具不少,但大多逃不过两个痛点:要么实时预览卡成幻灯片,要么离线渲染出来的脸像贴了张面具。前者让你根本没法对着摄像头互动,后者则让成品只适合自娱自乐,根本拿不出手。所以当我拿到这套内部代号为“Live…

📰

Linux下JDK多版本标准化部署与环境变量精准控制

1. 项目概述:为什么Linux下装JDK不是“点下一步”那么简单 你刚在某云服务器上配好CentOS或Ubuntu,想跑个Java程序, java -version 一敲——“command not found”。网上搜“Linux安装JDK”,前几条全是复制粘贴的三行命令&#…

📰

PHP反序列化漏洞深度解析:从魔术方法到POP链利用与防御

1. 从一个限制登录框开始讲起搞渗透测试这些年,见过太多开发者在写登录功能时踩同一个坑:用户输入看起来正常,参数名也简单,后端逻辑也就那么几行代码,结果就是被一条精心构造的字符串直接打穿内网。今天要聊的unseria…

📰

x86汇编中PROC、LOCAL与INVOKE的栈帧构建原理

1. 项目概述:为什么今天还要啃汇编里的过程与伪指令?“汇编语言-014”这个标题看起来像某套教学视频的第十四讲,但真正踩进去你会发现,它不是知识点的简单罗列,而是一道分水岭——跨过去,你写的就不再是“能…

📰

国密HTTPS零改造接入:Nginx+uWSGI+PyCryptodome实践

1. 方案背景与整体设计思路拆解1.1 等保测评里“传输安全”到底卡在哪这几年凡是要过等保三级测评的Web系统,几乎都会遇到同一个问题:测评项里对通信传输的机密性和完整性有明确要求,检查人员会实际抓包看协议、看证书、看加密套件。过去只开…

📰

Cursor 使用方法:把 Base URL 改到 TaoToken 的配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬