Java多线程案例入门:从零到实战的完整指南
目录导读
- 为什么必须掌握多线程?
- 线程基础概念速览(含核心问答)
- 三种经典创建线程的方式及代码案例
- 线程安全与锁机制:synchronized + Lock实战
- 生产者-消费者模式:wait/notify与BlockingQueue双解法
- 高频面试题问答(线程池、死锁、volatile)
- 总结与最佳实践建议
为什么必须掌握多线程?
在CPU多核普及的今天,单线程程序无法充分利用硬件资源,Java多线程能显著提升程序响应速度和吞吐量,据Stack Overflow 2023年调查,多线程与并发是Java开发者最关注的核心技能之一,本篇文章通过实战案例,帮助初学者快速建立多线程编程思维。

线程基础概念速览
- 进程:资源分配的单位;线程:CPU调度的最小单位。
- 线程状态:NEW(新建)、RUNNABLE(可运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)、TERMINATED(终止)。
- 上下文切换:线程调度开销,避免过多线程。
核心问答:线程和进程的区别?
答:进程拥有独立的地址空间和资源,而线程共享进程内存,切换成本更低,多线程适合I/O密集型任务,多进程适合CPU密集且需要强隔离的场景。
三种经典创建线程的方式及代码案例
案例1:继承Thread类
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 运行中");
}
}
// 调用:new MyThread().start();
案例2:实现Runnable接口(推荐)
Runnable task = () -> System.out.println("Runnable执行");
Thread t = new Thread(task);
t.start();
案例3:实现Callable接口(带返回值)
Callable<Integer> callable = () -> { return 1 + 2; };
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
System.out.println(futureTask.get()); // 输出3
实战提醒:通常使用
Runnable或Callable,避免单继承限制。
线程安全与锁机制详解
问题场景:多个线程同时对计数器累加
public class Counter {
private int count = 0;
public void increment() { count++; } // 非原子性操作
}
现象:最终结果小于预期值,因为count++是“读-改-写”三步,非线程安全。
synchronized关键字
public synchronized void increment() { count++; }
- 优缺点:自动加锁/释放,但无法中断等待、性能较低。
Lock接口 + ReentrantLock
Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try { count++; } finally { lock.unlock(); }
}
- 优点:支持公平锁、超时、可中断。
核心问答:synchronized和Lock哪个更好?
答:
synchronized代码简洁,JVM自动管理锁;Lock更灵活(如尝试非阻塞获取锁tryLock()),适合复杂并发场景,目前JDK 15+对synchronized做了大量优化(偏向锁、轻量级锁),性能差距缩小。
生产者-消费者模式:wait/notify与BlockingQueue
经典案例(使用wait/notify)
class Producer implements Runnable {
private Queue<Integer> queue;
private int maxSize;
@Override
public void run() {
while(true) {
synchronized(queue) {
while(queue.size() == maxSize) {
try { queue.wait(); } catch (InterruptedException e) { }
}
queue.add(new Random().nextInt());
queue.notifyAll();
}
}
}
}
// 消费者类似,判空条件后wait
更简洁的现代写法:ArrayBlockingQueue
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); // 生产者:queue.put(1); 消费者:queue.take();
put()和take()自动阻塞等待,避免手动通知,极大降低bug风险。
核心问答:为什么用while而非if判断条件?
答:防止虚假唤醒(线程在wait()返回后条件不再满足),使用
while循环重新检查,保证线程安全。
高频面试题问答(进阶)
Q1:线程池有哪些参数?怎么配置合理?
答:核心参数:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、workQueue(任务队列)、keepAliveTime,建议:
- CPU密集:核心线程 = CPU核数
- I/O密集:核心线程 = CPU核数 * 2
- 使用
Executors.newFixedThreadPool(n)时注意队列无界风险,生产环境建议ThreadPoolExecutor手动配置。
Q2:volatile能保证线程安全吗?
答:volatile保证可见性和有序性(禁止指令重排),但不保证原子性,例如volatile int count; count++依然不是线程安全的,需配合synchronized或AtomicInteger。
Q3:如何避免死锁?
答:死锁四大必要条件:互斥、占有并等待、不可剥夺、循环等待,打破任意一条即可,常见手段:加锁顺序一致、使用tryLock超时返回、减少锁粒度。
总结与最佳实践建议
- 优先使用高级并发工具(
BlockingQueue、ThreadPoolExecutor、CompletableFuture)而非底层API。 - 线程安全目标:共享数据不可变 > 线程封闭 > 同步控制。
- 性能调优:线程粒度不要过细,避免大量上下文切换。
- 测试:多线程问题难以复现,使用
jstack、jvisualvm排查死锁和内存竞争。
一句话总结:多线程编程的核心不是“开更多线程”,而是安全地管理共享资源的并发访问,掌握本文案例,即可应对70%以上的日常并发需求。
延伸阅读:如果你想深入并发设计模式(如Fork/Join、并行流),可以关注后续的《Java并发进阶:从锁到无锁编程》,希望本文对你的学习之路有实际帮助!