线程
- 引言
-
- 线程与进程的基本概念
- Java中的进程与线程实现
- 多线程同步
- 多线程通信机制
-
- 线程通信
-
- [wait/notify 机制](#wait/notify 机制)
- 生产者-消费者模型(阻塞队列前置知识)
- `BlockingQueue`队列
- 对比差异
- 线程安全集合
- Java线程池与并发工具
- [进程 vs. 线程 vs. 混合模式的选择逻辑](#进程 vs. 线程 vs. 混合模式的选择逻辑)
- 并发编程的常见陷阱与调优
- 性能监控工具链(Java生态)
引言
经过长久一段时间的沉淀,我将用我的知识体系带大家深入浅出Java进阶之路,今天我们的学习内容是线程与进程(怕大家搞混,所以先区分再深入了解线程)
欢迎大家来指正!!!
话说既然都学到这里了,想必是对Java有了一定的语法基础,那我们话不多说,开始学习!
首先我们得知道啥是线程和进程,毕竟一切知识都要从概念说起~
线程与进程的基本概念
进程的定义与特点
- 操作系统资源分配的基本单位,拥有
独立内存空间
线程的定义与特点
- 线程则是进程内的执行单元,
共享进程资源,但拥有独立的执行栈和程序计数器。是CPU调度的基本单位,共享进程资源,轻量级执行流
对比差异
- 资源占用、创建开销、通信方式、稳定性
说了这么多,你可能还是不懂,我来给你举个例子,就好比你打开了微信,微信程序在运行,这,就是一个进程,而微信里面的接收消息,传输文件则是一个又一个的线程,你接收消息的时候并不影响你发文件,这,就是并发性。这些线程它们属于同一个微信进程,共享部门的资源(内存里的聊天记录、用户信息等),但各自执行独立的任务。而进程就是微信是微信程序运行,QQ是QQ程序运行,他们互不打扰是独立的~
好的,那我们来用代码创建线程来了解一下吧~
Java中的进程与线程实现
要点:
- 进程的创建与管理:通过
Runtime.exec()或ProcessBuilder启动外部进程- 线程的创建方式:继承
Thread类与实现Runnable接口- 线程的生命周期:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、终止(Terminated)
进程的创建与管理
通过Runtime.exec()或ProcessBuilder启动外部进程
进程创建代码:
javapublic static void main(String[] args) throws Exception { // 启动记事本进程(Windows系统) Process process = Runtime.getRuntime().exec("notepad.exe"); Thread.sleep(3000); // 等待3秒 process.destroy(); // 关闭进程 } }此代码启动系统记事本,3秒后强制关闭。进程独立运行,与Java程序无直接内存共享。
线程的创建方式
继承Thread类与实现Runnable接口
线程创建代码:
java@Override public void run() { System.out.println("线程运行: " + Thread.currentThread().getName()); } } public class ThreadExample { public static void main(String[] args) { // 方式1:继承Thread类 MyThread thread1 = new MyThread(); thread1.start(); // 方式2:实现Runnable接口 Thread thread2 = new Thread(() -> { System.out.println("Lambda线程: " + Thread.currentThread().getName()); }); thread2.start(); } }线程共享进程内存,输出会显示不同线程名称(如Thread-0和Thread-1)。
线程的生命周期
新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、终止(Terminated)
创建声明周期的代码:
java@Override public void run() { // 运行状态(Running):CPU调度后执行run() System.out.println("线程 " + Thread.currentThread().getName() + " 进入运行状态"); try { // 阻塞状态(Blocked):调用sleep(),线程进入等待(TIMED_WAITING) System.out.println("线程 " + Thread.currentThread().getName() + " 进入阻塞状态(sleep 2秒)"); Thread.sleep(2000); // 模拟阻塞(等待2秒) } catch (InterruptedException e) { e.printStackTrace(); } // 阻塞结束后,回到运行状态 System.out.println("线程 " + Thread.currentThread().getName() + " 阻塞结束,继续运行"); } } public class ThreadLifeCycleDemo { public static void main(String[] args) throws InterruptedException { // 1. 新建状态(New):创建线程对象,未启动 MyThread thread = new MyThread(); System.out.println("线程状态(新建):" + thread.getState()); // 输出:NEW // 2. 就绪状态(Runnable):调用start(),线程进入就绪队列 thread.start(); System.out.println("线程状态(就绪):" + thread.getState()); // 输出:RUNNABLE(就绪/运行中) // 主线程休眠1秒,确保子线程进入运行状态 Thread.sleep(1000); System.out.println("线程状态(运行中):" + thread.getState()); // 输出:RUNNABLE(运行中) // 主线程休眠3秒,等待子线程进入阻塞状态(sleep期间) Thread.sleep(3000); System.out.println("线程状态(阻塞中):" + thread.getState()); // 输出:TIMED_WAITING(阻塞的一种) // 等待子线程执行完毕(终止状态) thread.join(); // 主线程等待子线程终止 System.out.println("线程状态(终止):" + thread.getState()); // 输出:TERMINATED } }
多线程同步
要点:
- 同步问题:竞态条件、数据不一致性
- 同步方法:
synchronized关键字、ReentrantLock锁
同步问题
我们知道了线程的创建,也知道了线程是实现高并发的手段。然而,高并发不仅仅意味着'多',更意味着'乱'。当多个线程同时访问共享数据时,如果不加以控制,就会出现竞态条件,导致数据不一致性(例如著名的银行转账或售票超卖问题)。
那么啥是竞争条件啊?其实就是当多个线程同时修改同一个变量(例如抢票、扣款),最终的结果取决于线程执行的先后顺序。
再给大家细致的讲一下这里为什么线程之间的变量修改对彼此不可见,其实是因为工作内存和主存的差异~
那么啥是主内存和工作内存呢?
工作内存与主内存的概念
在Java多线程中,每个线程拥有独立的工作内存(Working Memory),它是线程私有的数据区域,存储了该线程使用到的变量的副本。主内存(MainMemory)则是所有线程共享的内存区域,存储了变量的原始值。
工作内存类似于CPU缓存,用于加速线程执行效率。线程对变量的所有操作(读取、修改等)都必须在工作内存中进行,不能直接操作主内存的变量。
因此其根本原因就在如下所说:
变量不可见的根本原因
当线程A修改了变量X的值时,这个修改仅发生在A的工作内存中,不会立即同步到主内存。此时如果线程B需要读取X的值,它可能从主内存获取到旧值(因为A的修改尚未同步),或者从自己的工作内存中读取缓存值。
这种延迟或不同步导致线程间的修改对彼此不可见。即使线程A已经修改了变量,线程B可能仍然看到修改前的值。
JMM(Java内存模型)的规定 Java内存模型规定了线程如何与主内存交互:
- 读取操作:线程从主内存读取变量到工作内存
- 写入操作:线程将工作内存中的变量值刷新到主内存
- 如果没有适当的同步机制,这些操作的发生时机是不确定的,从而导致可见性问题。
同步方法
为了解决这些问题,Java 引入了同步机制。最基础的是使用 synchronized 关键字进行加锁,确保同一时间只有一个线程能进入临界区;进阶则使用 ReentrantLock,提供更灵活的锁操作。
内置锁(synchronized)
Java的
synchronized关键字是最基础的线程同步机制,通过修饰方法或代码块实现互斥访问。当线程进入同步块时自动获取锁,退出时释放锁,确保操作的原子性和可见性。由于锁的获取和释放由JVM管理,使用简单但灵活性较低。
javasynchronized(lockObject) { // 临界区代码 }
显式锁(ReentrantLock)
ReentrantLock是JDK 1.5引入的显式锁实现,提供比synchronized更丰富的功能:
- 尝试锁 :通过
tryLock()避免死锁或长时间等待。- 公平锁:通过构造函数指定公平策略,减少线程饥饿。
- 条件变量 :支持
Condition实现精细化的线程等待/唤醒机制。
javaLock lock = new ReentrantLock(); lock.lock(); try { // 临界区代码 } finally { lock.unlock(); }
volatile关键字
volatile确保变量的修改对所有线程立即可见,适用于单一变量的原子操作场景。它通过禁止指令重排序和强制读写主内存实现可见性,但不保证复合操作的原子性。
javavolatile int counter = 0;
final变量
final修饰的变量在对象正确构造后对其他线程可见,无需额外同步。JMM(Java内存模型)保证final字段的初始化安全,适合不可变共享数据。
javafinal String immutableData = "Initialized";
同步方法选择建议
- 简单场景 :优先使用
synchronized,代码简洁且不易出错。 - 复杂需求 :选择
ReentrantLock,利用其超时、公平锁等高级特性。 - 单一变量可见性 :
volatile是轻量级解决方案。 - 不可变数据 :
final变量无需同步即可安全共享。
多线程通信机制
要点
- 线程通信:
wait()/notify()机制、BlockingQueue队列- 线程安全集合:
ConcurrentHashMap、CopyOnWriteArrayList
线程通信
但是,同步解决了"争抢"的问题,但有时候线程之间不仅不抢,还需要合作。例如生产者生产满了需要通知消费者消费,消费者消费空了需要通知生产者生产。这就涉及到了线程通信~
通常我们的通信手段有两种:
wait/notify 机制: 基于 synchronized 的底层等待唤醒机制,让线程在对象上等待或被唤醒。
BlockingQueue(阻塞队列): 更高级的工具,自动实现了生产者-消费者模式,当队列满时自动阻塞生产者,空时自动阻塞消费者。
接下来,我们具体聊聊这两种方法~
wait/notify 机制
wait/notify 是 Java 中基于对象监视器(monitor)的线程间通信机制,依赖于 synchronized 关键字实现同步。核心方法包括 wait()、notify() 和 notifyAll()。
关键点:
wait():释放当前对象的锁,线程进入等待状态,直到其他线程调用notify()或notifyAll()。notify():随机唤醒一个在该对象上等待的线程。notifyAll():唤醒所有在该对象上等待的线程。- 调用这些方法前必须持有对象的锁(即必须在
synchronized块内使用)。
示例代码:
java
synchronized (lock) {
while (conditionNotMet) {
lock.wait(); // 释放锁并等待
}
// 执行任务
}
synchronized (lock) {
// 改变条件
lock.notify(); // 唤醒一个等待线程
}
注意事项:
- 使用
while而非if检查条件,避免虚假唤醒(spurious wakeup)。 notify()和notifyAll()需根据场景选择,避免死锁或性能问题。
生产者-消费者模型(阻塞队列前置知识)
在学习阻塞队列之前,我们先要了解多线程领域最经典的设计模式------生产者-消费者模型(Producer-Consumer Problem)。
那么什么是生产者-消费者模型?
想象一个快餐店:
生产者:负责做汉堡的厨师。
消费者:负责吃汉堡的顾客。
交易场所:传送带(缓冲区)。
模型定义:生产者线程负责生成数据/任务并放入共享缓冲区,消费者线程从缓冲区取出数据进行处理。
为什么要用它?
解耦:生产者和消费者互不依赖,只依赖缓冲区。
平衡负载:生产快慢不影响消费,反之亦然。
提高吞吐量:通过并发协作,充分利用CPU资源。
那么我们怎么实现呢?
实现该模型需要三个关键角色:
共享缓冲区:通常是队列(Queue),作为生产者和消费者的中间媒介。
生产者线程:源源不断制造数据,调用入队方法。
消费者线程:不断从队列取数据处理,调用出队方法。
关键难点:如何处理队列满(生产者不能塞)和队列空(消费者不能取)的边界情况?这就引出了我们今天的主角------阻塞队列。
BlockingQueue队列
阻塞队列:模型的完美实现
BlockingQueue 是 Java 并发包(java.util.concurrent)中提供的接口,它天生就是为了生产者-消费者模型而生的。
核心原理:
当队列满时:生产者调用 put() 方法会被阻塞,直到消费者取走元素腾出空间。
当队列空时:消费者调用 take() 方法会被阻塞,直到生产者放入新元素。
常用实现类:
ArrayBlockingQueue:基于数组的有界阻塞队列(推荐,内存可控)。
LinkedBlockingQueue:基于链表的阻塞队列(可设界,性能通常更高)。
代码实战:
实现一个基于 ArrayBlockingQueue 的完整示例,模拟了生产者生产数字,消费者消费数字的场景。
java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class ProducerConsumerDemo {
public static void main(String[] args) {
// 1. 创建一个容量为 10 的阻塞队列
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);
// 2. 定义生产者线程
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
queue.put(i); // 如果队列满,此处会自动阻塞等待
System.out.println(" 生产了: " + i);
Thread.sleep(100); // 模拟生产耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 3. 定义消费者线程
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
Integer value = queue.take(); // 如果队列空,此处会自动阻塞等待
System.out.println(" 消费了: " + value);
Thread.sleep(150); // 模拟消费耗时(比生产慢)
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 4. 启动线程
producer.start();
consumer.start();
}
}
性能特点
- 有界队列:必须指定固定容量,无法动态扩容
- 低竞争场景优化:基于单锁(ReentrantLock)实现,适合生产者-消费者数量较少的场景
- 公平性可选:可通过构造函数选择公平锁策略(避免线程饥饿)
- 内存预分配:底层使用数组存储,内存连续性好
- 阻塞策略:队列满/空时自动阻塞线程(通过Condition机制实现)
以下是一个完整的 Java 示例,展示 LinkedBlockingQueue的生产者-消费者模型:
java
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LinkedBlockingQueueExample {
// 创建容量为3的有界队列
private static final BlockingQueue<String> queue = new LinkedBlockingQueue<>(3);
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
// 生产者线程
executor.submit(() -> {
try {
queue.put("Message 1");
queue.put("Message 2");
queue.put("Message 3");
System.out.println("生产者尝试插入第4条消息...");
queue.put("Message 4"); // 队列已满时会阻塞
System.out.println("生产者插入完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
executor.submit(() -> {
try {
Thread.sleep(1000); // 延迟消费
System.out.println("消费者取出: " + queue.take());
Thread.sleep(1000);
System.out.println("消费者取出: " + queue.take());
Thread.sleep(1000);
System.out.println("消费者取出: " + queue.take());
System.out.println("消费者取出: " + queue.take()); // 会阻塞直到有新元素
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
executor.shutdown();
}
}
性能特点
- 适合高并发场景,吞吐量通常优于
ArrayBlockingQueue- 默认无界(
Integer.MAX_VALUE),但建议创建时指定容量- 使用两把锁(putLock/takeLock)实现生产消费分离
对比差异
- 吞吐量 :
LinkedBlockingQueue在高并发下表现更好(双锁分离设计) - 内存占用 :
ArrayBlockingQueue预分配内存更节省空间(链表节点有额外开销) - 实时性 :
ArrayBlockingQueue的数组访问具有更好的局部性
在 BlockingQueue 中,不同方法处理边界情况的行为不同,请根据场景选择:
BlockingQueue 方法对比
| 方法 | 队列满/空时的行为 | 适用场景 |
|---|---|---|
put(E e) |
阻塞线程,直到队列有空闲空间 | 生产者-消费者模型(无条件等待) |
take() |
阻塞线程,直到队列中有元素可取 | 生产者-消费者模型(无条件等待) |
offer(e, timeout) |
尝试放入元素,超时后放弃并返回 false |
需要控制等待时间的生产场景 |
poll(timeout) |
尝试取出元素,超时后返回 null |
需要控制等待时间的消费场景 |
总结通过使用 BlockingQueue,我们不需要手动编写 synchronized、wait() 或 notify() 代码,就能轻松实现线程安全的生产者-消费者模型。它是解决多线程协作、任务调度问题的首选工具。
线程安全集合
最后,为了在高并发下获得更好的性能,我们通常会使用专门设计的线程安全集合,例如读写分离的 CopyOnWriteArrayList 和分段高效的 ConcurrentHashMap,它们比传统的同步包装集合性能更好。
线程安全集合的性能优势分析
传统同步包装集合(如Collections.synchronizedList)通过全局锁实现线程安全,任何操作都需要获取同一把锁。高并发场景下,锁竞争会导致线程频繁阻塞,性能急剧下降。
那么接下来我们看一下到底是怎么实现的线程安全~
线程安全两种的实现机制
CopyOnWriteArrayList 和 ConcurrentHashMap 的线程安全性体现在它们的设计和内部实现上,避免了传统同步包装集合(如 Collections.synchronizedList)的全局锁竞争问题。
CopyOnWriteArrayList 通过写时复制(Copy-On-Write)机制保证线程安全。每次修改操作(如 add、set)都会创建底层数组的新副本,修改在新副本上进行,而读操作直接在旧数组上执行。这种机制确保读操作无需加锁,写操作通过独占锁保证原子性。
ConcurrentHashMap 在 JDK 7 中采用分段锁(Segment),每个段独立加锁,不同段可并发操作;在 JDK 8 后改为 CAS + synchronized 优化,仅锁住哈希桶的头节点。这种细粒度锁大幅减少了锁竞争。
读写分离设计(CopyOnWriteArrayList)
CopyOnWriteArrayList采用写时复制机制:修改操作(add/set等)会复制整个底层数组,在副本上修改后替换原数组。读操作直接访问原数组,无需加锁。
- 无锁读取:读操作完全无锁,可并发执行
- 写操作隔离:写操作通过复制避免影响读操作,但修改成本较高
- 适用场景:读多写少(如事件监听器列表)
示例代码片段:
java
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("item"); // 写操作加锁并复制数组
String item = list.get(0); // 读操作无锁
分段锁技术(ConcurrentHashMap)
ConcurrentHashMap将数据分段存储(Java 8前使用Segment,Java 8改用桶节点锁),不同段/桶的修改操作可并行执行。
- 细粒度锁:仅锁定当前操作的段/桶,其他段仍可访问
- CAS优化 :Java 8对部分操作采用CAS(Compare-And-Swap)无锁算法
PS:CAS优化详解
CAS(Compare-And-Swap)是一种无锁编程技术,通过原子操作实现多线程环境下的数据同步。CAS操作包含三个参数:内存位置(V)、预期原值(A)和新值(B)。当且仅当V的值等于A时,处理器才会将V的值更新为B,否则不执行操作。CAS优化通过减少锁竞争提高并发性能,适用于高并发场景。
CAS的优势在于避免线程阻塞,减少上下文切换开销。但可能引发ABA问题(即值从A变为B又变回A,CAS无法感知中间变化),需通过版本号或标记位解决。现代CPU通常提供原子指令(如x86的CMPXCHG)实现CAS。 - 扩容优化:支持多线程协同扩容,避免全局阻塞
性能对比(假设16段):
- 传统HashMap同步包装:所有线程竞争同一把锁
- ConcurrentHashMap:最多16个线程可并行修改不同段
安全性的具体表现
无竞态条件
CopyOnWriteArrayList 的迭代器基于创建时的数组快照,即使其他线程修改集合也不会抛出 ConcurrentModificationException。ConcurrentHashMap 的读操作完全无锁,写操作仅锁局部节点。
内存一致性
两者的写操作对 volatile 变量的修改遵循 happens-before 原则,确保写结果对后续读操作可见。例如 CopyOnWriteArrayList 通过 volatile 数组引用保证线程间可见性。
原子性操作
ConcurrentHashMap 提供原子性复合操作如 putIfAbsent、compute,避免传统同步集合需要外部加锁的问题。例如:
java
map.compute(key, (k, v) -> v == null ? newValue : v + 1);
与传统同步集合的对比
锁粒度差异
Collections.synchronizedMap 使用对象级别的全局锁,所有操作串行化。ConcurrentHashMap 的锁粒度更细,并发度取决于哈希桶数量或分段数。
迭代器行为
同步集合的迭代器需要外部加锁,否则可能抛出并发修改异常。CopyOnWriteArrayList 和 ConcurrentHashMap 的迭代器是弱一致性的,允许在迭代期间修改集合。
潜在风险与限制
写性能代价
CopyOnWriteArrayList 的写操作需要复制整个数组,频繁修改时性能较差。ConcurrentHashMap 的扩容操作可能引发段锁竞争。
弱一致性迭代
迭代器可能无法反映最新的集合状态,业务逻辑需容忍这种延迟。例如遍历 ConcurrentHashMap 时可能错过刚添加的元素。
内存占用
CopyOnWriteArrayList 的写时复制机制会导致内存占用翻倍,大数据量时可能引发 GC 压力。
选择依据
| 集合类型 | 锁粒度 | 适用场景 |
|---|---|---|
Collections.synchronizedList |
全局锁 | 写操作为主的低频并发场景 |
CopyOnWriteArrayList |
无锁读/写锁 | 遍历操作远多于修改的场景 |
ConcurrentHashMap |
分段锁+CAS | 高频读写的中高并发场景 |
通过降低锁竞争范围或消除非必要锁,这些并发集合在保证线程安全的同时显著提升了吞吐量,但需要根据具体场景权衡选择。
Java线程池与并发工具
小结
- 线程池的优势:降低资源消耗、提高响应速度、统一管理
- 核心类:
ThreadPoolExecutor、Executors工厂类- 并发工具包:
CountDownLatch、CyclicBarrier、Semaphore
前一部分我们重点学习了线程安全集合(如 ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue 等)解决了多线程环境下共享数据的访问安全问题,确保了在高并发场景下数据的一致性与可见性,避免了竞态条件和数据错乱。
然而真正的并发编程远不止"数据安全"这一环。两个更根本的问题需要解决:
-
线程资源开销:线程本身是昂贵的资源,频繁创建和销毁线程会带来巨大的系统开销(CPU、内存、调度成本)。如果为每个任务都新建一个线程,系统很快就会不堪重负。
-
线程协作与同步:多个线程不仅需要安全地访问数据,还常常需要等待彼此、协同执行、控制并发节奏,比如"等所有任务都准备好了再一起开始"或"限制同时访问某个资源的线程数量"。
为了解决这两个核心问题,Java 并发包 java.util.concurrent 提供了两大支柱性工具:
- 线程池(ThreadPool):对线程进行统一管理与复用,实现资源的高效利用。
- 并发工具类(Concurrency Utilities):提供高级的线程协调机制,简化复杂同步逻辑的实现。
线程安全集合、线程池和并发工具类共同构成了 Java 高并发编程的"三驾马车",缺一不可。
那么我们先来了解一下线程池吧~
线程池
线程池是一种利用"池化"思想管理线程的技术,通过预先创建并维护一组可复用的线程,避免重复创建和销毁,极大提升系统性能。
线程池的核心优势
降低资源消耗:通过复用线程减少频繁创建和销毁的开销,避免不必要的上下文切换。
提高响应速度:任务到达时可直接分配空闲线程执行,无需等待线程初始化。
统一管理与控制:支持配置最大线程数、任务队列、拒绝策略等,增强系统稳定性。
线程池实现类
ThreadPoolExecutor
提供精细化的线程池配置能力,关键参数包括:
corePoolSize:核心线程数,即使空闲也不会被回收maximumPoolSize:最大线程数,超出核心数时临时创建的线程上限keepAliveTime:非核心线程的空闲存活时间workQueue:任务队列(如LinkedBlockingQueue)RejectedExecutionHandler:拒绝策略(如丢弃任务或抛出异常)
Executors工厂类
快速创建常用线程池,但需注意潜在风险:
newFixedThreadPool(n):固定大小线程池,使用无界队列,可能堆积任务导致OOMnewCachedThreadPool():弹性线程池,适合短时任务,但可能过度创建线程newSingleThreadExecutor():单线程串行执行,无界队列同样存在OOM风险newScheduledThreadPool(n):支持定时或周期性任务
生产环境推荐手动配置
ThreadPoolExecutor,明确线程和队列的边界。
并发工具类
CountDownLatch
- 作用:阻塞一个或多个线程,直到其他线程完成指定操作(计数器归零)。
- 方法:
countDown()减少计数,await()阻塞等待。 - 场景:主线程等待所有子线程初始化完成。
CyclicBarrier
- 作用:同步一组线程,所有线程到达屏障点后统一继续执行。
- 特点:计数器可重置,支持循环使用。
- 场景:分阶段并行计算,每阶段完成后汇总数据。
Semaphore
- 作用:通过许可证控制资源访问并发数。
- 方法:
acquire()获取许可,release()释放许可。 - 场景:限流(如API调用)、资源池管理(如数据库连接)。
协同应用示例
- 线程池+安全队列
ThreadPoolExecutor处理任务,配合BlockingQueue实现生产者-消费者模型。 - 任务同步
使用CountDownLatch等待多个异步任务完成后再执行后续逻辑。 - 资源限流
通过Semaphore限制并发访问外部接口的线程数量,避免过载。
总结
- 线程安全集合 :解决共享数据访问问题(如
ConcurrentHashMap)。 - 线程池:优化线程生命周期管理,提升资源利用率。
- 并发工具:协调线程执行顺序,实现复杂同步逻辑。
三类工具结合使用,可构建高效、稳定的高并发系统。
进程 vs. 线程 vs. 混合模式的选择逻辑
进程的核心优势与场景
高隔离性:进程拥有独立内存空间,崩溃不会影响其他进程。适用于需要强隔离的场景,如浏览器标签页渲染、执行不可信代码(插件系统)。
线程的核心优势与场景
高并发协作:线程共享内存,通信成本低。适用于I/O密集型任务(Web服务器)、需要频繁共享状态的场景(GUI程序的后台计算与界面渲染)。
混合模式的典型实现
多进程+多线程:结合隔离性与并发效率。例如Nginx/Redis采用主进程管理工作进程,工作进程内部可能使用多线程处理I/O;分布式计算中集群级多进程协作,单机级多线程优化。
并发编程的常见陷阱与调优
线程泄露与内存溢出
现象:系统逐渐变慢并抛出OutOfMemoryError。原因包括未正确销毁线程或线程持有对象引用阻碍GC。
解决方案:使用线程池(ThreadPool)管理线程生命周期,避免手动创建。
死锁的诊断与规避
现象:程序无响应,CPU占用率低。通常由多线程循环等待锁引发。
解决方案:
- 超时机制:通过
tryLock设置锁获取超时,超时后回退。 - 资源有序分配:强制线程按统一顺序获取锁(如先锁A再锁B),打破循环等待条件。
性能监控工具链(Java生态)
JConsole
基础监控:通过JMX连接JVM,实时查看内存、线程、类加载状态。线程面板可快速检测死锁。
VisualVM
进阶诊断:支持内存Dump分析、CPU采样(定位高耗时方法)。配合Visual GC插件可监控堆内存分区(新生代/老年代)变化。
Async Profiler
低开销生产级工具:基于Linux perf_events和JVM TI接口,生成火焰图精准定位热点(CPU瓶颈、锁竞争、内存分配问题)。
以上就是我们今天学习的内容了,希望大家认真理解,遇到不会的追根溯源直到找到不会的点,最后还是老样子,给大家分享下自己喜欢的一句话
最慢的步伐不是跬步,而是徘徊
最快的脚步不是冲刺,而是坚持