第1章

⚡ 并发编程基础

理解并发编程的基本概念、Java内存模型和线程安全问题的根源

学习目标

什么是并发编程

并发编程(Concurrent Programming)是指在同一时间段内执行多个任务的编程方式。在现代计算机系统中,并发编程已经成为提高程序性能和响应能力的重要手段。

核心理解

并发编程不仅仅是技术层面的多线程,更是一种解决复杂问题的思维方式,它能够充分利用现代多核处理器的计算能力。

并发编程的应用场景

Web服务器
同时处理多个用户请求,提高服务器的吞吐量和响应速度。
桌面应用
UI线程与后台任务分离,保证用户界面的流畅性。
数据处理
并行处理大量数据,提高数据分析和计算的效率。

并发编程的优势

并发 vs 并行

并发(Concurrency)和并行(Parallelism)是两个容易混淆的概念,理解它们的区别对于掌握并发编程至关重要。

并发(Concurrency)

同一时间段内处理多个任务,但不一定同时执行。通过时间片轮转等方式在单核CPU上实现。

  • 逻辑上的同时进行
  • 可以在单核CPU上实现
  • 关注任务的组织和调度
并行(Parallelism)

同一时刻真正同时执行多个任务,需要多核CPU或多台机器的支持。

  • 物理上的同时进行
  • 需要多核CPU支持
  • 关注任务的同时执行
重要区别

并发是关于处理多个任务的能力,而并行是关于同时执行多个任务的能力。并发可以在单核上实现,但并行必须要有多核支持。

Java内存模型(JMM)

Java内存模型(Java Memory Model,JMM)定义了Java程序中各种变量的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。

内存模型结构

主内存(Main Memory)

所有线程共享的内存区域,存储所有变量的主副本。

  • 存储实例字段、静态字段
  • 所有线程都能访问
  • 变量的唯一主副本
工作内存(Working Memory)

每个线程私有的内存区域,存储主内存中变量的副本。

  • 线程私有,相互隔离
  • 存储变量的副本
  • 直接操作工作内存

内存交互操作

JMM定义了8种内存交互操作,用于控制主内存与工作内存之间的数据传输:

线程安全问题

当多个线程同时访问共享资源时,如果没有适当的同步机制,就可能出现线程安全问题。这些问题的根源在于并发访问共享数据时的竞态条件。

经典案例:不安全的计数器

UnsafeCounter.java
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内存模型正是围绕这三个特性来设计的。

可见性(Visibility)

当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。

可见性问题示例
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!");
    }
}
原子性(Atomicity)

一个操作或多个操作要么全部执行完成,要么全部不执行,不会被其他线程打断。

原子性问题示例
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(); // 原子操作
    }
}
有序性(Ordering)

程序执行的顺序按照代码的先后顺序执行,但编译器和处理器可能会进行指令重排序优化。

有序性问题示例
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接口、原子类等。我们将在后续章节详细学习这些解决方案。

返回目录 下一章:线程基础