代码编织梦想

面试题

1.你知道什么是Java内存模型吗?
2.JMM与volatile二者之间的关系。
3.JMM有哪些特性orJMM的三大特性是什么?
4.为什么要有JMM,它为什么出现?作用和功能是什么?
5.happens-before现行发生原则你有了解过吗?

计算机硬件存储系统

在这里插入图片描述

在这里插入图片描述
Java内存模型:JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在。它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写访问方式,并决定一个线程对于共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性、可见性和有序性展开。
原则:JMM键技术点都是围绕多线程的原子性、可见性和有序性展开。

能做什么?
1.通过JMM来实现线程和主内存之间的抽象关系。
2.屏蔽各个操作系统硬件平台的内存访问差异性,以实现Java程序在任何平台下都能达到一致的内存访问效果。

JMM三大特性:
1.可见性
在这里插入图片描述
系统主内存共享变量数据修改被写入的时机是不确定的,多线程并发下很可能出现脏读,所以每个线程都有自己的工作内存,线程的工作内存中保存了线程需要使用的变量在主内存中的数据副本,线程对数据的所有操作(读、写)都需要在线程自己的工作内存中进行,不能直接读写主内存中的变量。不同线程之间也无法访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
在这里插入图片描述

线程脏读:

在这里插入图片描述

2.原子性:指一个操作是不可打断的,在多线程环境下,操作不能被其他线程干扰。
3.有序性:对于一个线程的代码而言,我们习惯性认为代码从上到下,有序执行。但是实际上,为了性能,编译器和处理器往往会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果和它的顺序话执行结果相等,那么指令的执行顺序可以与代码顺序不同,此过程叫做指令重排序。
优缺点:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
显然不行,x值未定义,不能直接用。

volatile详解

volatile两大特性是可见性和有序性(禁止指令重排),为什么volatile可以保证可见性和有序性?
volatile靠内存屏障实现可见性和有序性,那什么是内存屏障?

内存屏障

内存屏障是什么?内存屏障(也成内存栅栏,屏障指令)是一类同步屏障指令,是CPU或编译器在对内存的随机访问的操作中的一个同步点,此点之前的所有读写指令都执行完后才能执行此点之后的操作,避免指令重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁止指令重排),但是volatile无法保证原子性。
内存屏障之前的所有写操作都要将数据会写到主内存
内存屏障之后的所有读操作都可以读取到内存屏障之前的所有写操作的最新结果(实现了可见性)
写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有写操作的最新结果会写到主内存中。也就是说看到Store屏障指令,就需要把该指令之前的所有写操作指令全部执行完毕才能玩下执行。
读屏障(Load Memory Barrier):处理器在读屏障之后的指令,都需要在读屏障之后执行。也就是说在Load屏障指令之后,能保证后面的读指令读取的数据就一定是最新的。
因此在重排序的时候,不允许将内存屏障之后的指令重排序到内存屏障之前。一句话,对一个volatile变量的写,一定先行发生于后续对于这个volatile变量的读,也叫读后写。

内存屏障的分类

内存屏障粗分为两种:1.读屏障(Load Memory Barrier) 2.写屏障(Store Memory Barrier)

读屏障(Load Memory Barrier)

作用:在读指令之前插入读屏障,让线程的工作内存(CPU高速缓存)中缓存的共享变量数据失效,重新回到主内存中去获取共享变量的最新数据。

写屏障(Store Memory Barrier)

作用:在写指令之后插入写屏障,强制将线程工作内存(CPU高速缓存)中的最新缓存数据回刷到主内存中。

细分四种内存屏障:

屏障类型指令实例说明
LoadLoadLoad1;LoadLoad;Load2保证load1的读取操作在load2及后续读取操作之前执行
StoreStoreStore1;StoreStore;Store2在Store2及其后的写操作执行前,保证Store1的写操作已经刷新到主内存
LoadStoreLoad1;LoadStore;Store2在Store2及其后逇写操作执行前,保证Load1的读操作已经读取完毕
StoreLoadStore1;StoreLoad;Load2保证Store1的写操作值已经更新到主内存之后,Load2的读操作才开始执行
第一个操作第二个操作:普通读写第二个操作:volatile读第二个操作:volatile写
普通读写可以重排可以重排不可以重排
volatile读不可以重排不可以重排不可以重排
volatile写可以重排不可以重排不可以重排
当第一个操作为volatile读时,无论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会重排到volatile读之前。
当第二个操作为volatile写时,无论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会重排序到volatile写之后
当第一个操作为volatile写,第二个操作为volatile读时,不能进行重排序

读屏障就是在每个volatile读之后插入一个LoadLoad屏障和一个LoadStore屏障

在这里插入图片描述

写屏障就是在每个volatile写操作之前插入一个StoreStore屏障,在每个volatile写操作之后插入一个StoreLoad屏障

在这里插入图片描述

volatile可见性验证

代码如下:

package multiThreads;

import java.sql.Time;
import java.util.concurrent.TimeUnit;

public class VolatileTest1 {
    static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " come in");
            while (flag) {

            }
            System.out.println(Thread.currentThread().getName() + " get out");
        }, "a").start();

        TimeUnit.MILLISECONDS.sleep(300L);

        flag = false;
        System.out.println(Thread.currentThread().getName() + "set flag to false");
    }
}

结果如下:
在这里插入图片描述
可以看到上述while(flag)循环无法结束,也就是flag被主线程修改为false之后,线程a的工作内存的值并未更新,还是true。即主线程对flag值的修改,对于线程a来说是不可见的。
解决方法,将flag变量用volatile修饰,保证其在多线程之间的可见性。
代码如下:

package multiThreads;

import java.sql.Time;
import java.util.concurrent.TimeUnit;

public class VolatileTest1 {
    static volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " come in");
            while (flag) {

            }
            System.out.println(Thread.currentThread().getName() + " get out");
        }, "a").start();

        TimeUnit.MILLISECONDS.sleep(300L);

        flag = false;
        System.out.println(Thread.currentThread().getName() + "set flag to false");
    }
}

结果如下:
在这里插入图片描述
可以看到,此时mian线程将flag值更新为false之后,线程a立刻就获取到了最新的值,结束了while循环。

volatile变量的读写过程

Java内存模型中定义了8种每个线程自己的工作内存主内存之间的原子操作
read(读取)->load(加载)->use(使用)->assign(赋值)->store(存储)->write(写入)->lock(锁定)->unlock(解锁)
工作内存与主内存之间的8种原子操作
在这里插入图片描述

volatile不具备原子性案例详解

代码示例:

package multiThreads;

import java.util.concurrent.TimeUnit;

public class IPlus {

    private int num;

    public synchronized void selfPlus() {
        num++;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public static void main(String[] args) throws InterruptedException {
        IPlus iPlus = new IPlus();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    iPlus.selfPlus();
                }
            }).start();
        }

        TimeUnit.MILLISECONDS.sleep(3000L);
        System.out.println(iPlus.getNum());
    }
}

运行结果如下:
在这里插入图片描述
使用synchronized修饰plus()方法,最终累加结果是正确的。

如果使用volatile修饰num,去除synchroniezd,可以保证正确吗?
代码如下:

package multiThreads;

import java.util.concurrent.TimeUnit;

public class IPlus {

    volatile int num;

    public void selfPlus() {
        num++;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public static void main(String[] args) throws InterruptedException {
        IPlus iPlus = new IPlus();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    iPlus.selfPlus();
                }
            }).start();
        }

        TimeUnit.MILLISECONDS.sleep(3000L);
        System.out.println(iPlus.getNum());
    }
}

运行结果:
在这里插入图片描述
从结果来看,volatile显然是不能保证原子性的。
在这里插入图片描述
volatile自增失效解析:假设此时主内存中有个变量num的最新值是5,线程a和线程b都是对num进行+1操作。线程a和线程b同时读取num=5这个值保存到自己的工作内存中,线程a先执行完+1操作,然后将num的最新值回写到主内存中,此时线程b虽然还未操作完,但是由于volatile修饰的变量具有可见性,线程b立刻就知道num的值发生了改变,那么线程b的此次+1操作则会失效,num的最终值是6.

i++操作不具有原子性:
在这里插入图片描述
结论:volatile不适合参与到依赖当前值的运算。
在这里插入图片描述

volatile禁止指令重排案例详解

在这里插入图片描述
在这里插入图片描述

volatile的适用场景

1.单一赋值可以,但是包含复合运算赋值不可以(++i之类)
volatile int a = 10;//可以
volatile boolean flag = false;//可以
2.状态标识标志业务是否结束(volatile boolean flag = false)
3.开销较低的读,写锁策略
示例代码:

package multiThreads;

public class UseVolatileTest {
    public static void main(String[] args) {

    }
}

class Counter {
    //读多写少的场景,使用volatile+内部锁的方式来优化程序性能
    //复合写操作,需要使用synchronized来保证写操作的原子性。
    //利用volatile来保证读操作的可见性
    private volatile int num;

    public int getNum() {
        return num;
    }

    public synchronized int getValue() {
        return ++num;
    }
}

4.DCL双端锁的发布
问题代码示例:

package multiThreads;

public class SafeDoubleCheckSingleton {

    private static SafeDoubleCheckSingleton safeDoubleCheckSingleton;

    private SafeDoubleCheckSingleton() {
    }


    public static SafeDoubleCheckSingleton getInstance() {
        if (null == safeDoubleCheckSingleton) {
            synchronized (SafeDoubleCheckSingleton.class) {
                if (null == safeDoubleCheckSingleton) {
                    //多线程情况下,由于指令重排,该对象可能还未初始化就被其他线程获取
                    safeDoubleCheckSingleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        return safeDoubleCheckSingleton;
    }
}

在这里插入图片描述
在这里插入图片描述
解决方案,给单例变量加上volatile
代码如下:

package multiThreads;

public class SafeDoubleCheckSingleton {

    private volatile static SafeDoubleCheckSingleton safeDoubleCheckSingleton;

    private SafeDoubleCheckSingleton() {
    }


    public static SafeDoubleCheckSingleton getInstance() {
        if (null == safeDoubleCheckSingleton) {
            synchronized (SafeDoubleCheckSingleton.class) {
                if (null == safeDoubleCheckSingleton) {
                    //多线程情况下,由于指令重排,该对象可能还未初始化就被其他线程获取
                    safeDoubleCheckSingleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        return safeDoubleCheckSingleton;
    }
}

小结:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_46439731/article/details/132929047

1入门基础知识-爱代码爱编程

1.C++98关键字 面试:C++语言中一共有多少个关键字 在C++98中有63个关键字。(在回答的时候一定要带上语言标准带上) 2.命名空间 2.1 概念 实际就是一个新的作用域,用来解决命名冲突问题 2.2 定义方式 方式一: namespace N1 (变量、函数、类) 方式二: 命名空间可以嵌套定义 方式三: 在同一个工程中

springboot属性注入增强(一)背景/需求-爱代码爱编程

一 背景 springboot 在启动时候会将系统的环境变量、项目的启动时设置的属性 、application.yml文件(或application.properties文件)、@PropertySource定义的配置文件中的属性加载到Environment对象中,分布式配置中心框架也会把配置加载到Environment中。而springboot的属性注

mybatis:the error occurred while setting parameters;foreach语句不生效-爱代码爱编程

根本原因就是在参数上,列举一下可能的原因: 1.sql语句中的传的参数类型和数据库中不一致 2.#{}写成${} 3.也有说是在sql语句后加了“;”有影响的 本人sql语句如下: 该条语句的参数是list,list中存着

gof23设计模式之责任链模式-爱代码爱编程

1.概述 责任链模式又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。 2.结构

java 类加载机制-爱代码爱编程

文章目录 前言一、概述二、类的生命周期2.1.加载2.2.验证2.3.准备2.4.解析2.5.初始化 三、类的加载时机3.1、主动引用3.2.被动引用 四、类加载器五、双亲委派模型5.1双亲委派工作

mybatis-plus数据表操作条件构造器wrapper-爱代码爱编程

一、Wapper分类 Wrapper : 条件构造抽象类,最顶端父类 AbstractWrapper : 用于查询条件封装,生成 sql 的 where 条件 QueryWrapper : Entity 对象封装操作类,不

自定义全局异常-爱代码爱编程

1.自定义响应结果码 接口 public interface ResultCode { //操作是否成功,true为成功,false操作失败 boolean success(); //操作代码 int code(); //提示信息 String message(); } 2.自定义异常类 import

如何使用java进行安全测试?-爱代码爱编程

要使用Java进行安全测试,可以按照以下步骤进行: 确定测试目标:首先,明确要测试的应用程序或系统的安全目标和需求。确定要测试的安全方面,如身份验证、授权、输入验证、安全配置等。 了解安全测试知识:熟悉常见的安全漏洞和攻击方法,如跨站脚本攻击(XSS)、SQL注入、跨站点请求伪造(CSRF)等。理解这些漏洞和攻击方法的原理和实现方式,以便在测试过程中

java视频压缩亲测有效,无需插件-爱代码爱编程

依赖  <!-- 视频压缩 --> <dependency> <groupId>ws.schild</groupId> <artifactId>jave-core</artifactId> <version>3.0.0</version&g

springboot整合规则引擎-爱代码爱编程

Springboot整合Drools 规则引擎 1.添加maven 依赖坐标,并创建springboot项目 <!-- drools规则引擎 --> <dependency> <gr

【java】jdk8 jvm参数配置及说明-爱代码爱编程

参数 说明 1.堆内存参数设置 -Xms 或 -XX:InitialHeapSize=n 设置堆的初始值 指令1:-Xms2g 指令2:-XX:InitialHeapSize=2048m -Xmx 或 -XX:MaxHeapSize=n 设置堆区最大值 指令1:-Xmx2g 指令2: -XX:MaxHeapSize=2048m-XX:NewSi

java——break、continue(学习笔记)-爱代码爱编程

1.break(主要与switch搭配使用) 在任何循环语句的主体部分,均可用break控制循环的流程。break用于强行退出循环,不执行循环中剩余的语句。 2.continue 用在循环语句体中,用于终止某次循环过程