Play Open
Loading Please wait Loading Please wait Loading Please wait Loading Please wait Loading Please wait Loading Please wait

揭秘Java并发编程:原理、陷阱与最佳实践(万字长文详解)

大家好呀!👋 今天咱们来聊聊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(默认):直接抛出RejectedExecutionExceptionCallerRunsPolicy:让提交任务的线程自己执行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高手!💪

希望这篇万字长文对你有帮助!如果觉得不错,别忘了点赞收藏哦~❤️ 有什么问题欢迎在评论区讨论!

推荐阅读文章

由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)

如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系

HTTP、HTTPS、Cookie 和 Session 之间的关系

什么是 Cookie?简单介绍与使用方法

什么是 Session?如何应用?

使用 Spring 框架构建 MVC 应用程序:初学者教程

有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误

如何理解应用 Java 多线程与并发编程?

把握Java泛型的艺术:协变、逆变与不可变性一网打尽

Java Spring 中常用的 @PostConstruct 注解使用总结

如何理解线程安全这个概念?

理解 Java 桥接方法

Spring 整合嵌入式 Tomcat 容器

Tomcat 如何加载 SpringMVC 组件

“在什么情况下类需要实现 Serializable,什么情况下又不需要(一)?”

“避免序列化灾难:掌握实现 Serializable 的真相!(二)”

如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)

解密 Redis:如何通过 IO 多路复用征服高并发挑战!

线程 vs 虚拟线程:深入理解及区别

深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别

10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!

“打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”

Java 中消除 If-else 技巧总结

线程池的核心参数配置(仅供参考)

【人工智能】聊聊Transformer,深度学习的一股清流(13)

Java 枚举的几个常用技巧,你可以试着用用

由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)

如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系

HTTP、HTTPS、Cookie 和 Session 之间的关系

使用 Spring 框架构建 MVC 应用程序:初学者教程

有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误

Java Spring 中常用的 @PostConstruct 注解使用总结

线程 vs 虚拟线程:深入理解及区别

深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别

10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!

探索 Lombok 的 @Builder 和 @SuperBuilder:避坑指南(一)

为什么用了 @Builder 反而报错?深入理解 Lombok 的“暗坑”与解决方案(二)

Posted in 23世界杯
Previous
All posts
Next