⚡ 并发编程基础
理解并发编程的基本概念、Java内存模型和线程安全问题的根源
学习目标
- 理解并发编程的基本概念和重要性
- 掌握并发与并行的区别
- 了解Java内存模型(JMM)
- 认识线程安全问题的根源
- 理解可见性、原子性、有序性三大特性
什么是并发编程
并发编程(Concurrent Programming)是指在同一时间段内执行多个任务的编程方式。在现代计算机系统中,并发编程已经成为提高程序性能和响应能力的重要手段。
并发编程不仅仅是技术层面的多线程,更是一种解决复杂问题的思维方式,它能够充分利用现代多核处理器的计算能力。
并发编程的应用场景
并发编程的优势
- 提高性能:充分利用多核CPU的计算能力
- 改善响应性:避免程序因长时间操作而卡顿
- 增强吞吐量:同时处理更多的任务和请求
- 资源利用率:更好地利用系统资源
并发 vs 并行
并发(Concurrency)和并行(Parallelism)是两个容易混淆的概念,理解它们的区别对于掌握并发编程至关重要。
同一时间段内处理多个任务,但不一定同时执行。通过时间片轮转等方式在单核CPU上实现。
- 逻辑上的同时进行
- 可以在单核CPU上实现
- 关注任务的组织和调度
同一时刻真正同时执行多个任务,需要多核CPU或多台机器的支持。
- 物理上的同时进行
- 需要多核CPU支持
- 关注任务的同时执行
并发是关于处理多个任务的能力,而并行是关于同时执行多个任务的能力。并发可以在单核上实现,但并行必须要有多核支持。
Java内存模型(JMM)
Java内存模型(Java Memory Model,JMM)定义了Java程序中各种变量的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
内存模型结构
所有线程共享的内存区域,存储所有变量的主副本。
- 存储实例字段、静态字段
- 所有线程都能访问
- 变量的唯一主副本
每个线程私有的内存区域,存储主内存中变量的副本。
- 线程私有,相互隔离
- 存储变量的副本
- 直接操作工作内存
内存交互操作
JMM定义了8种内存交互操作,用于控制主内存与工作内存之间的数据传输:
- lock(锁定):作用于主内存变量,标识变量为线程独占
- unlock(解锁):作用于主内存变量,释放锁定状态
- read(读取):从主内存读取变量值到工作内存
- load(载入):将read的值放入工作内存的变量副本
- use(使用):将工作内存变量值传递给执行引擎
- assign(赋值):将执行引擎的值赋给工作内存变量
- store(存储):将工作内存变量值传送到主内存
- write(写入):将store的值写入主内存变量
线程安全问题
当多个线程同时访问共享资源时,如果没有适当的同步机制,就可能出现线程安全问题。这些问题的根源在于并发访问共享数据时的竞态条件。
经典案例:不安全的计数器
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // 这不是原子操作!
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
// 创建1000个线程,每个线程执行1000次increment
Thread[] threads = new Thread[1000];
for (int i = 0; i < 1000; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
});
threads[i].start();
}
// 等待所有线程完成
for (Thread thread : threads) {
thread.join();
}
// 期望结果:1000000,实际结果:可能小于1000000
System.out.println("最终计数: " + counter.getCount());
}
}
count++操作实际包含三个步骤:读取count值、将值加1、写回count。在多线程环境下,这三个步骤可能被其他线程打断,导致数据不一致。
常见的线程安全问题
可见性、原子性、有序性
并发编程中的三大特性是理解线程安全问题的关键。Java内存模型正是围绕这三个特性来设计的。
当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。
public class VisibilityDemo {
private boolean flag = false;
public void writer() {
flag = true; // 线程1修改
}
public void reader() {
while (!flag) {
// 线程2可能永远看不到flag的修改
}
System.out.println("Flag is true!");
}
}
一个操作或多个操作要么全部执行完成,要么全部不执行,不会被其他线程打断。
public class AtomicityDemo {
private int count = 0;
public void increment() {
// 非原子操作:读取 -> 修改 -> 写入
count = count + 1;
}
// 使用AtomicInteger保证原子性
private AtomicInteger atomicCount = new AtomicInteger(0);
public void atomicIncrement() {
atomicCount.incrementAndGet(); // 原子操作
}
}
程序执行的顺序按照代码的先后顺序执行,但编译器和处理器可能会进行指令重排序优化。
public class OrderingDemo {
private int a = 0;
private boolean flag = false;
public void writer() {
a = 1; // 操作1
flag = true; // 操作2
// 可能被重排序为:flag = true; a = 1;
}
public void reader() {
if (flag) { // 操作3
int i = a; // 操作4,可能读到a=0
}
}
}
Java提供了多种机制来保证这三个特性:synchronized关键字、volatile关键字、Lock接口、原子类等。我们将在后续章节详细学习这些解决方案。