1154 字
3 分钟
Java多线程编程详解
多线程是Java面试的重灾区,synchronized、volatile、Lock的区别能聊一个小时。说实话,我最早写多线程代码也是各种踩坑——死锁、可见性问题、线程安全,一个都没落下。这篇文章把多线程的知识点串了一遍,全是实战经验。
Java多线程编程详解
线程 vs 进程
- 进程:操作系统分配资源的最小单位,每个进程有独立的内存空间
- 线程:CPU调度的最小单位,同一进程内的线程共享内存
区别:进程之间相互隔离,一个进程崩了不影响其他进程;线程之间共享内存,一个线程崩了可能拖垮整个进程。
创建线程的四种方式
1. 继承Thread类
public class MyThread extends Thread { @Override public void run() { System.out.println("线程运行中: " + Thread.currentThread().getName()); }}
// 使用new MyThread().start(); // 注意是start(),不是run()!坑:调run()不会启动新线程,只是在当前线程执行方法。我刚学的时候就犯过这个错——调了半天run()以为是并行的,结果发现一直在主线程里跑。
2. 实现Runnable接口(推荐)
public class MyRunnable implements Runnable { @Override public void run() { System.out.println("线程运行中"); }}
// 使用new Thread(new MyRunnable()).start();
// Lambda更简洁new Thread(() -> System.out.println("线程运行中")).start();推荐理由:Java单继承,继承了Thread就不能继承别的类了。实现Runnable更灵活。
3. 实现Callable接口(带返回值)
public class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 1; i <= 100; i++) sum += i; return sum; }}
// 使用FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());new Thread(futureTask).start();Integer result = futureTask.get(); // 获取结果,会阻塞直到线程完成4. 线程池(生产环境推荐)
ExecutorService executor = Executors.newFixedThreadPool(5);for (int i = 0; i < 10; i++) { int taskId = i; executor.submit(() -> { System.out.println("任务 " + taskId + " 运行中"); });}executor.shutdown();线程生命周期
新建(New) → 就绪(Runnable) ↔ 运行(Running) → 终止(Terminated) ↓ ↑ 阻塞/等待(Blocked/Waiting/Timed Waiting)- New:new了Thread对象,还没start
- Runnable:调了start,等待CPU分配时间片
- Running:正在执行
- Blocked/Waiting:等待锁、等待通知、sleep等
- Terminated:执行完毕
线程安全
多个线程同时访问共享数据时,如果不加控制,就会出问题。典型的场景:两个线程同时给一个变量+1,结果只加了1次。
解决方案
synchronized
// 同步代码块synchronized (lockObject) { // 线程安全的操作}
// 同步方法public synchronized void method() { // 线程安全的操作}Lock(更灵活)
Lock lock = new ReentrantLock();try { lock.lock(); // 线程安全的操作} finally { lock.unlock(); // 一定要在finally里释放}synchronized vs Lock:
- synchronized是关键字,自动释放锁;Lock需要手动释放
- Lock支持尝试获取锁(
tryLock())、可中断等高级功能 - 简单场景用synchronized,复杂场景用Lock
volatile —— 保证可见性
private volatile boolean running = true;volatile保证一个线程修改了变量,其他线程能立即看到。但它不保证原子性,count++这种操作还是需要synchronized。
原子类
AtomicInteger count = new AtomicInteger(0);count.incrementAndGet(); // 原子+1,线程安全线程安全集合
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();ThreadLocal —— 线程局部变量
每个线程都有自己的副本,互不干扰:
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);threadLocal.set(100);int value = threadLocal.get();threadLocal.remove(); // 记得清理,防止内存泄漏!线程通信
wait/notify
synchronized (lock) { while (!condition) { lock.wait(); // 释放锁,等待通知 } // 条件满足,继续执行 lock.notify(); // 通知等待的线程}CountDownLatch —— 等待多个线程完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) { new Thread(() -> { // 执行任务 latch.countDown(); // 完成一个,计数减1 }).start();}
latch.await(); // 等待所有线程完成System.out.println("所有任务完成");CyclicBarrier —— 多个线程互相等待
CyclicBarrier barrier = new CyclicBarrier(3, () -> { System.out.println("所有线程都到达屏障点");});
for (int i = 0; i < 3; i++) { new Thread(() -> { barrier.await(); // 等待其他线程 // 继续执行 }).start();}CountDownLatch vs CyclicBarrier:
- CountDownLatch是”等别人做完”,CyclicBarrier是”互相等”
- CountDownLatch只能用一次,CyclicBarrier可以重用
Semaphore —— 限流
Semaphore semaphore = new Semaphore(2); // 同时最多2个线程执行
for (int i = 0; i < 5; i++) { new Thread(() -> { try { semaphore.acquire(); // 执行任务 } finally { semaphore.release(); } }).start();}线程池
为什么用线程池?
- 减少线程创建和销毁的开销
- 控制并发数,防止资源耗尽
- 统一管理线程
创建线程池
推荐用ThreadPoolExecutor手动创建,不要用Executors的快捷方法(newFixedThreadPool等可能因为队列无界导致OOM):
ThreadPoolExecutor executor = new ThreadPoolExecutor( 2, // corePoolSize:核心线程数 5, // maximumPoolSize:最大线程数 60, // keepAliveTime:空闲线程存活时间 TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), // 任务队列 new ThreadPoolExecutor.AbortPolicy() // 拒绝策略);执行流程:核心线程→任务队列→最大线程→拒绝策略
拒绝策略:
AbortPolicy:抛异常(默认)CallerRunsPolicy:调用者线程执行DiscardPolicy:丢弃DiscardOldestPolicy:丢弃最旧的任务
并发集合
ConcurrentHashMap:线程安全的HashMap,高并发下比HashTable效率高多了CopyOnWriteArrayList:读多写少场景,每次写操作复制整个数组ConcurrentLinkedQueue:无界队列,高性能BlockingQueue:阻塞队列,生产者-消费者模式的标配
实战贴士
- 优先用线程池:不要手动new Thread,用线程池管理线程
- synchronized范围要小:只同步必要的代码块,不要同步整个方法
- Lock要在finally里释放:忘了unlock会导致死锁
- ThreadLocal用完要remove:在线程池环境下,线程复用会导致内存泄漏
- 并发集合代替手动同步:能用ConcurrentHashMap就别自己加synchronized
常见坑
- 调了run()没调start():没有启动新线程,代码在调用线程里执行。这个我刚学的时候就踩过。
- 死锁:两个线程各自持有对方需要的锁,互相等待。用
tryLock()可以避免。 - ThreadLocal内存泄漏:线程池中的线程不会销毁,ThreadLocal的值一直存在,用完记得
remove()。 - 线程池耗尽:任务队列无界,任务提交速度>处理速度,最终OOM。用有界队列。
- 可见性问题:没有加volatile或synchronized,一个线程修改了变量,另一个线程看不到。
分享
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时
相关文章 智能推荐
1
Java网络编程详解
Java 用Java写一个简单的TCP聊天室是大学时最有成就感的事情之一。Socket编程虽然基础,但理解TCP/IP协议栈对排查网络问题帮助很大。
2
Java泛型编程详解
Java 泛型刚出来的时候我觉得不就是个类型参数嘛,直到被PECS原则(Producer Extends Consumer Super)绕晕了才明白泛型的水有多深。
3
Java面向对象编程基础详解
Java 封装、继承、多态——这三个词背了无数遍,但真正理解面向对象是在写了上万行代码之后的事。这篇文章用代码实例讲清楚OOP的核心概念。
4
Java注解详解
Java 刚学Java时觉得注解不就是几个@符号嘛,后来用Spring Boot才发现注解能玩出这么多花样——从编译检查到运行时注入,注解远不止看起来那么简单。
5
Java NIO详解
Java NIO的Selector模型让我第一次感受到了单线程处理万级连接的魅力。相比传统IO的阻塞读写,Channel加Buffer的组合简直是为高性能网络编程量身定做。






