- 引言
- 一、核心概念:线程是什么?
- 二、如何创建并运行一个线程?
- 三、线程安全:共享资源的“修罗场”
- 四、JUC并发工具集(重中之重)
- 五、原子类:无锁的线程安全
- 总结与展望
- 互动环节
引言
在现代多核CPU的背景下,并发编程是挖掘机器性能、提升应用吞吐量的关键手段。然而,它也是一把“双刃剑”,在带来性能提升的同时,也引入了诸如线程安全、死锁、上下文切换开销等一系列复杂问题。Java作为一门企业级语言,从最初的 synchronized 关键字,到强大的 java.util.concurrent (JUC) 包,为我们提供了一整套强大的并发工具。
本文将从线程的基本概念讲起,逐步深入到JUC的核心组件,旨在帮助你构建一个清晰、系统的Java并发知识体系。
一、核心概念:线程是什么?
在深入细节之前,我们先统一一下认知。
- 进程 vs 线程
- 进程:可以理解为一个独立的应用程序。例如,你同时打开的Chrome浏览器和IDEA开发工具就是两个进程。每个进程都有自己独立的内存空间,互不干扰。
- 线程:是进程中的执行单元,也称为“轻量级进程”。一个进程可以包含多个线程,所有线程共享进程的内存空间(如堆、方法区)。这就好比一个工厂(进程)里有多个流水线(线程),它们共享工厂的电力、原料仓库等资源。
- 上下文切换
单核CPU在同一时刻只能执行一个线程。为了让用户感觉多个线程在同时执行,CPU需要通过分配时间片来轮流执行各个线程。当一个线程的时间片用完或被高优先级线程抢占时,就需要保存当前线程的状态(如程序计数器、寄存器信息),然后加载另一个线程的状态,这个过程就是上下文切换。频繁的上下文切换会消耗大量资源。 - 线程的生命周期
线程从创建到销毁,会经历多种状态:
- NEW(新建):线程被创建,但尚未调用 start() 方法。
- RUNNABLE(可运行):调用了 start() 方法,线程已在JVM中,等待操作系统分配CPU时间片。它可能正在运行,也可能在就绪队列中等待。
- BLOCKED(阻塞):线程试图获取一个内部对象锁(非JUC中的锁),而该锁正被其他线程持有。
- WAITING(等待):线程进入等待状态,需要被其他线程显式地唤醒(如调用 Object.notify() 或 LockSupport.unpark())。
- TIMED_WAITING(超时等待):线程进入等待状态,但会在指定的时间后自动唤醒(如 Thread.sleep(long millis)、Object.wait(long timeout))。
- TERMINATED(终止):线程已执行完毕。
二、如何创建并运行一个线程?
Java提供了三种主要的创建线程的方式:
1.继承 Thread 类
重写 run() 方法,然后创建子类实例并调用其 start() 方法。
class MyThread extends Thread { @Override public void run() { System.out.println("线程运行中: " + Thread.currentThread().getName()); } } // 使用 MyThread thread = new MyThread(); thread.start(); // 注意:是start()而不是run(),run()只是普通方法调用
2.实现 Runnable 接口(更推荐)
实现 Runnable 接口的 run() 方法,然后将 Runnable 实例作为参数传递给 Thread 构造函数。
class MyRunnable implements Runnable { @Override public void run() { System.out.println("线程运行中: " + Thread.currentThread().getName()); } } // 使用 Thread thread = new Thread(new MyRunnable()); thread.start();
优点:避免了单继承的局限性,更适合资源共享。
3.实现 Callable 接口
Callable 与 Runnable 类似,但关键区别在于它有返回值,并且可以抛出异常。、
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; class MyCallable implements Callable<String> { @Override public String call() throws Exception { Thread.sleep(1000); return "任务执行结果"; } } // 使用 FutureTask<String> futureTask = new FutureTask<>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start(); // 获取返回值(会阻塞当前线程直到计算完成) String result = futureTask.get(); System.out.println(result);
三、线程安全:共享资源的“修罗场”
当多个线程共享同一份数据,并且至少有一个线程会对数据进行写操作时,如果不采取任何保护措施,就极易产生线程安全问题。
示例:一个经典的线程不安全案例
public class UnsafeCounter { private int count = 0; public void add() { count++; // count = count + 1; } public int get() { return count; } }
count++ 看似是一个操作,但实际上是一个“读取-修改-写入”的三步操作。在多线程环境下,可能会发生线程A刚读取完值,CPU就被线程B抢走,B也读取了同样的值并完成加1写入,随后A又用自己的旧值加1后写入,最终导致两次加法操作只生效了一次。
解决方案主要有以下几种:
1.synchronized关键字
synchronized 是Java提供的内置锁,用于保证代码块的互斥访问。同一时刻,只有一个线程能持有某个对象的锁,从而进入被synchronized保护的代码块。
- 同步代码块:需要显式指定锁对象。
- public void add() { synchronized (this) { // 以当前对象实例作为锁 count++; } }
- 同步实例方法:锁是当前对象实例 (this)。
- public synchronized void add() { // 锁是this count++; }
- 同步静态方法:锁是当前类的 Class 对象。
- public static synchronized void add() { // 锁是UnsafeCounter.class // ... }
2.volatile关键字
volatile 是一个轻量级的同步机制,它主要解决的是可见性和有序性问题,但不保证原子性。
- 可见性:当一个线程修改了 volatile 修饰的变量,新值会立即被刷新到主内存中。当其他线程需要读取这个变量时,它会从主内存重新读取新值,而不是使用自己工作内存中的旧值。
- 有序性:禁止指令重排序优化。
适用场景:通常用于标志位(如 boolean flag),一个线程写,多个线程读。
public class VolatileExample { private volatile boolean flag = false; // 使用volatile保证可见性 public void writer() { flag = true; // 写操作 } public void reader() { if (flag) { // 读操作,总能读到最新的值 // do something } } }
3.Lock接口 (如ReentrantLock)
java.util.concurrent.locks.Lock 接口提供了比 synchronized 更灵活、更强大的锁操作。
其实现类 ReentrantLock(可重入锁)是最常用的。
优势:
- 尝试非阻塞获取锁:tryLock()。
- 可中断的获取锁:lockInterruptibly(),等待锁的线程可以被中断。
- 超时获取锁:tryLock(long time, TimeUnit unit)。
- 支持公平锁:构造函数传入 true 可以创建一个公平锁(等待时间最长的线程优先获得锁),默认为非公平锁,吞吐量更高。
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SafeCounterWithLock { private int count = 0; private final Lock lock = new ReentrantLock(); // 创建Lock实例 public void add() { lock.lock(); // 获取锁 try { count++; // 临界区代码 } finally { lock.unlock(); // 必须在finally块中释放锁,防止异常导致死锁 } } }
四、JUC并发工具集(重中之重)
java.util.concurrent (JUC) 包提供了大量高效、实用的并发工具类,极大地简化了并发编程。
1.ExecutorService线程池
“线程池”顾名思义,就是预先创建好一批线程,放在一个“池子”里管理。有任务需要执行时,就从池子里拿一个空闲线程来执行,任务完成后线程不销毁,而是回到池中等待下一个任务。这避免了频繁创建和销毁线程的巨大开销。
核心实现类:ThreadPoolExecutor
理解其构造参数至关重要:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
参数名 |
含义 |
corePoolSize |
核心线程数。即使线程空闲,也不会被回收(除非设置了allowCoreThreadTimeOut)。 |
maximumPoolSize |
最大线程数。线程池能容纳的最大线程数。 |
keepAliveTime |
空闲线程存活时间。当线程数超过核心线程数时,多余的空闲线程在等待新任务的最长时间,超过则被回收。 |
unit |
keepAliveTime 的时间单位。 |
workQueue |
工作队列。用于保存等待执行的任务的阻塞队列。 |
threadFactory |
线程工厂。用于创建新线程,可以自定义线程名、优先级等。 |
handler |
拒绝策略。当线程池和队列都已满时,如何处理新提交的任务。 |
工作流程:
- 提交任务。
- 如果当前运行线程数 < corePoolSize,则创建新线程执行任务。
- 否则,将任务放入 workQueue。
- 如果队列已满,且运行线程数 < maximumPoolSize,则创建新线程执行任务。
- 如果队列已满,且运行线程数已达 maximumPoolSize,则触发拒绝策略。
通常不直接 new ThreadPoolExecutor,而是使用 Executors 工具类提供的工厂方法(注意:Executors 提供的某些方法可能有隐患,如无界队列可能导致OOM,需根据场景选择):
// 固定大小的线程池 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10); // 单线程的线程池 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); // 可缓存的线程池(线程数可弹性伸缩) ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 提交任务 future = executor.submit(myCallableTask); // 优雅关闭 executor.shutdown();
2. 并发集合 (Concurrent Collections)
传统的 HashMap, ArrayList 等集合类不是线程安全的。JUC提供了一系列高性能的线程安全集合。
- ConcurrentHashMap: 并发版的 HashMap。采用分段锁(JDK7)或 CAS + synchronized(JDK8及以后)实现高并发读写,性能远高于使用 Collections.synchronizedMap() 包装的HashMap。
- CopyOnWriteArrayList: 并发版的 ArrayList。写时复制——每次修改(增、删、改)操作时,都会复制底层数组,在新数组上操作,完成后将引用指向新数组。读操作完全无锁,性能极高。适用于读多写少的场景(如监听器列表)。
3. 同步辅助类
JUC提供了几个强大的工具类,来协调多个线程之间的控制流。
CountDownLatch(倒计时门闩)
允许一个或多个线程等待其他一组线程完成操作。
构造时传入一个计数器。等待的线程调用 await() 方法阻塞,其他线程完成工作后调用 countDown() 方法使计数器减1。当计数器减为0时,所有等待的线程被唤醒。
典型场景:主线程等待所有子线程完成任务后再继续。
// 模拟:主线程等待5个Worker线程完成任务 public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(5); // 计数器初始为5 for (int i = 0; i < 5; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + " 完成任务"); latch.countDown(); // 计数器减1 }, "Worker-" + i).start(); } latch.await(); // 主线程在此等待,直到计数器为0 System.out.println("所有Worker任务已完成,主线程继续执行"); } }
CyclicBarrier(循环栅栏)
让一组线程相互等待,直到所有线程都到达一个公共的屏障点,然后才能继续执行。计数器可以重置后重复使用。
构造时传入参与线程的数量和一个可选的 Runnable 任务(在所有线程到达屏障后执行)。每个线程调用 await() 方法通知屏障自己已到达,然后被阻塞。当最后一个线程到达后,屏障开放,所有被阻塞的线程继续执行,并可执行可选的屏障动作。
典型场景:多线程计算数据,最后合并计算结果。
// 模拟:3个士兵线程集合完毕后才能一起行动 public class CyclicBarrierDemo { public static void main(String[] args) { CyclicBarrier barrier = new CyclicBarrier(3, () -> { System.out.println("所有士兵已集合完毕,出发!"); // 屏障动作 }); for (int i = 0; i < 3; i++) { new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + " 到达集合点"); barrier.await(); // 等待其他士兵 // 屏障开放后,所有线程同时继续执行 System.out.println(Thread.currentThread().getName() + " 开始行动"); } catch (Exception e) { e.printStackTrace(); } }, "Soldier-" + i).start(); } } }
Semaphore(信号量)
用来控制同时访问特定资源的线程数量。它通过发放“许可”来管理。
构造时传入许可的数量。线程通过 acquire() 方法获取许可,如果许可已发完,则线程阻塞。使用完资源后,通过 release() 方法释放许可。
典型场景:数据库连接池、流量控制。
// 模拟:一个只有3个许可的厕所 public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3); // 3个许可 for (int i = 1; i <= 10; i++) { new Thread(() -> { try { semaphore.acquire(); // 获取许可 System.out.println(Thread.currentThread().getName() + " 占了一个坑位"); Thread.sleep(2000); // 模拟使用时间 System.out.println(Thread.currentThread().getName() + " 释放坑位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); // 释放许可 } }, "Person-" + i).start(); } } }
三者的简单区别:
- CountDownLatch: 一个线程等多个线程。(一次性)
- CyclicBarrier: 多个线程相互等。(可循环)
- Semaphore: 限制同时执行的线程数量。
五、原子类:无锁的线程安全
JUC提供了一系列原子类(如 AtomicInteger, AtomicLong, AtomicReference),它们通过无锁的方式实现了线程安全的原子操作。其核心原理是 CAS (Compare-And-Swap)。
CAS 是一种乐观锁机制。它包含三个操作数:
- 内存位置 (V)
- 期望的原值 (A)
- 新值 (B)
CAS的原理是:只有当 V 的值等于 A 时,才会用 B 去更新 V 的值;否则,什么都不做(或者重试)。整个操作是一个原子指令,由CPU保证其原子性。
import java.util.concurrent.atomic.AtomicInteger; public class AtomicCounter { private AtomicInteger count = new AtomicInteger(0); // 初始化原子整型 public void add() { count.incrementAndGet(); // 原子性的 ++i // 底层实现类似于: // int current; // do { // current = get(); // } while (!compareAndSet(current, current + 1)); } public int get() { return count.get(); } }
优点:性能通常比锁更高,因为避免了线程挂起和上下文切换。
缺点:存在 ABA问题(可以通过 AtomicStampedReference 加版本号解决),以及自旋循环可能长时间占用CPU。
总结与展望
本文系统性地梳理了Java线程与并发编程的核心知识:
- 基础:理解了线程、进程、生命周期等概念。
- 创建:掌握了三种创建线程的方式,推荐使用 Runnable 和 Callable。
- 安全:学会了使用 synchronized, volatile, Lock 来解决线程安全问题。
- 工具:深入学习了JUC的核心——线程池、并发集合和三大同步工具类 (CountDownLatch, CyclicBarrier, Semaphore)。
- 无锁:了解了原子类和CAS无锁编程的思想。
并发编程的世界远不止于此。要成为一名真正的并发专家,你还可以继续探索:
- 更底层的原理:JMM(Java内存模型)、happens-before原则、锁优化(自旋锁、锁消除、锁粗化、偏向锁、轻量级锁)。
- 更高级的工具:CompletableFuture(异步编程)、Fork/Join 框架(分治并行任务)。
- 问题排查:如何使用 jstack 等工具诊断死锁、活锁、资源耗尽等问题。
并发编程复杂但充满魅力,是区分Java程序员水平高低的重要标尺。希望本文能为你打下坚实的基础!