探索Java并发编程--从基础到高级实践技巧
Thread(线程)
线程 = 程序执行的最小单位(一个进程至少有一个线程)。线程内有自己的执行栈、程序计数器(PC),但与同进程内其他线程共享堆内存与进程资源
在java中,线程由java.lang.Thread表示,但我们通常不直接操作裸线程,而用更高层的并发工具(线程池、任务、并发集合等)
一、线程的生命周期与常见操作
状态:NEW、RUNNABLE、BLOCKED / WAITING / TIMED_WAITING、TERMINATED
- NEW:对象创建后,未start()
- RUNNABLE:可运行(包括实际运行或等待运行)
- BLOCKED:等待获取对象监视器(synchronized)
- WAITING:等待其他线程通知(Object.wait()、Thread.join()无超时)
- TERMINATED:run方法返回或抛出异常结束
常用方法:
- start():启动线程(不要直接调用run,会变成普通方法调用)
- join():等待线程结束
- sleep():当前线程休眠(会释放CPU,但不释放锁)
- interrupt():中断线程(合作式,需要线程内检查isInterrupted()或捕获InterruptedException)
- yield():礼让(建议少用,平台依赖)
二、创建线程
- 继承Thread
// 问题:单继承限制、职责不清(把任务和线程绑定)
class MyThread extends Thread {public void run() { /* 任务 */ }
}
new MyThread().start();
- 实现Runnable
// 好处:解耦任务和线程、可复用
Runnable task = () -> { /* 任务 */ };
new Thread(task).start();
- 实现Callable + Future(用于返回值 / 异常)
// 支持返回值和抛异常
Callable<Integer> c = () -> 1+2;
Future<Integer> f = executor.submit(c);
Integer result = f.get(); // 阻塞取结果
- 使用线程池(ExecutorService / ThreadPoolExecutor)
ExecutorService pool = Executors.newFixedThreadPool(5);
pool.submit(() -> { /* 任务 */ });
pool.shutdown();
// 不要滥用Executor.*生成的未定制池(可能使用无界队列或者不合适的场景)一般直接构造ThreadPoolExecutor并调参
- ForkJoinPool(分治、并行流)
- 适合CPU密集型、可分解任务(RecursiveTask)。采用工作窃取(work-stealing)
- CompletableFuture(异步组合)
- 异步流式编程supplyAsync、链式 thenApply、异常处理 exceptionally,能把异步任务组合成复杂流程。
- 虚拟线程(Project Loom —— 概念)
- 新一代思想:大量“轻量级线程”由 JVM 管理,降低线程/请求的成本,能用同步编程模型写大并发程序。
- (提示:具体语法/可用性随 JDK 版本变化,写生产代码前确认 JDK 支持情况。)
三、同步与内存可见性
并发核心问题是:共享可变状态会导致竞态、可见性问题
-
synchronized(内置锁)
- 监视器(monitor):synchronized修饰方法或代码块
- 可重入(同一线程可重复获得同一把锁)
- wait() / notify() / notifyAll()在持锁状态下使用,用于线程间协作
// 特点:保证同一时刻只有一个线程能执行临界区代码,并且进入 / 退出时会触发内存可见性更新/*单例模式(懒汉式加锁) */ public class Singleton {private static Singleton instance;public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance;} }/*多线程安全更新共享资源 */ public class Counter {private int count = 0;public synchronized void increment() {count++;}public synchronized int getCount() {return count;} }/*保证集合安全(eg:ArrayList在多线程下不安全,可用Collections.synchronizedList包装) */
-
volatile
- 保证可见性(写入一个volatile变量后,后续读能看到最新值)和禁止指令重排序(对单个变量)
- 不能替代锁来保证复合操作的原子性(比如count++不是原子性)
// 特点:让一个线程修改的变量能立即被其他线程看到;禁止指令重排序。 // 适合 状态标志、开关类变量 /*线程停止标志 */ class Task implements Runnable {private volatile boolean running = true;public void run() {while (running) {// do work...}}public void stop() {running = false; // 立刻被其他线程看到} } /*配置热更新一个后台线程定时刷新配置,把值写入volatile变量,业务线程能马上读到新值 */// 注意:下面这种情况要用synchronized或AtomicInteger
-
Lock(java.util.concurrent.locks)
- ReentrantLock:比synchronized更灵活(可中断获取锁、可轮询、可超时),支持Condition(类似多个wait / nottify集合)
- ReadWriteLock:读写分离锁,读多写少场景有利
// 显示锁,比synchronized更灵活,可定时、可中断、支持多个条件队列 /*尝试获取锁(避免死等) */ ReentrantLock lock = new ReentrantLock(); if (lock.tryLock(1, TimeUnit.SECONDS)) {try {// 临界区} finally {lock.unlock();} } else {// 获取锁失败,执行降级逻辑 }/* 读写分离(适合读多写少) */ ReadWriteLock rwLock = new ReentrantReadWriteLock(); Lock r = rwLock.readLock(); Lock w = rwLock.writeLock();
-
原子类(AtomicInteger / Long / Reference)
- 基于CAS(无锁算法),用于计数等简单场景。注意ABA问题(可用AtomicStampedReference)
// 基于CAS实现的无锁并发,常用于计数器、序列号 /*高性能计数器 */ private AtomicInteger count = new AtomicInteger(); public void increment() {count.incrementAndGet(); }/*并发限流 / 统计QPS、在线人数统计,避免用synchronized降低性能 */
-
内存模型(JMM)要点
- 主内存 + 工作内存(线程本地缓存),写到主内存后其他线程可见。volatile / 锁会产生内存屏障,保证可见性与有序性
四、常用并发工具(java.util.concurrent)
- 线程池:ThreadoolExecutor(核心、最大线程数、队列、饱和策略)
- 阻塞队列:ArrayBlockingQueue,LinkBlockingQueue,PriorityBlockQueue(常用于生产者-消费者)
- 同步辅助类:
CountDownLatch
,CyclicBarrier
,Semaphore
,Phaser
,Exchanger
- 并发集合:
ConcurrentHashMap
,CopyOnWriteArrayList
,ConcurrentLinkedQueue
- ForkJoinPool(并行任务)
- BlockingQueue + ThreadPool:典型生产者-消费者模式
- FutureTask:可作为Runnable也可获取结果,常用于把Callable封装成任务
五、线程安全问题与常见陷阱
- 竞态条件(race condition):并发修改同一状态导致错误结果
- 死锁(deadlock):两个或多个线程互相持有对方需要的锁。四要素:互斥、持有并等待、不可剥夺、循环等待
- 活锁(livelock):线程不断执行但无法取得进展(一直让步)
- 饥饿(starvation):某线程长期无法获得CPU或锁资源
- 优先级反转(priority inversion):低优先级线程持锁导致高优先级线程等待
- 内存泄露(ThreadLocal):线程池中使用ThreadLocal若不remove(),会造成对象无法回收(尤其在容器应用中)
排查工具:jstack(线程dump)、jvisualvm、jconsole、应用日志、线程名 / ID打点、Flight Recorder。线程dump是定位死锁 / 阻塞的利器
六、守护线程(Daemon)vs 用户线程(User)
- 用户线程:只要存在任意用户线程,JVM不会退出
- 守护线程:JVM在所有用户线程结束后会退出,即使守护线程还在跑