大家好呀!👋 今天咱们来聊聊Java并发编程这个既让人兴奋又让人头疼的话题。作为一个写了10年代码的老司机🚗,我敢说并发编程绝对是Java中最难啃的骨头之一,但同时也是最有价值的部分!今天我就用最通俗易懂的方式,带你彻底搞懂Java并发编程的方方面面!
一、并发编程基础篇:从"小卖部抢购"说起
1.1 什么是并发?为什么需要并发?
想象一下学校小卖部中午卖限量版辣条🌶️的场景:如果所有同学都一窝蜂冲进去,那小卖部肯定乱成一锅粥🍲。聪明的校长想了个办法:让大家排队,每次只允许5个同学进去买。
这就是并发的基本思想!💡
并发(Concurrency):指在同一时间段内,多个任务交替执行(注意不是同时!)
并行(Parallelism):才是真正的多个任务同时执行(需要多核CPU支持)
为什么需要并发?🤔
- 提高程序响应速度(比如网页边加载图片边渲染文字)
- 充分利用多核CPU的计算能力
- 让耗时操作(如IO)不阻塞主线程
1.2 进程 vs 线程:家族企业里的分工
把操作系统比作一个大公司🏢:
- 进程:就像公司里独立的部门(财务部、技术部),有自己独立的办公室和资源
- 线程:就像部门里的员工👨💻👩💻,共享部门的资源,可以随时沟通
Java中我们主要操作线程。创建一个线程非常简单:
Thread myThread = new Thread(() -> {
System.out.println("我是新线程!");
});
myThread.start();
1.3 线程的生命周期:从出生到退休
一个线程的一生要经历这些阶段👶→👴:
- 新建(New):刚创建,还没调用start()
- 就绪(Runnable):调用了start(),等待CPU分配时间片
- 运行(Running):获得CPU时间片,正在执行
- 阻塞(Blocked):等待锁、IO操作等
- 终止(Terminated):执行完毕或被中断
可以用一张图表示:
新建 → 就绪 ↔ 运行 → 阻塞 → 就绪 → ... → 终止
二、线程安全篇:多线程的"共享零食"问题
2.1 可怕的竞态条件(Race Condition)
想象班级里有一个共享零食箱🍪,规则是:每次只能拿一块饼干。但如果有两个同学同时伸手…
class SnackBox {
private int cookies = 100;
public void takeCookie() {
if(cookies > 0) {
// 这里可能被其他线程打断!
cookies--;
System.out.println("拿走一块饼干,剩余:" + cookies);
}
}
}
当多个线程同时执行这段代码时,可能会出现:
- 线程A检查cookies=1
- 线程B也检查cookies=1
- 两个线程都执行cookies–,最终cookies=-1!😱
这就是竞态条件:结果依赖于线程执行的顺序。
2.2 解决之道:同步与锁🔒
Java提供了多种同步机制:
1. synchronized 关键字
public synchronized void takeCookie() {
// 现在一次只有一个线程能进入这个方法
if(cookies > 0) {
cookies--;
}
}
synchronized的三种用法:
- 修饰实例方法:锁住当前实例对象
- 修饰静态方法:锁住整个类
- 同步代码块:灵活控制锁的范围
2. volatile 关键字
保证变量的可见性(一个线程修改后,其他线程立即可见):
private volatile boolean running = true;
3. 原子类 (AtomicInteger等)
private AtomicInteger cookies = new AtomicInteger(100);
public void takeCookie() {
cookies.decrementAndGet(); // 原子操作
}
2.3 锁的深入:从厕所门到银行金库🚪
Java中的锁可以分为:
- 乐观锁:假设冲突少,先操作,有冲突再重试(如CAS)
- 悲观锁:假设冲突多,先加锁再操作(如synchronized)
- 可重入锁:同一个线程可以重复获取同一把锁(ReentrantLock)
- 读写锁:读共享,写独占(ReentrantReadWriteLock)
// 使用ReentrantLock的例子
private final Lock lock = new ReentrantLock();
public void takeCookie() {
lock.lock();
try {
if(cookies > 0) {
cookies--;
}
} finally {
lock.unlock(); // 一定要在finally中释放!
}
}
三、线程协作篇:等待与通知机制
3.1 wait() 和 notify() 的故事
想象一个外卖小哥和顾客的场景🍔:
- 顾客:点完餐后调用
wait()
进入等待状态 - 外卖小哥:送餐到达后调用
notify()
唤醒顾客
class FoodDelivery {
private boolean arrived = false;
public synchronized void waitForDelivery() throws InterruptedException {
while(!arrived) { // 要用while而不是if!
wait(); // 释放锁并等待
}
System.out.println("终于吃到外卖了!");
}
public synchronized void deliver() {
arrived = true;
notifyAll(); // 通知所有等待的顾客
}
}
⚠️ 注意:
- 必须在同步代码块内使用wait/notify
- 要用while循环检查条件,而不是if(防止虚假唤醒)
- 优先使用
notifyAll()
而不是notify()
3.2 更高级的协作工具:JDK并发工具类
Java并发包(java.util.concurrent)提供了更强大的工具:
1. CountDownLatch:多人赛跑发令枪🏃♂️
CountDownLatch startSignal = new CountDownLatch(1);
// 运动员线程
new Thread(() -> {
startSignal.await(); // 等待发令枪
System.out.println("开始跑步!");
}).start();
// 裁判线程
System.out.println("各就各位...");
Thread.sleep(2000);
startSignal.countDown(); // 发令枪响
2. CyclicBarrier:团队旅行集合点🧳
CyclicBarrier meetingPoint = new CyclicBarrier(3, () -> {
System.out.println("所有人都到齐了,出发!");
});
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 到达集合点");
meetingPoint.await(); // 等待其他人
}).start();
}
3. Semaphore:限量版商品抢购🎟️
Semaphore semaphore = new Semaphore(5); // 只有5个购买名额
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println("抢到限量版商品!");
Thread.sleep(2000);
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
四、线程池篇:管理线程的"人力资源部"
4.1 为什么要用线程池?
频繁创建销毁线程就像公司天天招聘又解雇员工:
- 招聘成本高(线程创建开销大)
- 管理混乱(系统资源耗尽)
线程池的优势:
- 降低资源消耗:复用已创建的线程
- 提高响应速度:任务到达时线程已存在
- 提高可管理性:统一分配、监控
4.2 Java中的线程池体系
Java通过Executor
框架提供线程池支持:
Executor ← ExecutorService ← AbstractExecutorService ← ThreadPoolExecutor
常用的工厂方法:
// 1. 固定大小的线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 2. 可缓存的线程池(自动扩容)
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 3. 单线程池(保证顺序执行)
ExecutorService singleThread = Executors.newSingleThreadExecutor();
// 4. 定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
4.3 线程池的7大核心参数
理解ThreadPoolExecutor
的构造参数非常重要:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(长期保留)
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
4.4 线程池的工作流程📊
- 提交任务时,优先创建核心线程
- 核心线程满了,任务进入工作队列
- 队列满了,创建非核心线程(不超过maximumPoolSize)
- 线程数达最大值且队列满,触发拒绝策略
4.5 四种拒绝策略
- AbortPolicy(默认):直接抛出RejectedExecutionException
- CallerRunsPolicy:让提交任务的线程自己执行
- DiscardPolicy:默默丢弃任务,不报错
- DiscardOldestPolicy:丢弃队列中最老的任务,然后重试
五、并发集合篇:线程安全的"储物柜"
Java提供了多种线程安全的集合类:
5.1 ConcurrentHashMap:高效的并发哈希表
ConcurrentHashMap map = new ConcurrentHashMap<>();
map.put("apple", 1);
map.computeIfAbsent("banana", k -> 2); // 原子操作
特点:
- 分段锁设计(Java 7)或CAS+synchronized(Java 8+)
- 高并发下性能远优于Hashtable
- 迭代器弱一致性(不抛出ConcurrentModificationException)
5.2 CopyOnWriteArrayList:写时复制的列表
适合读多写少的场景:
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
list.add("Java");
list.get(0); // 读取不需要加锁
原理:
- 每次修改时复制整个底层数组
- 读操作完全无锁
5.3 BlockingQueue:生产者-消费者神器
BlockingQueue queue = new ArrayBlockingQueue<>(10);
// 生产者
queue.put("item"); // 队列满时会阻塞
// 消费者
String item = queue.take(); // 队列空时会阻塞
常见实现类:
- ArrayBlockingQueue:有界数组实现
- LinkedBlockingQueue:可选有界链表实现
- PriorityBlockingQueue:支持优先级的无界队列
- SynchronousQueue:不存储元素的特殊队列
六、常见陷阱与最佳实践
6.1 并发编程的"八大陷阱"💀
-
死锁:多个线程互相等待对方释放锁
// 线程1 synchronized(lockA) { synchronized(lockB) { ... } } // 线程2 synchronized(lockB) { synchronized(lockA) { ... } }
-
活锁:线程不断重试失败的操作(像两个人在走廊互相让路)
-
饥饿:某些线程永远得不到CPU时间
-
内存可见性问题:一个线程的修改对另一个线程不可见
-
上下文切换开销:线程太多反而降低性能
-
虚假唤醒:wait()在没有notify()的情况下返回
-
ThreadLocal内存泄漏:忘记remove()导致内存无法回收
-
双重检查锁定问题:错误的单例模式实现
6.2 最佳实践清单✅
- 优先使用高层工具:如并发集合、线程池,而不是自己造轮子
- 尽量减少同步范围:同步代码块越小越好
- 使用不可变对象:避免共享可变状态
- 优先使用volatile而非锁,当适用时
- 文档化线程安全策略:明确说明类的线程安全级别
- 避免过早优化:先保证正确性,再考虑性能
- 使用线程池而非直接创建线程
- 考虑使用并行流简化并行计算(Java 8+)
List numbers = Arrays.asList(1, 2, 3); int sum = numbers.parallelStream().mapToInt(i -> i).sum();
七、实战案例:模拟高并发售票系统🎫
让我们用一个完整的例子巩固所学:
public class TicketSystem {
private final int totalTickets;
private final AtomicInteger remainingTickets;
private final ExecutorService executor;
private final Random random = new Random();
public TicketSystem(int totalTickets, int threadCount) {
this.totalTickets = totalTickets;
this.remainingTickets = new AtomicInteger(totalTickets);
this.executor = Executors.newFixedThreadPool(threadCount);
}
public void startSale() {
for (int i = 0; i < 1000; i++) { // 模拟1000个购票请求
executor.execute(() -> {
try {
// 模拟网络延迟
Thread.sleep(random.nextInt(100));
buyTicket();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
}
private void buyTicket() {
while (true) {
int current = remainingTickets.get();
if (current <= 0) {
System.out.println("票已售罄!");
return;
}
if (remainingTickets.compareAndSet(current, current - 1)) {
System.out.printf("%s 购票成功,剩余票数:%d%n",
Thread.currentThread().getName(), current - 1);
return;
}
// CAS失败,重试
}
}
public static void main(String[] args) {
TicketSystem system = new TicketSystem(100, 10); // 100张票,10个窗口
system.startSale();
}
}
这个例子展示了:
- 使用AtomicInteger保证原子操作
- 线程池管理购票线程
- CAS(Compare-And-Swap)无锁编程
- 处理高并发竞争
八、Java并发编程的未来
随着硬件发展,并发编程也在不断进化:
-
虚拟线程(Java 19+):轻量级线程,大幅提升并发能力
Thread.startVirtualThread(() -> { System.out.println("我是虚拟线程!"); });
-
结构化并发(Java 21+):使并发代码更易编写和维护
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future user = scope.fork(() -> findUser()); Future order = scope.fork(() -> fetchOrder()); scope.join(); // 等待所有任务完成 } // 自动清理所有线程
-
反应式编程:如Project Reactor、RxJava等异步编程模型
九、总结
Java并发编程就像管理一个高效的团队👥,需要:
- 明确分工(线程职责单一)
- 良好沟通(线程间协作)
- 资源管理(线程池、锁)
- 避免冲突(线程安全)
- 持续优化(性能调优)
记住这些要点,你就能写出高效、安全的并发程序!虽然并发编程很复杂,但掌握了它,你就能处理各种高性能场景,成为真正的Java高手!💪
希望这篇万字长文对你有帮助!如果觉得不错,别忘了点赞收藏哦~❤️ 有什么问题欢迎在评论区讨论!