JUC系列(八)Java内存模型 volatile关键字与单例模式实践

本文深入探讨Java并发中的Volatile关键字特性,包括其可见性、原子性和禁止指令重排的特点,并通过示例代码进行验证。同时介绍了Java内存模型(JMM)的基本概念及规则。此外,还讲解了Java中几种实现单例模式的方法,如饿汉式、DCL懒汉式、静态内部类和枚举。

📣 📣 📣 📢📢📢
☀️☀️你好啊!小伙伴,我是小冷。是一个兴趣驱动自学练习两年半的的Java工程师。
📒 一位十分喜欢将知识分享出来的Java博主⭐️⭐️⭐️,擅长使用Java技术开发web项目和工具
📒 文章内容丰富:覆盖大部分java必学技术栈,前端,计算机基础,容器等方面的文章
📒 如果你也对Java感兴趣,关注小冷吧,一起探索Java技术的生态与进步,一起讨论Java技术的使用与学习
✏️高质量技术专栏专栏链接: 微服务数据结构netty单点登录SSMSpringCloudAlibaba
😝公众号😝想全栈的小冷,分享一些技术上的文章,以及解决问题的经验
当前专栏JUC系列

JMM

谈谈队 Volatile的理解

Volatile 是 java 虚拟机的轻量级同步机制

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

什么是Jmm

JMM就是Java内存模型(java memory model) Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行线程不能直接读写主内存中的变量

jmm的一些同步约定
  1. 线程解锁前 必须把共享变量 立刻刷新回主缓存
  2. 线程加锁 前,必须读取主内存中的最新值到工作中
  3. 加锁和解锁必须是同一把锁

线程 : 工作内存和 主内存

Jmm中的八种操作:

image-20220304163433104

  • lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
  • read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
  • load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
  • use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
  • store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
  • write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

JMM对8种内存交互操作制定的规则:

  • 不允许read、load、store、write操作之一单独出现,也就是read操作后必须load,store操作后必须write。
  • 不允许线程丢弃他最近的assign操作,即工作内存中的变量数据改变了之后,必须告知主存。
  • 不允许线程将没有assign的数据从工作内存同步到主内存。
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过load和assign操作。
  • 一个变量同一时间只能有一个线程对其进行lock操作。多次lock之后,必须执行相同次数unlock才可以解锁。
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值。在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值。
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。
  • 一个线程对一个变量进行unlock操作之前,必须先把此变量同步回主内存。

Volatile

验证 这个关键字的特性

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

1 可见性

    //volatile 之后 就会循环停止了,验证了特性 1 可见性
    private volatile static int num = 0;

    public static void main(String[] args) {
        //此时循环 没有结束 为什么? 因为线程队主内存的变化并不知道
        new Thread(() -> {
            while (num == 0) {

            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = 1;
    }

2 不保证原子性

使用 synchronized 关键字 保证原子性 结果为 20000

   private static int num = 0;

    public synchronized static void add() {
        num++;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + num);
    }

使用 Volatile 不保证原子性 17824 每次出来的都不一样,可能是两万 概率不大

    private volatile static int num = 0;

    public static void add() {
        num++;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + num);
    }

问题 如果不上锁 也不用 关键字 ,我们怎么保证原子性

image-20220304165005009

使用JUC 包中的 原子类 来解决 原子性问题

比如 int : 替换成 原子类 AtomicInteger

 private volatile static AtomicInteger num = new AtomicInteger(0);

    public static void add() {
        //num++;
        // incr 自增
        num.getAndIncrement();
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    add();
                }
            }).start();
        }
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + num);
    }

保证了原子操作 输出20000

image-20220304165350036

这些原子类的底层直接和操作系统挂钩,在内存中修改值,Unsafe 类是一个中很特殊的存在

指令重排

什么是 指令重排: 我们写的程序 计算机不一定会按照我们写的顺序来执行

源代码 -> 编译器优化重排 -> 指令并行可能会重排->内存系统也会重排–>执行

处理器再进行指令重排的时候 考虑数据之间的依赖性

int x = 1;// 1
int y = 2;// 2
x = x + 5;// 3
y = x * x;// 4

我们期待的是 1234, 可能执行的时候变成了 2134 1324
但是不可能是 4123

可能 造成影响最后的执行结果 a b x y 默认值都是 0

image-20220304170008300

正常的结果 : x=0;y=0; 可能由于指令重排

image-20220304170120677

指令重排 导致的奇怪结构 = x=2 y=1

volatile 可以避免指令重排

内存屏障 CPU 指令 作用:

  1. 保证特定操作的执行顺序
  2. 可以保证某些变量的内存可见性

image-20220304170643130

单例模式

饿汉式

public class Hungryemo {
    private Hungryemo() {
    }

    private final static Hungryemo HUNGRYEMO = new Hungryemo();

    public static Hungryemo getIstance() {
        return HUNGRYEMO;
    }
}

DCL懒汉式

package single;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * @projectName: JUC
 * @package: single
 * @className: LazyMan
 * @author: 冷环渊 doomwatcher
 * @description: TODO
 * @date: 2022/3/4 17:09
 * @version: 1.0
 */
public class LazyMan {
    private static boolean flag = true;

    private LazyMan() {
        synchronized (LazyMan.class) {
            if (flag == false) {
                flag = true;
            } else {
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
            if (lazyMan != null) {
                throw new RuntimeException("不要试图用反射破坏单例模式");
            }
        }
    }

    //必须 加上 volatile 避免 指令重排
    private volatile static LazyMan lazyMan;

    // 双重检测锁模式 的懒汉式单例 DCL懒汉式
    public static LazyMan getInstance() {
        if (lazyMan == null) {
            synchronized (LazyMan.class) {
                if (lazyMan == null) {
                    lazyMan = new LazyMan();
                    //    会有问题 不是一个原子性操作
                    /* 1. 分配内存空间
                     * 2 。 执行构造对象 初始化对象
                     * 3. 把这个对象指向空间
                     *
                     * 比如 我们期望执行的是 123
                     * 但是 指令重排 执行是 132
                     * a 线程可能没有问题,
                     * b 就会以为已经完成构造,指向就是null 可能会空指针
                     * */
                }
            }
        }
        return lazyMan;
    }

    //但是会有并发问题
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<EnumSngle> field = EnumSngle.class.getDeclaredConstructor(String.class, int.class);
        field.setAccessible(true);
        //LazyMan lazyMan = field.newInstance();
        //LazyMan lazyMan1 = field.newInstance();
        EnumSngle lazyMan = field.newInstance();
        EnumSngle lazyMan1 = field.newInstance();

        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }
}

静态内部类

public class Holder {
    private Holder() {
    }

    public static Holder getInstance() {
        return innerClass.holder;
    }

    public static class innerClass {
        private final static Holder holder = new Holder();
    }


}

枚举

//enum 是什么? 本身也是一个 Class 类
public enum EnumSngle {
    INSTANCE;

    private EnumSngle() {
    }

    public EnumSngle getInstance() {
        return INSTANCE;
    }
}
class test{
        //但是会有并发问题
    public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Constructor<EnumSngle> field = EnumSngle.class.getDeclaredConstructor(String.class, int.class);
        field.setAccessible(true);
        //LazyMan lazyMan = field.newInstance();
        //LazyMan lazyMan1 = field.newInstance();
        EnumSngle lazyMan = field.newInstance();
        EnumSngle lazyMan1 = field.newInstance();

        System.out.println(lazyMan);
        System.out.println(lazyMan1);
    }
}
<think>好的,我需要回答用户关于Javavolatile关键字的定义、作用、使用场景以及面试回答技巧的问题。首先,我要整理已有的知识,并结合提供的引用内容来确保回答的准确性。 根据引用内容,volatile关键字主要涉及可见性、有序性和原子性。引用3提到volatile保证可见性和有序性,但不保证原子性,这需要明确。引用4强调了禁止指令重排,这是有序性的一部分。引用2中的代码示例展示了volatile如何解决线程间的可见性问题。 接下来,需要将信息结构化。定义部分需要简明扼要,作用分点列出可见性、有序性,并指出不保证原子性。使用场景应包括状态标志、双重检查锁定等。面试技巧部分要强调原理和实际例子,并区分synchronized的不同。 要注意用户提到的面试回答技巧,可能需要结合常见面试问题,如volatilesynchronized的区别,适用场景等。同时,引用中的例子可以用来具体说明,比如状态标志的例子来自引用2,双重检查锁定可能来自其他知识。 需要避免技术错误,比如确保提到内存屏障(引用3中的内容)和禁止指令重排(引用4)。同时,指出volatile不适用于需要原子操作的情况,比如i++,这时需要用synchronized或Atomic类。 最后,生成相关问题时要覆盖特性、应用场景、synchronized的区别、原理和常见误区,帮助用户进一步巩固知识点。</think>### Java volatile关键字详解 #### 一、定义核心特性 `volatile`是Java提供的轻量级同步机制,用于修饰变量。其核心特性包括: 1. **可见性**:保证多线程环境下,变量的修改对所有线程立即可见(通过强制从主内存读写数据,而非线程本地缓存)[^3] 2. **禁止指令重排**:通过内存屏障确保代码执行顺序程序顺序一致[^4] #### 二、三大作用 1. **线程可见性保证** ```java // 示例:状态标志位 private volatile boolean isRunning = true; void worker() { while(isRunning) { // 工作循环 } } ``` *当主线程修改`isRunning`时,工作线程能立即感知状态变化[^2]* 2. **有序性保障** ```java // 双重检查锁定单例模式 class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if(instance == null) { synchronized(Singleton.class) { if(instance == null) { instance = new Singleton(); // 防止指令重排导致未初始化完成的对象被返回 } } } return instance; } } ``` 3. **原子性限制** - **不保证复合操作的原子性**:如`i++`操作(读取-修改-写入三步操作) - **适用场景**:仅适用于单步读/写操作 #### 三、使用场景 | 场景类型 | 说明 | 示例 | |-------------------|----------------------------------------------------------------------|--------------------------| | 状态标志 | 简单的布尔状态标记 | 线程终止标志 | | 一次性安全发布 | 配合final字段使用 | 对象初始化完成状态 | | 独立观察 | 定期发布的观察结果 | 系统指标统计 | | 双重检查锁定 | 单例模式中减少同步开销 | 单例对象初始化[^4] | #### 四、面试回答技巧 1. **原理层面回答** - 内存屏障实现:写操作前插入StoreStore屏障,写操作后插入StoreLoad屏障;读操作前插入LoadLoad和LoadStore屏障[^3] - JMM(Java内存模型)角度:建立happens-before关系 2. **对比synchronized** || volatile | synchronized | |----------|-----------|--------------| | 可见性 | ✅ | ✅ | | 原子性 | ❌ | ✅ | | 阻塞性 | 非阻塞 | 阻塞 | | 适用场景 | 状态标志 | 复合操作 | 3. **常见问题应答示例** Q: "volatile能替代synchronized吗?" A: "不能。volatile仅保证单次读写的原子性,而synchronized能保证代码块的原子性。例如计数器场景,必须使用synchronized或Atomic类" 4. **实践建议** - 明确标注使用意图(如// volatile used for visibility) - 配合`juc.atomic`包使用更安全 - 避免过度使用(性能影响比synchronized小但仍存在) #### 五、常见误区 1. **认为volatile变量完全线程安全** *错误示例*:`volatile int count = 0; count++;` 正确做法:使用`AtomicInteger` 2. **final混用不当** *正确用法*:`private final volatile Map config;`(仅当需要延迟初始化时)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

冷环渊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值