Java是怎么解决并发问题的?

Happens-Before规则(前言)

Happens-Before规则 是 Java 内存模型(JMM)中用于定义线程间操作可见性和有序性的一种规范。它的核心目的是:确保一个线程的某些操作结果对其他线程是可见的,并且这些操作在时间上的顺序不会被重排序破坏

简单来说,如果操作 A Happens-Before 操作 B,那么操作 A 的结果对操作 B 是可见的,并且操作 A 在逻辑上先于操作 B 发生。


Happens-Before的核心作用

  1. 保证可见性
    • 确保一个线程对共享变量的修改对其他线程可见。
  2. 保证有序性
    • 防止编译器或处理器对指令进行重排序,从而破坏程序的逻辑顺序。

Happens-Before规则的具体内容

以下是 Java 内存模型中定义的 Happens-Before 规则:

1. 程序顺序规则(Program Order Rule)

  • 在同一个线程内,按照代码的书写顺序,前面的操作 Happens-Before 后面的操作。
  • 注意:这只是逻辑上的顺序,实际执行时可能会因为指令重排序而改变。

例子:

java 复制代码
int x = 1; // 操作1
int y = 2; // 操作2
// 操作1 Happens-Before 操作2

2. volatile变量规则(Volatile Variable Rule)

  • 对一个 volatile 变量的写操作 Happens-Before 后续对该变量的读操作
  • 这是 volatile 关键字的核心特性之一:它不仅能保证可见性,还能禁止指令重排序

例子:

java 复制代码
class VolatileExample {
    private volatile boolean flag = false;

    public void writer() {
        flag = true; // 写操作
    }

    public void reader() {
        if (flag) {  // 读操作
            System.out.println("Flag is true");
        }
    }
}
// 写操作 Happens-Before 读操作

3. 锁规则(Lock Rule)

  • 解锁操作 Happens-Before 后续的加锁操作
  • 这意味着一个线程释放锁后,另一个线程获取锁时可以看到之前线程的所有操作结果。

例子:

java 复制代码
class LockExample {
    private int x = 0;
    private final Object lock = new Object();

    public void writer() {
        synchronized (lock) {
            x = 42; // 写操作
        } // 解锁操作
    }

    public void reader() {
        synchronized (lock) {
            System.out.println(x); // 读操作
        } // 加锁操作
    }
}
// 解锁操作 Happens-Before 加锁操作

4. 线程启动规则(Thread Start Rule)

  • 线程的 start() 方法调用 Happens-Before 该线程中的任何操作

例子:

java 复制代码
class ThreadStartExample {
    private int x = 0;

    public void startThread() {
        x = 42; // 操作1
        Thread t = new Thread(() -> {
            System.out.println(x); // 操作2
        });
        t.start();
    }
}
// 操作1 Happens-Before 操作2

5. 线程终止规则(Thread Termination Rule)

  • 一个线程的所有操作 Happens-Before 其他线程检测到该线程已经终止(如通过 Thread.join()Thread.isAlive() 判断)。

例子:

java 复制代码
class ThreadTerminationExample {
    private int x = 0;

    public void runThreads() throws InterruptedException {
        Thread t = new Thread(() -> {
            x = 42; // 操作1
        });
        t.start();
        t.join(); // 等待线程结束
        System.out.println(x); // 操作2
    }
}
// 操作1 Happens-Before 操作2

6. 中断规则(Interruption Rule)

  • 调用线程的 interrupt() 方法 Happens-Before 被中断线程检测到中断事件(如调用 isInterrupted() 或捕获 InterruptedException)。

例子:

java 复制代码
class InterruptExample {
    public void interruptThread(Thread t) {
        t.interrupt(); // 操作1
    }

    public void checkInterrupt(Thread t) {
        if (t.isInterrupted()) { // 操作2
            System.out.println("Thread is interrupted");
        }
    }
}
// 操作1 Happens-Before 操作2

7. 传递性规则(Transitivity Rule)

  • 如果操作 A Happens-Before 操作 B,且操作 B Happens-Before 操作 C,那么操作 A Happens-Before 操作 C。

例子:

java 复制代码
class TransitivityExample {
    private int x = 0;
    private volatile boolean flag = false;

    public void writer() {
        x = 42;           // 操作A
        flag = true;      // 操作B
    }

    public void reader() {
        if (flag) {       // 操作C
            System.out.println(x); // 操作D
        }
    }
}
// 操作A Happens-Before 操作B,操作B Happens-Before 操作C
// 因此,操作A Happens-Before 操作D

关键点:

  1. Happens-Before 并不一定是时间上的先后顺序,而是逻辑上的顺序。
  2. 它不仅保证了可见性,还防止了指令重排序导致的问题。
  3. 常见的 Happens-Before 规则包括程序顺序规则、volatile规则、锁规则、线程启动规则等。

解决并发问题 理解的第一个维度:

核心知识点JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需 禁用缓存编译优化的方法。

具体来说,这些方法包括:

volatile、synchronized 和 final 三个关键字

Happens-Before 规则

解决并发问题 理解的第二个维度:可见性,有序性,原子性

  • 原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

请分析以下哪些操作是原子性操作:

java 复制代码
x = 10;        //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
y = x;         //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
x++;           //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。
x = x + 1;     //语句4: 同语句3

上面4个语句只有语句1的操作具备原子性。也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

  • 基本数据类型中,除了longdouble外,其他类型(如intshortbytecharbooleanfloat)的读写操作在JVM中通常是原子的。
  • longdouble类型的读写操作可能不是原子的,因为它们在某些平台上可能会被拆分为两个32位的操作执行。

例子:

java 复制代码
// 原子性操作
int x = 10; // 原子操作
x = 20;     // 原子操作

// 非原子性操作(可能)
long y = 123456789L; // 在某些平台上可能不是原子操作

从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。

由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

  • 可见性
    Java提供了volatile关键字来保证可见性。
    当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存(volatile的作用是通过禁止指令重排序确保缓存一致性协议 (如MESI)来实现可见性),当有其他线程需要读取时,它会去内存中读取新值。
    而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

注意: volatile并不能完全替代synchronizedLock,因为它只保证可见性和有序性,但不保证复合操作的原子性。

  • volatile通过缓存一致性协议(如MESI)确保一个线程对共享变量的修改对其他线程立即可见。
  • volatile适用于单个变量 的可见性问题,但如果涉及多个变量或复合操作,仍然需要使用synchronizedLock

例子:

java 复制代码
class VolatileExample {
    private volatile boolean flag = true;

    public void stop() {
        flag = false; // 修改flag,其他线程会立即看到
    }

    public void run() {
        while (flag) {
            // 执行任务
        }
    }
}
  • 有序性
    在Java里面,可以通过volatile关键字来保证一定的"有序性"。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的
  • synchronizedLock通过引入内存屏障(Memory Barrier)来防止指令重排序,从而间接保证有序性。
  • volatile也可以通过禁止指令重排序来保证有序性。

例子:

java 复制代码
// 使用volatile保证有序性
class VolatileOrderExample {
    private volatile boolean initialized = false;
    private int value = 0;

    public void init() {
        value = 42;          // 操作1
        initialized = true;  // 操作2,volatile保证不会被重排序到操作1之前
    }

    public void use() {
        if (initialized) {   // volatile保证可见性和有序性
            System.out.println(value);
        }
    }
}

Happens-Before规则不仅用于保证有序性,还用于确保可见性。

常见的Happens-Before规则包括:

  1. 程序顺序规则:同一个线程内,前面的操作Happens-Before后面的操作。
  2. 锁规则:解锁操作Happens-Before后续的加锁操作。
  3. volatile规则:对volatile变量的写操作Happens-Before后续的读操作。
  4. 线程启动规则:线程启动操作Happens-Before该线程内的任何操作。
  5. 线程终止规则:线程内的任何操作Happens-Before其他线程检测到该线程结束。

例子:

java 复制代码
class HappensBeforeExample {
    private int x = 0;
    private volatile boolean flag = false;

    public void writer() {
        x = 42;           // 操作1
        flag = true;      // 操作2,volatile写
    }

    public void reader() {
        if (flag) {       // 操作3,volatile读
            System.out.println(x); // 操作4,一定能打印42
        }
    }
}

在这个例子中,volatile变量flag的写操作Happens-Before其读操作,因此x=42的结果对reader线程是可见的。

相关推荐
此木|西贝2 小时前
【设计模式】享元模式
java·设计模式·享元模式
李少兄3 小时前
解决Spring Boot多模块自动配置失效问题
java·spring boot·后端
bxlj_jcj4 小时前
JVM性能优化之年轻代参数设置
java·性能优化
八股文领域大手子4 小时前
深入理解缓存淘汰策略:LRU 与 LFU 算法详解及 Java 实现
java·数据库·算法·缓存·mybatis·哈希算法
不当菜虚困4 小时前
JAVA设计模式——(八)单例模式
java·单例模式·设计模式
m0_740154674 小时前
Maven概述
java·maven
吗喽对你问好4 小时前
Java位运算符大全
java·开发语言·位运算
Java致死5 小时前
工厂设计模式
java·设计模式·简单工厂模式·工厂方法模式·抽象工厂模式
程序员JerrySUN5 小时前
驱动开发硬核特训 · Day 21(上篇) 抽象理解 Linux 子系统:内核工程师的视角
java·linux·驱动开发