Java 并发编程核心常识
为什么需要并发编程?
- 充分利用多核 CPU:现代计算机大多是多核的,并发编程可以让程序的不同部分同时运行,从而提高 CPU 的利用率,加快程序执行速度。
- 提升程序响应性:对于有用户界面的程序或需要处理大量 I/O 操作(如网络请求、文件读写)的程序,使用多线程可以将耗时操作放到后台线程执行,避免主线程被阻塞,保持界面的流畅响应。
- 简化建模:某些现实世界的问题本身就是并发的(如多个客户端同时访问服务器),用并发模型来编写代码更符合直觉,更容易实现。
并发编程的三大核心问题
并发编程并非万能药,它会引入一系列复杂问题,其中最核心的三个是:
-
原子性:一个或多个操作,要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。
(图片来源网络,侵删)- 问题:最常见的例子是
i++操作,它实际上包含三个步骤:读取i的值、i加 1、写回i的值,在多线程环境下,如果两个线程同时读取到i的旧值,然后各自加 1,再写回,最终结果只增加了 1,而不是预期的 2。 - 解决方案:使用
synchronized关键字、java.util.concurrent.atomic包下的原子类(如AtomicInteger)。
- 问题:最常见的例子是
-
可见性:当一个线程修改了一个共享变量的值,其他线程能够立即得知这个修改。
- 问题:由于每个线程都有自己的工作内存(CPU 缓存),变量可能从主内存拷贝到线程的工作内存中进行修改,如果修改后的值没有及时写回主内存,其他线程就无法看到最新的值。
- 解决方案:
volatile关键字:保证了变量的可见性,禁止指令重排序。synchronized关键字:在解锁时,会将工作内存中的变量写回主内存;在加锁时,会清空工作内存,从主内存中重新加载变量。final关键字:被final修饰的字段,在构造函数完成之后,对其他所有线程都是可见的。
-
有序性:即程序执行的顺序按照代码的先后顺序执行。
- 问题:为了优化性能,编译器和处理器可能会对指令进行重排序,在单线程环境下,重排序不会影响最终结果;但在多线程环境下,重排序可能会破坏代码的逻辑。
- 解决方案:
volatile关键字:可以禁止指令重排序。synchronized关键字:一个变量在同一个时刻只允许一条线程对其进行锁定,这使得持有同一个锁的两个同步块只能串行地进入,从而保证了有序性。
synchronized关键字可以同时解决原子性、可见性和有序性问题。volatile关键字可以解决可见性和有序性问题,但不能保证原子性。
Java 并发基础工具
synchronized 关键字
这是 Java 最基础、最常用的同步机制。

- 作用:确保在同一时刻,只有一个线程可以执行被
synchronized修饰的代码块或方法。 - 三种用法:
- 实例方法:锁是当前实例对象(
this)。 - 静态方法:锁是当前类的
Class对象。 - 代码块:锁可以是任意对象,灵活性最高。
- 实例方法:锁是当前实例对象(
- 特点:
- 可重入:一个线程可以多次获取它已经持有的锁,这避免了死锁(一个线程在调用一个
synchronized方法时,可以调用同一个对象的另一个synchronized方法)。 - 非公平锁:默认情况下,
synchronized是非公平的,新来的线程可能直接抢占锁,而不是等待,这可能导致某些线程长时间等待(“饥饿”)。
- 可重入:一个线程可以多次获取它已经持有的锁,这避免了死锁(一个线程在调用一个
volatile 关键字
- 作用:保证变量的可见性和禁止指令重排序。
- 适用场景:当一个线程写一个变量,而其他线程读这个变量,并且不依赖当前值进行更复杂的原子操作时,最典型的场景是状态标记(如
boolean flag)。 - 不适用场景:不能用于
i++这样的复合操作,因为它不保证原子性。
java.util.concurrent.atomic 包
- 作用:提供了一系列原子变量类,如
AtomicInteger,AtomicLong,AtomicReference等。 - 原理:这些类内部使用 CAS (Compare-And-Swap) 操作,这是一种现代 CPU 都支持的硬件指令,可以实现无锁的原子操作。
- 优点:相比于
synchronized,CAS 通常能带来更高的性能,因为它避免了线程的阻塞和上下文切换。 - 缺点:
- ABA 问题:如果一个值从 A 变成 B,又变回 A,CAS 操作会认为它没有被修改过,可以通过在变量上附加一个“版本号”来解决(
AtomicStampedReference)。 - 自旋开销:CAS 失败,线程会不断地尝试(自旋),在竞争激烈的情况下,这会消耗大量 CPU 资源。
- ABA 问题:如果一个值从 A 变成 B,又变回 A,CAS 操作会认为它没有被修改过,可以通过在变量上附加一个“版本号”来解决(
高级并发工具
java.util.concurrent.locks 包
提供了比 synchronized 更强大、更灵活的锁机制。
-
ReentrantLock(可重入锁):- 功能与
synchronized类似,但提供了更多功能。 - 公平性:可以设置为公平锁(FIFO),线程会按照请求的顺序获取锁。
- 锁超时:可以尝试获取锁,并在指定时间内失败,而不是无限等待。
- 可中断:可以响应中断,在等待锁的过程中可以被其他线程中断。
- 多个条件变量:一个
ReentrantLock可以绑定多个Condition,实现更精确的线程唤醒控制。
- 功能与
-
ReadWriteLock(读写锁):- 将锁分为读锁和写锁。
- 规则:多个读锁可以同时持有,但写锁是独占的。
- 适用场景:读多写少的场景,可以大大提高并发性能。
线程池 (java.util.concurrent.ExecutorService)
创建和管理线程本身是昂贵的,线程池可以复用已创建的线程,避免频繁创建和销毁线程的开销。

- 核心类:
ThreadPoolExecutor。 - 创建方式:推荐使用
Executors工厂类,但要注意其局限性(如FixedThreadPool和CachedThreadPool可能导致资源耗尽,生产环境更推荐手动配置ThreadPoolExecutor)。 - 关键参数:
corePoolSize:核心线程数。maximumPoolSize:最大线程数。workQueue:工作队列,用于存放等待执行的任务。keepAliveTime:线程空闲时间,超过此时间将被回收。
- 拒绝策略:当工作队列已满且线程数达到最大值时,新提交的任务会被拒绝。
RejectedExecutionHandler提供了四种默认策略(如AbortPolicy直接抛出异常)。
并发集合 (java.util.concurrent 包下的集合)
ConcurrentHashMap:线程安全的HashMap,在 Java 8 中,它通过 CAS + synchronized 实现,分段锁的概念被弱化,并发性能更高。CopyOnWriteArrayList/CopyOnWriteArraySet:写时复制集合,任何修改操作(add, set)都会复制整个底层数组,读操作不加锁,性能极高。- 适用场景:读远多于写,且集合大小不大的场景。
BlockingQueue:阻塞队列,是生产者-消费者模式的最佳实践,当队列满时,生产者会阻塞;当队列空时,消费者会阻塞。- 常用实现:
ArrayBlockingQueue(有界),LinkedBlockingQueue(可界),SynchronousQueue(不存储元素)。
- 常用实现:
CountDownLatch, CyclicBarrier, Semaphore
CountDownLatch(倒计时门闩):允许一个或多个线程等待其他一组线程完成操作,是一次性的。- 场景:主线程等待多个子任务全部完成后继续执行。
CyclicBarrier(循环栅栏):让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,所有线程才会继续执行,是可循环使用的。- 场景:多个线程分阶段执行任务,每个阶段都需要所有线程都到达后才能进入下一阶段。
Semaphore(信号量):控制同时访问特定资源的线程数量。- 场景:资源有限的场景,如数据库连接池、停车场车位管理。
线程的生命周期
- NEW (新建):线程被创建,但尚未启动。
- RUNNABLE (运行):Java 将
READY(就绪) 和RUNNING(运行) 状态统称为RUNNABLE,线程已启动,正在等待 CPU 时间片或正在执行。 - BLOCKED (阻塞):线程因为等待获取锁而进入阻塞状态。
- WAITING (等待):线程等待另一个线程执行特定操作(如
wait(),join(),LockSupport.park()),这种状态需要被显式唤醒。 - TIMED_WAITING (超时等待):和
WAITING类似,但它可以在指定时间后自动唤醒(如sleep(time),wait(time))。 - TERMINATED (终止):线程执行完毕或因异常退出。
最佳实践和常见陷阱
- 避免过度同步:同步会带来性能开销,只对真正需要共享和修改的数据进行同步。
- 优先使用
java.util.concurrent包:这些工具经过精心设计和测试,比自己用synchronized手动实现更可靠、更高效。 - 注意线程安全:不要将可变对象(特别是集合类)不加保护地共享。
ArrayList,HashMap不是线程安全的。 - 避免死锁:死锁发生的四个必要条件(互斥、持有并等待、不可剥夺、循环等待),破坏其中一个即可避免,常见的预防方法:按固定顺序获取锁,使用
tryLock设置超时。 - 谨慎使用
Executors创建线程池:了解FixedThreadPool和CachedThreadPool的潜在风险,优先使用ThreadPoolExecutor并合理配置参数。 - 使用
ThreadLocal谨慎:ThreadLocal用于创建线程隔离的变量,可以避免多线程问题,但它可能导致内存泄漏,因为每个线程都有自己的副本,如果线程不被销毁(如线程池中的线程),这些副本会一直存在,使用后记得调用remove()方法。 - 优先使用不可变对象:不可变对象天生是线程安全的,因为它们的状态创建后就不能被修改。
