1. JavaEE多线程初阶:从基础到实战
多线程是JavaEE开发中绕不开的核心技术,也是面试官最喜欢深挖的知识点之一。记得我第一次在生产环境遇到线程安全问题时,整整排查了三天才发现是SimpleDateFormat未做线程隔离导致的。这种"血泪史"让我意识到,多线程不是知道几个概念就能应付的,必须建立完整的知识体系。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多线程基础概念解析
2.1 进程与线程的本质区别
进程是操作系统资源分配的最小单位,每个进程都有独立的地址空间。而线程是CPU调度的最小单位,共享进程的内存空间。举个例子:Chrome浏览器是一个进程,每个标签页可以看作一个线程,它们共享浏览器的缓存和Cookie,但各自执行不同的页面渲染任务。
关键区别:进程间通信(IPC)需要特殊机制,而线程可以直接读写共享内存
2.2 Java线程的三种创建方式
- 继承Thread类:最基础的方式,但Java单继承的特性限制了扩展性
java复制class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running");
}
}
- 实现Runnable接口:更灵活,推荐方式
java复制class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable running");
}
}
- 使用Callable+Future:可以获取返回值,适合需要结果的计算任务
java复制Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();
System.out.println(future.get()); // 阻塞获取结果
3. 线程生命周期与状态转换
3.1 六种线程状态详解
| 状态 | 触发条件 | 特点 |
|---|---|---|
| NEW | new Thread()后 | 尚未调用start() |
| RUNNABLE | 调用start() | 可能在运行或等待CPU时间片 |
| BLOCKED | 等待获取监视器锁 | 同步代码块场景 |
| WAITING | 调用wait()/join() | 无限期等待 |
| TIMED_WAITING | 调用sleep(n)/wait(n)/join(n) | 带超时的等待 |
| TERMINATED | run()执行完毕或抛出未捕获异常 | 线程生命周期结束 |
3.2 状态转换实战演示
java复制Thread t = new Thread(() -> {
try {
Thread.sleep(1000); // TIMED_WAITING
synchronized(lock) { // BLOCKED(如果锁被占用)
lock.wait(500); // TIMED_WAITING
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE
4. 线程同步与锁机制
4.1 synchronized的三种用法
- 实例方法同步:锁对象是当前实例
java复制public synchronized void method() {
// 临界区代码
}
- 静态方法同步:锁对象是Class对象
java复制public static synchronized void staticMethod() {
// 临界区代码
}
- 代码块同步:显式指定锁对象
java复制public void blockMethod() {
synchronized(lockObj) {
// 临界区代码
}
}
4.2 volatile关键字的正确理解
volatile保证可见性和有序性,但不保证原子性。典型场景:
java复制class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true; // 写操作
}
public void reader() {
if (flag) { // 读操作
// 保证能看到最新值
}
}
}
常见误区:认为volatile可以替代锁。实际上它只适合状态标志等简单场景
5. 线程池深度解析
5.1 ThreadPoolExecutor核心参数
java复制new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 空闲线程存活时间
unit, // 时间单位
workQueue, // 任务队列
threadFactory, // 线程工厂
handler // 拒绝策略
);
5.2 四种拒绝策略对比
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy | 直接抛出RejectedExecutionException | 严格要求任务不丢失 |
| CallerRunsPolicy | 由调用线程执行该任务 | 适合慢速消费场景 |
| DiscardPolicy | 静默丢弃任务 | 允许丢失部分任务 |
| DiscardOldestPolicy | 丢弃队列中最老的任务并重试 | 允许丢失非关键任务 |
6. 常见问题排查与优化
6.1 死锁检测与预防
死锁产生的四个必要条件:
- 互斥条件
- 请求与保持
- 不剥夺条件
- 循环等待
检测工具:
bash复制jstack <pid> | grep -A 10 deadlock
预防方案:
- 按固定顺序获取锁
- 使用tryLock()设置超时
- 避免嵌套锁
6.2 线程池参数调优经验
- CPU密集型任务:核心线程数=CPU核数+1
- IO密集型任务:核心线程数=CPU核数*2
- 混合型任务:拆分线程池分别处理
监控指标:
java复制ThreadPoolExecutor executor = ...;
executor.getActiveCount(); // 活动线程数
executor.getQueue().size(); // 队列积压
7. JavaEE中的多线程实践
7.1 Servlet线程安全注意事项
Servlet默认是多线程的,实例变量需要特别注意:
java复制public class UnsafeServlet extends HttpServlet {
private int count; // 危险!所有请求共享
protected void doGet(...) {
count++; // 非原子操作
}
}
解决方案:
- 使用局部变量
- 加synchronized同步
- 使用Atomic原子类
7.2 Spring中的异步处理
- 启用异步支持:
java复制@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
executor.initialize();
return executor;
}
}
- 使用@Async注解:
java复制@Service
public class AsyncService {
@Async
public CompletableFuture<String> asyncMethod() {
// 长时间任务
return CompletableFuture.completedFuture("result");
}
}
8. 多线程调试技巧
8.1 IDEA线程调试配置
- 断点右键选择"Thread"模式
- 在Debug窗口切换线程上下文
- 使用"Evaluate Expression"查看线程局部变量
8.2 日志中的线程追踪
推荐日志格式:
properties复制%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
输出示例:
code复制2023-08-20 14:30:45 [http-nio-8080-exec-1] INFO c.e.s.ServiceImpl - Processing request
2023-08-20 14:30:45 [pool-1-thread-2] DEBUG c.e.s.AsyncTask - Start async task
9. 性能优化实战案例
9.1 计数器优化对比
原始版本:
java复制class Counter {
private int count;
public synchronized void increment() {
count++;
}
}
优化方案:
- 使用AtomicLong:
java复制private AtomicLong count = new AtomicLong();
public void increment() {
count.incrementAndGet();
}
- LongAdder(JDK8+):
java复制private LongAdder count = new LongAdder();
public void increment() {
count.increment();
}
性能对比(1000万次操作):
| 实现方式 | 耗时(ms) |
|---|---|
| synchronized | 520 |
| AtomicLong | 320 |
| LongAdder | 120 |
9.2 并发集合选择指南
| 场景 | 推荐实现类 | 特点 |
|---|---|---|
| 高频读少写 | CopyOnWriteArrayList | 写时复制,读无锁 |
| 键值对缓存 | ConcurrentHashMap | 分段锁,高并发 |
| 优先级队列 | PriorityBlockingQueue | 阻塞式,自动排序 |
| 生产者消费者 | LinkedBlockingQueue | 可选容量,双锁设计 |
| 延迟任务 | DelayQueue | 按延迟时间排序 |
10. 面试常见问题剖析
10.1 线程安全三问
-
什么是线程安全?
- 当多个线程访问某个类时,这个类始终表现出正确的行为
-
如何保证线程安全?
- 无状态对象
- 不可变对象
- 加锁机制
- 线程封闭
-
synchronized和ReentrantLock的区别?
- 锁的实现:前者JVM实现,后者JDK实现
- 功能特性:后者支持公平锁、条件变量、可中断等
- 使用方式:前者自动释放,后者需要显式unlock()
10.2 线程池执行流程问题
典型问题:"提交任务时线程池如何处理?"
标准回答应包含:
- 核心线程未满→创建新线程
- 核心线程已满→入队列
- 队列已满→创建非核心线程
- 达到最大线程数→执行拒绝策略
11. 生产环境最佳实践
11.1 线程命名规范
推荐使用命名线程池:
java复制ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("order-process-%d")
.build();
ExecutorService executor = new ThreadPoolExecutor(
5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
namedThreadFactory
);
11.2 异常处理机制
未捕获异常会导致线程终止!两种处理方式:
- 为线程设置UncaughtExceptionHandler:
java复制Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
logger.error("Thread {} threw exception", t.getName(), e);
});
- 在Runnable/Callable内部捕获:
java复制executor.submit(() -> {
try {
// 业务代码
} catch (Exception e) {
logger.error("Task failed", e);
}
});
12. 现代并发工具类
12.1 CountDownLatch应用
典型场景:主线程等待多个子任务完成
java复制CountDownLatch latch = new CountDownLatch(3);
IntStream.range(0, 3).forEach(i ->
new Thread(() -> {
try {
// 子任务
} finally {
latch.countDown();
}
}).start()
);
latch.await(); // 阻塞直到计数器归零
12.2 CompletableFuture组合操作
链式调用示例:
java复制CompletableFuture.supplyAsync(() -> queryFromDB())
.thenApplyAsync(result -> processData(result))
.thenAcceptAsync(processed -> sendNotification(processed))
.exceptionally(ex -> {
logger.error("Pipeline failed", ex);
return null;
});
13. 性能监控与诊断
13.1 JConsole监控线程
- 连接Java进程
- 查看"线程"选项卡
- 分析:
- 死锁检测
- 线程状态分布
- CPU时间占比
13.2 Arthas诊断命令
常用命令:
bash复制thread -n 3 # 查看最忙的3个线程
thread -b # 检测死锁
thread --state BLOCKED # 查看阻塞线程
14. 并发设计模式
14.1 Worker Thread模式
典型实现:
java复制class WorkerThread extends Thread {
private final BlockingQueue<Runnable> queue;
public WorkerThread(BlockingQueue<Runnable> queue) {
this.queue = queue;
}
public void run() {
while (true) {
Runnable task = queue.take();
task.run();
}
}
}
14.2 Producer-Consumer模式
使用BlockingQueue实现:
java复制BlockingQueue<Item> queue = new LinkedBlockingQueue<>();
// 生产者
new Thread(() -> {
while (true) {
Item item = produceItem();
queue.put(item);
}
}).start();
// 消费者
new Thread(() -> {
while (true) {
Item item = queue.take();
consumeItem(item);
}
}).start();
15. 内存模型与happens-before
15.1 JMM核心概念
Java内存模型规定了:
- 线程如何与主内存交互
- 什么情况下写操作对其它线程可见
- 指令重排序的限制规则
15.2 happens-before规则
八大规则包括:
- 程序顺序规则
- 锁规则
- volatile规则
- 线程启动规则
- 线程终止规则
- 中断规则
- 终结器规则
- 传递性
16. 并发代码测试策略
16.1 并发单元测试
使用CountDownLatch模拟并发:
java复制@Test
public void testConcurrentAccess() throws InterruptedException {
int threadCount = 10;
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
startLatch.await();
// 执行测试代码
endLatch.countDown();
}).start();
}
startLatch.countDown();
endLatch.await();
// 验证结果
}
16.2 压力测试工具
推荐使用JMeter进行:
- 创建线程组设置并发数
- 添加HTTP请求采样器
- 配置结果监听器
- 分析吞吐量和响应时间
17. 常见反模式与陷阱
17.1 锁粒度问题
错误示范:
java复制public synchronized void processOrder() {
// 查询数据库
// 计算金额
// 调用支付
// 更新库存
// 发送通知
}
优化方案:
java复制public void processOrder() {
Order order = queryOrder();
synchronized(this) {
calculateAmount(order);
}
callPayment(order);
updateStock(order);
sendNotification(order);
}
17.2 线程泄漏问题
典型场景:
java复制ExecutorService executor = Executors.newFixedThreadPool(5);
while (true) {
executor.submit(() -> {
// 任务可能永远阻塞
socket.read(); // 没有超时设置
});
}
解决方案:
- 使用带超时的操作
- 限制任务执行时间:
java复制Future<?> future = executor.submit(task);
future.get(1, TimeUnit.SECONDS);
18. 并发编程学习路线
18.1 推荐学习顺序
- 线程基础(创建/启动/状态)
- 同步机制(synchronized/volatile)
- 并发集合(CopyOnWriteArrayList/ConcurrentHashMap)
- 线程池(Executor框架)
- 高级工具类(CountDownLatch/CyclicBarrier)
- 原子变量与CAS
- 锁优化与性能调优
18.2 必读参考资料
- 《Java并发编程实战》
- 《Java并发编程之美》
- Oracle官方Java并发教程
- JSR-133内存模型规范
- Doug Lea的并发工具类论文
19. 真实案例:订单超卖问题
19.1 问题重现
初始实现:
java复制public boolean reduceStock(Long itemId, int num) {
Item item = itemDao.selectById(itemId);
if (item.getStock() >= num) {
item.setStock(item.getStock() - num);
return itemDao.updateById(item) > 0;
}
return false;
}
并发测试时出现库存减为负数!
19.2 解决方案演进
- 方案一:数据库悲观锁
java复制@Transactional
public boolean reduceStock(Long itemId, int num) {
Item item = itemDao.selectByIdForUpdate(itemId); // for update
// 其余逻辑不变
}
- 方案二:乐观锁
java复制public boolean reduceStock(Long itemId, int num) {
int updated = itemDao.reduceStockWithVersion(itemId, num, oldVersion);
return updated > 0;
}
- 方案三:Redis原子操作
java复制public boolean reduceStock(Long itemId, int num) {
String key = "item:stock:" + itemId;
Long value = redisTemplate.opsForValue().increment(key, -num);
return value != null && value >= 0;
}
20. 未来发展趋势
20.1 协程(Loom项目)
虚拟线程特性预览:
java复制Thread.startVirtualThread(() -> {
System.out.println("Virtual thread running");
});
特点:
- 轻量级(非OS线程)
- 自动调度
- 兼容现有代码
20.2 响应式编程
Reactor示例:
java复制Mono.fromCallable(() -> queryFromDB())
.subscribeOn(Schedulers.parallel())
.map(result -> processData(result))
.subscribe(
processed -> sendNotification(processed),
error -> logger.error("Error", error)
);
