返回

多线程剖析之内存模型、原子性和可见性

Android

前言

在多线程编程中,我们经常会遇到一些看似奇怪的现象,比如:

  • 多个线程同时对一个共享变量进行修改,导致数据不一致。
  • 一个线程修改了共享变量,但另一个线程却看不到这个修改。

要理解这些现象产生的原因,就必须理解 Java 线程内存模型。

Java 线程内存模型

Java 线程内存模型(Java Memory Model,JMM)定义了线程如何访问共享变量的规则。JMM 规定,每个线程都有自己的私有内存,称为线程私有内存(Thread-Local Memory,TLM)。当一个线程修改一个共享变量时,这个修改只会发生在该线程的 TLM 中。其他线程无法直接访问该线程的 TLM,因此它们看不到这个修改。

为了让其他线程能够看到这个修改,需要将这个修改从 TLM 复制到主内存(Main Memory,MM)中。主内存是所有线程共享的内存区域,因此其他线程可以从主内存中读取这个修改。

JMM 还定义了两种类型的变量:

  • 共享变量: 可以在多个线程中访问的变量。
  • 局部变量: 只能在声明它的线程中访问的变量。

局部变量存储在 TLM 中,共享变量存储在主内存中。

原子性

原子性(Atomicity)是指一个操作要么完全执行,要么完全不执行,不存在中间状态。在 Java 中,原子性由原子变量(Atomic Variable)来保证。原子变量是通过特殊的硬件指令来实现的,这些指令可以确保一个操作在执行过程中不会被其他线程中断。

Java 中提供了原子变量类,例如 AtomicInteger、AtomicLong 等。这些类提供了原子性的增减、比较和交换操作。

可见性

可见性(Visibility)是指一个线程修改了共享变量,其他线程能够看到这个修改。在 Java 中,可见性由 volatile 来保证。volatile 关键字可以确保一个变量在被修改后立即被写入主内存,并且其他线程可以立即从主内存中读取这个修改。

volatile 关键字还可以防止指令重排序。指令重排序是指编译器和处理器可以改变指令的执行顺序,以提高程序的性能。但是,指令重排序可能会导致共享变量的修改对其他线程不可见。volatile 关键字可以防止指令重排序,从而确保共享变量的修改对其他线程始终可见。

举个例子

为了更好地理解 Java 线程内存模型、原子性和可见性,我们来看一个例子。

public class SharedCounter {

    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

这个类定义了一个共享变量 count,以及两个方法 increment() 和 getCount()。increment() 方法对 count 进行递增操作,getCount() 方法返回 count 的值。

现在,我们创建两个线程,每个线程都调用 increment() 方法 10000 次,然后调用 getCount() 方法获取 count 的值。

public class Main {

    public static void main(String[] args) {
        SharedCounter counter = new SharedCounter();

        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(counter.getCount());
    }
}

运行这个程序,我们会发现 count 的值可能不是 20000,而是小于 20000。这是因为 increment() 方法和 getCount() 方法都不是原子的,因此可能出现一个线程修改了 count,但另一个线程却看不到这个修改的情况。

为了解决这个问题,我们可以使用原子变量来保证 increment() 方法和 getCount() 方法的原子性。

public class SharedCounter {

    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}

现在,我们再次运行这个程序,我们会发现 count 的值始终是 20000。这是因为 AtomicInteger 类提供了原子性的增减和获取操作,因此可以保证 increment() 方法和 getCount() 方法的原子性。

结论

Java 线程内存模型、原子性和可见性是多线程编程中非常重要的概念。理解这些概念可以帮助我们编写出更加健壮和安全的并发程序。