一、为什么每个Java程序员都要懂多线程?
(敲黑板!)大家有没有遇到过这样的场景:你的程序运行速度像蜗牛一样慢,CPU利用率却低得可怜?这时候就该多线程登场了!举个真实案例——去年我帮朋友优化一个电商秒杀系统,单线程处理请求时QPS(每秒查询率)只能到50,改成多线程后直接飙升到2000+!
不过先别急着兴奋,多线程就像一把双刃剑。搞得好性能飞升,搞不好就是各种bug满天飞(别问我怎么知道的,都是泪啊)。咱们先来搞懂几个核心概念:
- 进程 vs 线程:进程是独立的应用,线程是进程里的执行单元(可以理解为工厂和工人的关系)
- 并行 vs 并发:并行是真·同时执行(需要多核CPU),并发是快速切换的假象(单核也能玩)
二、手把手教你创建线程的3种姿势
1. 继承Thread类(适合简单场景)
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程启动:" + Thread.currentThread().getName());
}
}
// 使用示例
new MyThread().start();
2. 实现Runnable接口(推荐方式)
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable的线程:" + Thread.currentThread().getName());
}
}
// 启动线程的正确姿势
new Thread(new MyRunnable()).start();
3. 使用Callable+Future(需要返回值时用)
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
TimeUnit.SECONDS.sleep(2);
return 42;
});
// 获取返回值(会阻塞!)
Integer result = future.get();
三、线程的生命周期全解析
(重点预警!)线程的一生要经历这些状态:
- NEW → 刚出生的小宝宝
- RUNNABLE → 准备就绪的运动员
- BLOCKED → 被关小黑屋了
- WAITING → 望眼欲穿的等待
- TIMED_WAITING → 带计时器的等待
- TERMINATED → 光荣退休
看个状态转换图更直观:
启动线程 获取锁失败 wait()被调用 sleep(timeout)
NEW → RUNNABLE → BLOCKED ←---------------------→ WAITING
| ^ | notify()/notifyAll()
| | | join(timeout)
V | V
RUNNING TIMED_WAITING
四、线程同步的5大杀器
1. synchronized关键字(基础款)
// 同步方法
public synchronized void transfer(int amount) {
// 转账操作
}
// 同步代码块(更灵活)
public void update() {
synchronized(this) {
// 临界区代码
}
}
2. ReentrantLock(高配版)
Lock lock = new ReentrantLock();
void safeMethod() {
lock.lock();
try {
// 危险操作
} finally {
lock.unlock(); // 必须放在finally块!
}
}
3. 原子类(无锁编程神器)
AtomicInteger counter = new AtomicInteger(0);
// 线程安全的自增
counter.incrementAndGet();
4. CountDownLatch(多人赛跑发令枪)
CountDownLatch latch = new CountDownLatch(3);
// 工作线程
new Thread(() -> {
doWork();
latch.countDown();
}).start();
// 主线程等待
latch.await(); // 阻塞直到计数器归零
5. CyclicBarrier(循环路障)
CyclicBarrier barrier = new CyclicBarrier(3, () ->
System.out.println("所有玩家已就位!"));
// 每个线程到达屏障点时
barrier.await(); // 会阻塞直到所有线程到达
五、线程池的正确打开方式
(血泪教训!)千万别再new Thread了!线程池才是王道:
// 创建线程池的正确姿势
ExecutorService pool = Executors.newFixedThreadPool(5);
// 提交任务
for (int i = 0; i < 100; i++) {
pool.execute(() -> {
// 执行具体任务
});
}
// 优雅关闭
pool.shutdown();
pool.awaitTermination(1, TimeUnit.HOURS);
线程池参数调优的黄金法则:
- corePoolSize:日常并发量
- maximumPoolSize:最大承载量(建议不要超过CPU核心数*2)
- keepAliveTime:闲置线程存活时间
- workQueue:任务队列(ArrayBlockingQueue vs LinkedBlockingQueue)
六、实战:模拟12306抢票系统
来,咱们用多线程实现一个真实的抢票场景:
class TicketSystem {
private AtomicInteger tickets = new AtomicInteger(100);
public boolean grabTicket(String user) {
int remaining = tickets.decrementAndGet();
if(remaining >= 0) {
System.out.println(user + "抢票成功!剩余票数:" + remaining);
return true;
}
System.out.println(user + "手慢了...");
return false;
}
}
// 模拟1000人同时抢票
TicketSystem system = new TicketSystem();
ExecutorService pool = Executors.newFixedThreadPool(20);
for (int i = 0; i < 1000; i++) {
final String user = "用户" + i;
pool.execute(() -> system.grabTicket(user));
}
运行这个程序你会看到:
- 票数可能变成负数(如果不做同步控制)
- 同一张票可能被多个人抢到(经典的线程安全问题)
- 打印顺序混乱(这就是线程执行的随机性)
要解决这些问题,就需要用到前面讲的同步机制。试试用synchronized或ReentrantLock改造这个程序吧!
七、避坑指南(血泪总结)
-
死锁预防四原则:
- 按固定顺序获取锁
- 设置锁超时时间
- 使用tryLock()
- 避免嵌套锁
-
性能优化三板斧:
- 减少锁的粒度(比如用ConcurrentHashMap代替synchronizedMap)
- 使用读写锁(ReentrantReadWriteLock)
- 尽量使用无锁数据结构(Atomic类、LongAdder)
-
常见异常处理:
- InterruptedException:正确处理线程中断
- RejectedExecutionException:合理配置线程池拒绝策略
- TimeoutException:设置合理的超时时间
-
内存可见性陷阱:
- 总是用volatile修饰会被多个线程访问的变量
- 或者使用Atomic类保证原子性
- 不要依赖普通的++操作(它根本不是原子操作!)
八、学习路线图(打怪升级指南)
- 新手村:掌握Thread/Runnable基础用法
- 初级副本:理解synchronized和volatile
- 中级挑战:玩转JUC包(java.util.concurrent)
- 高级战场:研究AQS(AbstractQueuedSynchronizer)源码
- 终极BOSS:掌握Disruptor等高性能并发框架
最后送大家一句话:多线程编程就像在钢丝上跳舞,既要保持平衡(线程安全),又要追求优雅(高性能)。多写多练,多踩坑,才是最快的成长方式!