1. Java多线程编程概述
在Java开发中,多线程编程是提升程序性能的核心技术之一。当我们需要处理高并发请求、执行耗时操作或充分利用多核CPU资源时,多线程就成为了必备技能。但多线程编程并非简单的创建几个Thread对象那么简单,它涉及线程安全、锁机制、线程通信等复杂概念。
我曾在电商系统开发中遇到过这样的场景:在促销活动期间,系统需要同时处理数万个用户的秒杀请求。如果采用单线程处理,用户等待时间会呈指数级增长。通过合理运用多线程技术,我们将请求处理吞吐量提升了近20倍。这个案例让我深刻认识到,掌握多线程不仅是为了应对面试,更是解决实际性能问题的利器。
2. Java线程基础与创建方式
2.1 线程的生命周期
Java线程从创建到销毁会经历多个状态变化:
- NEW:线程对象刚创建,尚未启动
- RUNNABLE:线程正在JVM中执行或等待系统资源
- BLOCKED:线程被阻塞,等待监视器锁
- WAITING:线程无限期等待其他线程执行特定操作
- TIMED_WAITING:线程在指定时间内等待
- TERMINATED:线程执行完毕
理解这些状态对调试多线程问题至关重要。我曾遇到过一个生产环境死锁问题,通过jstack分析线程dump文件,发现多个线程处于BLOCKED状态,最终定位到是同步代码块设计不合理导致的。
2.2 创建线程的三种方式
1. 继承Thread类
java复制class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行: " + Thread.currentThread().getName());
}
}
// 使用
new MyThread().start();
2. 实现Runnable接口
java复制class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable执行: " + Thread.currentThread().getName());
}
}
// 使用
new Thread(new MyRunnable()).start();
3. 使用Callable和Future(带返回值)
java复制ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
Thread.sleep(1000);
return "任务完成";
});
// 获取结果
String result = future.get(); // 阻塞直到获取结果
实际开发中推荐使用Runnable或Callable方式,因为Java不支持多重继承,且这种方式更符合面向对象的设计原则。
3. 线程同步与锁机制
3.1 synchronized关键字
synchronized是Java最基本的同步机制,可用于:
- 实例方法:锁住当前对象实例
- 静态方法:锁住当前类的Class对象
- 代码块:可指定锁对象
java复制// 实例方法同步
public synchronized void increment() {
count++;
}
// 静态方法同步
public static synchronized void staticMethod() {
// ...
}
// 同步代码块
public void doSomething() {
synchronized(lockObject) {
// 临界区代码
}
}
我曾在一个支付系统中使用synchronized解决余额并发修改问题,但后来发现当并发量高时性能下降明显。这是因为synchronized是重量级锁,在高并发场景下会有较大开销。
3.2 volatile关键字
volatile确保变量的可见性和禁止指令重排序,但不保证原子性。适合用于状态标志:
java复制private volatile boolean running = true;
public void stop() {
running = false;
}
public void run() {
while(running) {
// 执行任务
}
}
3.3 Java并发包中的锁
Java 5引入了java.util.concurrent.locks包,提供了更灵活的锁机制:
ReentrantLock示例:
java复制private final Lock lock = new ReentrantLock();
public void performTask() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 确保锁释放
}
}
ReentrantLock相比synchronized的优势:
- 可尝试获取锁(tryLock)
- 可中断的锁获取(lockInterruptibly)
- 公平锁选项
- 支持多个条件变量
4. 线程池与并发工具类
4.1 线程池的创建与使用
Java通过Executors提供几种常用线程池:
java复制// 固定大小线程池
ExecutorService fixedPool = Executors.newFixedThreadPool(5);
// 缓存线程池
ExecutorService cachedPool = Executors.newCachedThreadPool();
// 单线程池
ExecutorService singleThread = Executors.newSingleThreadExecutor();
// 定时任务线程池
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(3);
但在生产环境中,更推荐直接使用ThreadPoolExecutor,可以更精细地控制参数:
java复制ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 工作队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
我在实际项目中曾因不当设置线程池参数导致OOM:队列设置过大(LinkedBlockingQueue无界),当任务堆积时内存耗尽。教训是:必须根据业务特点合理设置队列容量和拒绝策略。
4.2 常用并发工具类
CountDownLatch - 等待多个线程完成:
java复制CountDownLatch latch = new CountDownLatch(3);
// 工作线程
new Thread(() -> {
// 执行任务
latch.countDown();
}).start();
// 主线程等待
latch.await(); // 阻塞直到计数器归零
CyclicBarrier - 线程到达屏障点后一起继续:
java复制CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达屏障点");
});
// 工作线程
new Thread(() -> {
// 执行第一阶段
barrier.await();
// 执行第二阶段
}).start();
Semaphore - 控制资源访问的并发数:
java复制Semaphore semaphore = new Semaphore(5); // 允许5个并发
void accessResource() throws InterruptedException {
semaphore.acquire();
try {
// 访问受限资源
} finally {
semaphore.release();
}
}
5. 线程安全设计与最佳实践
5.1 常见的线程安全问题
- 竞态条件:多个线程同时修改共享数据导致结果不可预测
- 死锁:多个线程互相等待对方释放锁
- 活锁:线程不断重试失败的操作
- 内存可见性:一个线程的修改对其他线程不可见
5.2 避免死锁的策略
- 锁顺序:所有线程以相同顺序获取锁
- 锁超时:使用tryLock设置超时时间
- 死锁检测:定期检查线程依赖关系
java复制// 使用tryLock避免死锁
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// 成功获取两个锁
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
5.3 无锁编程与原子类
Java提供了一系列原子类,如AtomicInteger、AtomicReference等:
java复制private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // 原子操作
}
在低竞争环境下,原子类的性能通常优于锁。我曾用AtomicLong替换synchronized计数器,QPS提升了约30%。
6. Java内存模型与happens-before
6.1 JMM核心概念
Java内存模型定义了线程如何与内存交互,关键点包括:
- 每个线程有自己的工作内存
- 主内存是所有线程共享的
- volatile变量直接读写主内存
- synchronized建立内存屏障
6.2 happens-before规则
- 程序顺序规则:同一线程中的操作按程序顺序执行
- 锁规则:解锁操作先于后续的加锁操作
- volatile规则:volatile写先于后续的读
- 线程启动规则:线程的start()先于它的任何操作
- 线程终止规则:线程的所有操作先于它的终止检测
理解这些规则对编写正确的并发程序至关重要。我曾调试过一个看似不可能出现的并发bug,最终发现是因为忽视了happens-before规则导致的内存可见性问题。
7. 并发集合类的使用
7.1 ConcurrentHashMap
java复制ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 线程安全的putIfAbsent
map.putIfAbsent("key", 1);
// 原子更新
map.compute("key", (k, v) -> v == null ? 1 : v + 1);
ConcurrentHashMap在JDK 8中进行了重大改进,采用CAS+synchronized实现,分段锁被废弃。
7.2 CopyOnWriteArrayList
适合读多写少的场景:
java复制CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 写操作会复制整个数组
list.add("item");
// 读操作不需要同步
String item = list.get(0);
7.3 BlockingQueue实现生产者-消费者
java复制BlockingQueue<String> queue = new LinkedBlockingQueue<>(10);
// 生产者
new Thread(() -> {
while (true) {
String item = produceItem();
queue.put(item); // 队列满时阻塞
}
}).start();
// 消费者
new Thread(() -> {
while (true) {
String item = queue.take(); // 队列空时阻塞
consumeItem(item);
}
}).start();
在实际项目中,我曾用BlockingQueue实现日志异步处理系统,生产者将日志放入队列,消费者线程批量写入数据库,显著提升了系统吞吐量。
8. CompletableFuture异步编程
Java 8引入的CompletableFuture大大简化了异步编程:
java复制CompletableFuture.supplyAsync(() -> {
// 异步执行任务
return fetchDataFromRemote();
}).thenApply(data -> {
// 转换结果
return processData(data);
}).thenAccept(result -> {
// 消费最终结果
saveResult(result);
}).exceptionally(ex -> {
// 异常处理
log.error("任务失败", ex);
return null;
});
在微服务架构中,我常用CompletableFuture实现多个服务的并行调用:
java复制CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(
() -> userService.getUser(userId), executor);
CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(
() -> orderService.getLatestOrder(userId), executor);
userFuture.thenCombine(orderFuture, (user, order) -> {
// 合并两个异步结果
return new UserOrderInfo(user, order);
}).thenAccept(System.out::println);
这种模式比传统的回调方式更简洁,也更符合函数式编程的思想。
