当前位置: 首页 > news >正文

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 后面的操作。
  • 注意:这只是逻辑上的顺序,实际执行时可能会因为指令重排序而改变。

例子:

int x = 1; // 操作1
int y = 2; // 操作2
// 操作1 Happens-Before 操作2

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

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

例子:

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 后续的加锁操作
  • 这意味着一个线程释放锁后,另一个线程获取锁时可以看到之前线程的所有操作结果。

例子:

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 该线程中的任何操作

例子:

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() 判断)。

例子:

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)。

例子:

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。

例子:

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中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

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

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位的操作执行。

例子:

// 原子性操作
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

例子:

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也可以通过禁止指令重排序来保证有序性。

例子:

// 使用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其他线程检测到该线程结束。

例子:

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线程是可见的。

相关文章:

  • 高效图像处理工具:从需求分析到落地实现
  • 【vue + JS】OCR图片识别、文字识别
  • react对比vue的核心属性
  • 2340单点修改、区间查询
  • 独立开发记录:使用Trae和Cloudflare快速搭建了自己的个人博客
  • 深度学习与大模型-矩阵
  • 解数独 (leetcode 37
  • 生化混合智能技术(Biochemical Hybrid Intelligence, BHI)解析与应用
  • devServer changeOrigin不管用
  • 101.在 Vue 3 + OpenLayers 使用 declutter 避免文字标签重叠
  • RTSP协议规范与SmartMediaKit播放器技术解析
  • 【Golang】第五弹----函数
  • go-文件缓存与锁
  • stm32 晶振换算
  • 【蔚蓝星球的节日】世界海洋日的探索与海洋的重要性
  • 【Rust基础】Rust后端开发常用库
  • ssm框架整合
  • 芯科科技推出的BG29超小型低功耗蓝牙®无线SoC,是蓝牙应用的理想之选
  • 哈尔滨算力服务器托管推荐-青蛙云
  • 利用DeepSeek搭建跨工作表数据的可视化分析动态面板
  • 35款移动应用存在违法违规收集使用个人信息情况,涉及智谱清言、Kimi等
  • 越秀地产约41.49亿元出售北京海淀功德寺项目公司65%股权,此前已质押给华润置地
  • 22国外长联合声明:要求以方立即允许全面恢复对加沙援助
  • 又有明星分析师晋升管理层:“白金分析师”武超则已任中信建投证券党委委员
  • 安徽凤阳通报鼓楼瓦片脱落:去年3月维修竣工,已成立调查组
  • 安徽凤阳通报鼓楼瓦片脱落:2023年曾维修,已成立调查组