JUC并发编程(五)volatile/可见性/原子性/有序性->JMM(Java内存模型)

目录

 一 可见性

二 原子性

三 有序性

四 volatile原理

1 如何保证可见性

2 如何保证有序性

3 无法解决指令交错(无法保证原子性)

4 双重检查锁(double-checked locking)

5 happens-before


总体概括

特性含义如何保障
原子性一个或多个操作要么全部执行成功,要么全部失败synchronizedAtomicInteger 等
可见性一个线程对共享变量的修改,其他线程能看到volatilesynchronizedfinal
有序性程序执行顺序与代码顺序一致volatilesynchronizedLock

 一 可见性

1 问题

代码展示:

package day01.neicun;

public class example1 {
    static boolean flag = true;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag) {

            }
        });

        t1.start();
        // 这里释放休眠,防止t1线程还未执行就被主线程给修改变量
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;

    }
}

2 解决

  • 1 volatile易变关键字

它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存当中查找变量的值,必须到主存当中获取他的值,线程操作volatile变量都是直接操作主存。

线程不会再从缓存当中获取volatile的值,而是从主存当中获取,效率会有所下降,但是保证了共享变量在多个线程之间的可见性。

public class example1 {
    volatile static boolean flag = true;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (flag) {

            }
        });

        t1.start();
        // 这里释放休眠,防止t1线程还未执行就被主线程给修改变量
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;

    }
}
  • 2 使用synchroized

加锁(如 synchronizedReentrantLock)会强制线程从主内存中读取变量的最新值,而不是从本地缓存(工作内存)中读取。这是 Java 内存模型(JMM)保证可见性的核心机制之一。

package day01.neicun;

public class example1 {
    static boolean flag = true;
    // 锁对象
    final static Object obj = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            //这里会在执行完判断后将锁释放,那修改变量的同步代码块就可以运行了
            while (true) {
                synchronized (obj) {
                    if (flag) {
                        break;
                    }
                }
            }
        });

        t1.start();
        // 这里释放休眠,防止t1线程还未执行就被主线程给修改变量
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (obj){
            flag = false;
        }

    }
}

二 原子性

机制原理适用场景特点
synchronized互斥锁需要保护临界区代码块或方法JVM内置,简单易用,可能阻塞
显式锁 (Lock)更灵活的互斥锁需要高级锁特性(可中断、公平等)更灵活,需手动释放锁
原子类CAS + volatile对单个变量进行原子操作无锁,高性能
不可变对象状态不可修改共享只读数据无同步开销,最安全

三 有序性

有序性是指:程序执行的顺序按照代码的先后顺序执行。

1 指令重排

指令重排(Instruction Reordering)是现代CPU和编译器为了提高程序执行效率而采用的关键优化技术。它通过重新排列指令的执行顺序来最大化利用CPU资源,但可能引发多线程环境下的可见性问题。

现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。

1 使用volatile的情况

public class ConcurrencyTest {
    int num = 0;
    volatile boolean ready = false;

    public void actor1(I_Result r) {
        if (ready) { // 读取 volatile 变量,确保看到最新的值
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    public void actor2(I_Result r) {
        num = 2; // 写操作
        ready = true; // 写 volatile 变量,确保前面的操作已完成
    }
}

四 volatile原理

volatile 关键字的底层原理是 内存屏障(Memory Barriers) 机制。

volatile 解决的是 "一个线程写,其他线程读" 时的有序性和可见性问题,而解决 "多个线程同时写" 需要更强的同步机制。

  • 对volatile变量的写指令后会加入写屏障
  • 对volatile变量的读指令前会加入读屏障

总的来说写屏障保障之前的写and重排,读屏障保障之后的读and重排

1 如何保证可见性

  • 写屏障保证在该屏障之前的,对变量的改动都同步到主存当中。

  • 读屏障保证在该屏障之后对变量的读取,加载的是主存中的最新数据。

2 如何保证有序性

  • 写屏障会确保指令重排序时,不会将屏障之前的代码排在写屏障之后。

  • 读屏障会确保指令重排序时,不会将屏障之后的代码排在读屏障之前。

3 无法解决指令交错(无法保证原子性)

volatile 解决的是 "一个线程写,其他线程读" 时的有序性和可见性问题,而解决 "多个线程同时写" 需要更强的同步机制(结合锁机制确保)。

  • 写屏障仅仅是保证之后的读取能读取到最新的结果,但是不能保证读跑到他前面去。
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序。

4 双重检查锁

双重检查锁(double-checked locking)的核心目的---以单例为例

1. 双重检查的作用

双重检查锁(Double-Checked Locking)的核心目的是:

  • 性能优化:第一次检查避免不必要的同步开销。
  • 线程安全:第二次检查确保在同步块内只有一个线程能创建实例。
代码示例
public final class Singleton {
    // 1. volatile 修饰的静态实例
    private static volatile Singleton instance;

    // 2. 私有构造函数
    private Singleton() {
    }

    // 3. 双重检查的获取实例方法
    public static Singleton getInstance() {
        if (instance == null) {                  // 第一次检查(无锁)
            synchronized (Singleton.class) {     // 同步块
                if (instance == null) {          // 第二次检查(有锁)
                    instance = new Singleton();  // 创建实例
                }
            }
        }
        return instance;
    }
}

一些知识点

  1. final 修饰类的原因
    防止子类化破坏单例性,确保全局唯一性。

  2. 实例变量 private 的原因
    强制通过工厂方法访问,防止外部直接修改实例状态。(private为类内部)

  3. 实例变量 volatile 的原因
    禁止指令重排序(解决部分构造问题),保证跨线程可见性。

  4. 构造函数 private 的原因
    禁止外部实例化,确保单例控制权唯一。

  5. 两次 null 判断的原因
    首次检查(无锁)避免性能开销;二次检查(同步块内)防止重复创建。

  6. 类对象加锁的原因
    类锁保证全局唯一性,且独立于实例生命周期,避免死锁风险。

  7. 类的实现目的
    实现高性能线程安全单例:延迟加载、双重检查锁定、全局唯一实例访问。

破坏单例模式

1 反射方式

通过反射强行调用私有构造函数(setAccessible(true)),绕过单例的静态实例控制逻辑,直接创建新对象。

2 反序列化

反序列化时,ObjectInputStream 默认通过反射调用无参构造器新建对象,而非返回单例的静态实例。

普通方式解决

package day01.vola;

import java.io.Serial;
import java.io.Serializable;

public final class Singleton implements Serializable {
    // 1. volatile 修饰的静态实例
    private static volatile Singleton instance;

    // 2. 私有构造函数
    private Singleton() {
        // 防止反射破坏单例
        if (instance != null) {
            throw new RuntimeException("单例类禁止通过反射创建实例!");
        }
    }

    // 3. 双重检查的获取实例方法
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    // 4. 防止反序列化破坏单例
    @Serial
    private Object readResolve(){
        return instance;
    }
}

枚举方式解决

为何可以实现单例并解决反序列化和反射而导致的问题

  1. 实例数量固定:枚举在编译期确定所有实例,运行时无法创建新实例

  2. 类加载机制:枚举实例在首次访问时由 JVM 静态初始化(线程安全)

  3. JVM 底层保障

    • 禁止反射创建枚举实例(Constructor.newInstance() 直接抛出异常)

    • 特殊序列化机制(仅存储枚举名,反序列化通过 Enum.valueOf() 还原)

  4. 语法限制

    • 枚举构造器强制私有化

    • 不能显式实例化(编译器阻止 new 操作)

代码实现:

package day01.vola;

public enum Single {
    // 唯一单例实例(名称可自定义)
    INSTANCE;

    // 单例状态字段(自动线程安全)
    private int requestCount = 0;

    // 单例业务方法
    public void handleRequest() {
        requestCount++;
        System.out.println("Handled request #" + requestCount);
    }

    // 可添加静态业务方法
    public static void warmUp() {
        System.out.println("Singleton initialized");
    }
}

静态内部类实现单例

5 happens-before

happens-before规定了对共享变量的写操作对其他线程的读操作可见,他是可见性与有序性的一套规则总结。

 八大规则

规则示例作用
程序顺序规则x=1; y=2; → x 先于 y单线程操作顺序保留
监视器锁规则解锁 → 后续加锁synchronized 可见性保证
volatile规则写 volatile → 后续读禁止重排序 + 跨线程可见
线程启动规则thread.start() → 线程内操作父线程配置对子线程可见
线程终止规则线程结束 → thread.join()子线程操作对父线程可见
传递性规则A→B 且 B→C ⇒ A→C跨操作链式可见
中断规则interrupt() → 检测到中断中断信号可靠传递
finalize规则构造结束 → finalize()对象完整构造后才回收

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小安同学iter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值