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

相关推荐
陈小桔9 分钟前
idea中重新加载所有maven项目失败,但maven compile成功
java·maven
小学鸡!10 分钟前
Spring Boot实现日志链路追踪
java·spring boot·后端
xiaogg367822 分钟前
阿里云k8s1.33部署yaml和dockerfile配置文件
java·linux·kubernetes
逆光的July38 分钟前
Hikari连接池
java
微风粼粼1 小时前
eclipse 导入javaweb项目,以及配置教程(傻瓜式教学)
java·ide·eclipse
番茄Salad1 小时前
Spring Boot临时解决循环依赖注入问题
java·spring boot·spring cloud
天若有情6731 小时前
Spring MVC文件上传与下载全面详解:从原理到实战
java·spring·mvc·springmvc·javaee·multipart
祈祷苍天赐我java之术1 小时前
Redis 数据类型与使用场景
java·开发语言·前端·redis·分布式·spring·bootstrap
Olrookie2 小时前
若依前后端分离版学习笔记(二十)——实现滑块验证码(vue3)
java·前端·笔记·后端·学习·vue·ruoyi
倚栏听风雨3 小时前
java.lang.SecurityException异常
java