详解线程的三大特性:原子性、可见性和有序性

在多线程编程中,理解线程的 原子性可见性有序性 是构建正确并发程序的基础。以下是它们的详细解释:


1. 原子性 (Atomicity)

定义 原子性指的是操作不可被中断,要么全部执行完成,要么完全不执行。

特性

  • 原子性操作在执行时不会被其他线程干扰。

  • 如果多个线程同时访问共享资源,原子性可以防止数据的不一致。

Java 中的原子性

  • 原子性操作示例:

    • 读取和写入基本数据类型(如 intfloat)是原子性的。

    • volatile 变量的读取/写入是原子性的(不适用于复合操作)。

  • 非原子性操作:

    • 复合操作(如 counter++counter = counter + 1)是非原子性的。这些操作实际上包括三步:读取变量值、修改值、写回变量。

解决方案

  • 使用 同步机制(如 synchronized 块或方法):

    synchronized (lock) {
        counter++;
    }
  • 使用 原子类(如 AtomicInteger):

    AtomicInteger counter = new AtomicInteger();
    counter.incrementAndGet();
    

      


2. 可见性 (Visibility)

定义 可见性指的是一个线程对共享变量的修改对其他线程是可见的。

特性

  • 在多线程环境中,如果没有同步机制,一个线程对变量的修改可能不会立刻被其他线程看到(由于 CPU 缓存或编译优化)。

  • 线程可能会一直使用自己 CPU 缓存中的值,而看不到其他线程更新后的值。

Java 中的可见性

  • 存在可见性问题的场景:

    private static boolean flag = false;
    ​
    public static void main(String[] args) {
        new Thread(() -> {
            while (!flag) {
                // do something
            }
            System.out.println("Thread ended.");
        }).start();
    ​
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    ​
        flag = true; // 主线程修改了 flag,但子线程可能看不到
    }
  • 解决方案:

    1. 使用 volatile 关键字:

      private static volatile boolean flag = false;
      • volatile 确保变量的修改对所有线程立即可见。

    2. 使用 同步机制(如 synchronized 或显式锁 Lock),因为同步也能保证可见性。


3. 有序性 (Ordering)

定义 有序性指的是程序代码的执行顺序,通常来说,程序会按照代码编写的顺序执行,但编译器和处理器会为了优化性能进行 指令重排

特性

  • 在单线程环境中,指令重排不会影响程序的正确性。

  • 在多线程环境中,指令重排可能导致意想不到的结果,因为线程之间的执行顺序无法预测。

指令重排的例子

int a = 1; // (1)
int b = 2; // (2)
int c = a + b; // (3)

在单线程中,执行顺序是 (1) -> (2) -> (3),但由于重排优化,CPU 可能将 (2)(1) 的顺序交换。尽管结果在单线程环境中是正确的,但多线程中可能导致数据问题。

Java 中的有序性

  • 可能出现问题的场景:

    • 使用 volatile 可以禁止指令重排:

      package com.example.demopool;
      
      import org.springframework.stereotype.Component;
      
      /**
       * @Author: cv master
       * @Date: 2024/11/15 09:17
       */
      
      @Component
      public class B {
          private boolean flag = false;
          private int a = 0;
      
          public void writer() {
              a = 1;       // 写变量
              flag = true; // 通知其他线程
          }
      
          public void reader() {
              if (flag) { // 读标志
                  System.out.println(a); // 此时 a 的值一定是 1
              }
          }
      
          public static void main(String[] args) throws InterruptedException {
              B b = new B();
              Thread thread1 = new Thread(b::writer);
              Thread thread2 = new Thread(b::reader);
              thread1.start();
              thread2.start();
              thread1.join();
              thread2.join();
          }
      }
    • 在上述代码中,volatile 保证了写操作的顺序,使得 a = 1 一定发生在 flag = true 之前。

解决方案

  • 使用 volatile:确保关键变量的修改不会被指令重排。

  • 使用 synchronized显式锁:同步代码块可以强制线程按照指定顺序执行。


三者关系与 happens-before 原则

  • 原子性可见性 是独立的,但有时需要结合使用才能实现正确的多线程行为。

  • 有序性 通常需要通过 volatile 或同步机制来确保。

  • happens-before 是 Java 内存模型中定义的一种原则,用于规定线程间的操作顺序:

    • 一个线程对变量的写操作对另一个线程的读操作可见,必须满足 happens-before 原则。

    • 如:synchronizedvolatile、线程启动/终止等操作会建立 happens-before 关系。


总结

特性描述解决方法
原子性 操作不可中断,要么全部执行成功,要么完全不执行。 使用 synchronized 或原子类如 AtomicInteger
可见性 一个线程的修改对其他线程立刻可见。 使用 volatile 或同步机制如 synchronized
有序性 程序执行顺序符合预期,避免指令重排导致问题。 使用 volatile、同步机制(synchronized 或锁)。
posted @ 2024-11-21 09:37  luorx  阅读(8)  评论(0编辑  收藏  举报