Bootstrap

Java并发性质

一、简介

Java 多线程中的并发性质是确保多线程程序正确性和高效性的核心概念。这些性质主要包括 原子性、可见性 和 有序性。

1.1 原子性(Atomicity)
  • 定义:

    • 原子性是指一个操作或一系列操作是不可分割的,在执行过程中不会被其他线程中断或干扰。要么全部执行完成,要么全部不执行。
  • 问题来源:

    • 在多线程环境下,如果一个操作不是原子的,可能会导致数据不一致。例如:
      • i++;这个操作其实分成以下3步进行:
        • 读取i的值;
        • 将i的值加1;
        • 将加1后的值赋给i;
      • 如果多个线程同时执行 i++,可能会导致结果不符合预期。
    • 示例:
      public class AtomicityExample {
      	private static int count = 0;
      
      	public static void increment() {
      		count++;  // 这不是原子性操作
      	}
      
      	public static void main(String[] args) throws InterruptedException {
      		Thread thread1 = new Thread(() -> {
          		for (int i = 0; i < 1000; i++) {
              		increment();
          		}
      		});
      
      		Thread thread2 = new Thread(() -> {
          		for (int i = 0; i < 1000; i++) {
              		increment();
          		}
      		});
      
      		thread1.start();
      		thread2.start();
      
      		thread1.join();
      		thread2.join();
      
      		System.out.println("Count: " + count);
      	}
      }
      在这个例子中,count++操作实际上包含了三个步骤(读取、增加、写回),
      在多线程环境下不是原子性的。因此,最终的结果可能不是2000。
      
  • ‌解决方案:

    • 对于基本数据类型的变量,Java内存模型(JMM)保证了读取和赋值操作的原子性。
    • 对于更复杂的操作,可以使用synchronized关键字、java.util.concurrent.locks.Lock接口、以及java.util.concurrent.atomic包下的原子类(如AtomicInteger、AtomicLong等)来保证操作的原子性。
  • 示例:

    • 使用AtomicInteger保证操作的原子性。
      import java.util.concurrent.atomic.AtomicInteger;
      
      public class AtomicityExample {
      	private static AtomicInteger atomicCount = new AtomicInteger(0);
      
      	public static void increment() {
      		atomicCount.incrementAndGet();  // 原子性操作
      	}
      
      	public static void main(String[] args) throws InterruptedException {
      		Thread thread1 = new Thread(() -> {
          		for (int i = 0; i < 1000; i++) {
              		increment();
          		}
      		});
      
      		Thread thread2 = new Thread(() -> {
          		for (int i = 0; i < 1000; i++) {
              		increment();
          		}
      		});
      
      		thread1.start();
      		thread2.start();
      
      		thread1.join();
      		thread2.join();
      
      		System.out.println("Atomic Count: " + atomicCount.get());
      	}
      }
      incrementAndGet()方法是一个原子性操作,保证了计数的正确性。
      
1.2 可见性(Visibility)
  • 定义:

    • 可见性是指一个线程对共享变量的修改,其他线程能够立即看到修改后的值。
  • 问题来源

    • 为了提高CPU性能,会将数据缓存在寄存器或缓存中,而不是直接从主内存读取。这可能导致一个线程修改了共享变量的值,但其他线程仍然读取到旧值。
    • 示例:
      public class VisibilityExample {
      	private static boolean flag = false;
      
      	public static void write() {
      		flag = true;
      	}
      
      	public static void read() {
      		while (!flag) {
          		// 等待flag变为true
      		}
      		System.out.println("Flag is true");
      	}
      
      public static void main(String[] args) {
      	Thread thread1 = new Thread(VisibilityExample::write);
      	Thread thread2 = new Thread(VisibilityExample::read);
      
      	thread1.start();
      		try {
          		Thread.sleep(100);  // 等待thread1执行
      		} catch (InterruptedException e) {
          		e.printStackTrace();
      		}
      		thread2.start();
      	}
      }
      在这个例子中,thread1将flag设置为true,但thread2可能看不到这个改变,
      因为它可能一直在自己的工作内存中读取旧的flag值。
      
  • 解决方案:

    • 使用volatile关键字修饰变量,可以确保对该变量的修改能够立即被其他线程看到。
    • 使用 synchronized 关键字,确保线程在释放锁之前将变量的修改刷新到主内存。
    • 使用 final 关键字,确保在构造函数完成后,变量的值对所有线程可见。
  • 示例:

    • 使用volatile关键字保证可见性。
      public class VisibilityExample {
      	private static volatile boolean flag = false;
      
      	public static void write() {
      		flag = true;
      	}
      
      	public static void read() {
      		while (!flag) {
          		// 等待flag变为true
      		}
      		System.out.println("Flag is true");
      	}
      
      	public static void main(String[] args) {
      		Thread thread1 = new Thread(VisibilityExample::write);
      		Thread thread2 = new Thread(VisibilityExample::read);
      
      		thread1.start();
      		try {
          		Thread.sleep(100);  // 等待thread1执行
      		} catch (InterruptedException e) {
          		e.printStackTrace();
      		}
      		thread2.start();
      	}
      }
      这里volatile关键字确保了thread1对flag的修改对thread2是可见的。
      
1.3 有序性(Ordering)
  • 定义:

    • 有序性是指程序执行的顺序按照代码的先后顺序执行。在并发编程中,由于编译器优化和CPU指令重排序,可能会导致程序的执行顺序与代码的编写顺序不一致,从而影响程序的正确性。
  • 问题来源

    • 为了提高性能,编译器和处理器可能会对指令进行重排序,只要不影响单线程的执行结果。但在多线程环境下,重排序可能导致不可预期的结果。
    • 示例:
      int a = 0;
      boolean flag = false;
      
      // 线程 1
      a = 1;          // 语句 1
      flag = true;    // 语句 2
      
      // 线程 2
      if (flag) {     // 语句 3
      	int b = a;  // 语句 4
      }
      如果线程 1 的语句 1 和语句 2 被重排序,线程 2 可能会读取到 a 的旧值(0)。
      
  • 解决方案:

    • 使用 volatile 关键字,防止指令重排序。
    • 使用 synchronized 关键字,确保同一时刻只有一个线程执行代码块。
    • 使用 java.util.concurrent 包中的并发工具类(如 CountDownLatch、CyclicBarrier 等)。
  • 示例:

    • 使用volatile关键字或同步块。
      public class OrderingExample {
      	private static volatile int x = 0, y = 0;
      
      	public static void writer() {
      		x = 1;
      		y = 2;
      	}
      
      	public static void reader() {
      		int a = y;
      		int b = x;
      		System.out.println("x: " + b + ", y: " + a);
      	}
      
      	public static void main(String[] args) throws InterruptedException {
      		Thread thread1 = new Thread(OrderingExample::writer);
      		Thread thread2 = new Thread(OrderingExample::reader);
      
      		thread1.start();
      		thread1.join();
      		thread2.start();
      		thread2.join();
      	}
      }
      volatile关键字确保了writer线程对x和y的赋值操作不会被重排序,
      从而保证了reader线程看到正确的结果。
      

二、Java 内存模型

Java 内存模型(Java Memory Model, JMM) 是 Java 虚拟机(JVM)规范中定义的一种抽象模型,用于描述多线程环境下,线程如何与主内存(Main Memory)以及线程的本地内存(Local Memory,如缓存或寄存器)交互。

2.1 JMM 的核心概念

在这里插入图片描述

  1. 主内存与工作内存
    • 主内存(Main Memory):所有线程共享的内存区域,存储了所有的变量(实例字段、静态字段等)。
    • 工作内存(Working Memory):每个线程独享的内存区域,存储了线程使用的变量的副本。线程对变量的所有操作(读取、赋值等)都发生在工作内存中,不能直接操作主内存。
  2. 内存间的交互
    JMM 定义了线程如何将数据从主内存读取到工作内存,以及如何将修改后的数据写回主内存。具体操作包括:
    • read:从主内存读取变量到工作内存。
    • load:将读取的值放入工作内存的变量副本中。
    • use:线程使用工作内存中的变量值。
    • assign:线程将新值赋给工作内存中的变量。
    • store:将工作内存中的变量值写回主内存。
    • write:将 store 操作的值写入主内存的变量中。
2.2 Happens-Before 规则

Happens-Before 是 JMM 的核心规则,用于定义操作之间的可见性和有序性。如果操作 A Happens-Before 操作 B,那么操作 A 的结果对操作 B 可见,且操作 A 在操作 B 之前执行。常见的 Happens-Before 规则有如下:

  1. 程序顺序规则:在同一个线程中,前面的操作 Happens-Before 后面的操作。
  2. volatile 规则:对一个 volatile 变量的写操作 Happens-Before 后续对该变量的读操作。
  3. 锁规则:解锁操作 Happens-Before 后续的加锁操作。
  4. 线程启动规则:线程的 start() 方法 Happens-Before 该线程的任何操作。
  5. 线程终止规则:线程中的所有操作 Happens-Before 其他线程检测到该线程已经终止(如通过 join() 或 isAlive())。
  6. 传递性规则:如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
2.3 JMM 的实现机制
  1. volatile 关键字
    • 保证变量的可见性:每次读取 volatile 变量时,都会从主内存中读取;每次写入 volatile 变量时,都会立即刷新到主内存。
    • 禁止指令重排序:通过内存屏障(Memory Barrier)实现。
  2. synchronized 关键字
    • 保证原子性:同一时刻只有一个线程可以执行同步代码块。
    • 保证可见性:线程在释放锁之前,会将工作内存中的变量刷新到主内存。
    • 保证有序性:通过锁机制防止指令重排序。
  3. final 关键字
    • 保证可见性:在构造函数中完成对 final 变量的初始化后,其他线程可以看到正确的值。
    • 禁止指令重排序:防止构造函数中的指令重排序。
  4. 内存屏障(Memory Barrier)
    内存屏障是 JMM 实现有序性的底层机制,用于禁止特定类型的指令重排序。常见的屏障包括:
    • LoadLoad:确保前面的读操作在后面的读操作之前完成。
    • StoreStore:确保前面的写操作在后面的写操作之前完成。
    • LoadStore:确保前面的读操作在后面的写操作之前完成。
    • StoreLoad:确保前面的写操作在后面的读操作之前完成。

三、总结

性质定义问题来源解决方案
原子性操作不可分割非原子操作导致数据不一致synchronized、原子类、锁
可见性修改对其他线程可见缓存导致读取旧值volatile、synchronized、final
有序性代码执行顺序符合预期指令重排序导致意外结果volatile、synchronized
;