JMM内存模型与volatile的理解

JMM内存模型理解

一、JMM即Java Memory Model,也就是java内存模型。

在这里插入图片描述

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取、赋值等)都必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各线程中的工作内存中存储着主内存中的变量拷贝副本,因此不同线程间不能访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成

1.1、JMM特性:
1、可见性
 
2、原子性 
3、有序性
1.2、看下面代码:
class MyVolatile{
        int a =0;
        public void add(){
            this.a = 20;
        }
    }
public class JMMUtils {
    public static void main(String[] args) {
        MyVolatile mv = new MyVolatile();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
            }catch (Exception e){
                e.printStackTrace();
            }
            mv.add();
            System.out.println("当前子线程:"+Thread.currentThread().getName()+"当前值:"+mv.a);
        },"son").start();
        while (mv.a == 0){

        }
        System.out.println("当前主线程:"+Thread.currentThread().getName()+"当前值:"+mv.a);
    }
}
这段代码执行的结果是:
当前子线程:son当前值:20
1.3、这里可以看到,线程son改变了变量a的值,但主线程并没有看到,这是为什么呢,这是不是与上面的可见性矛盾?

计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。
有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中

这就是上面main线程获取到的值依然是0的原因!

1.4、如何解决数据一致性问题呢?

1、通过在总线加LOCK锁的方式;
2、通过缓存一致性协议
3、将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个
Runnable 对象调用外部类的这些方法

MESI协议是指:M(modified)修改、E(exclusive)独享、互斥、S(shared)共享、I(invalid)无效的
多个cpu从主内存读取同一个数据到各自的高速缓存中,其中当某个cpu修改了缓存里的数据,该数据马上同步回主内存,其他cpu通过总线嗅探机制,可以感知到数据的变化从而将自己缓存的数据失效。
MESI协议只对汇编指令中执行加锁操作的变量有效,表现到java中为使用voliate关键字定义变量或使用加锁操作

1.5、线程可见性的9种方式
public class VisibilityTest {

   //① 添加volatile private volatile boolean flag = true;
   private boolean flag = true;
   //⑤通过 final 关键字保证可见性 添加Integer private Integer count = 0;
    private int count = 0;

    public void refresh() {
        flag = false;
        System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
    }

    public void load() {
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
        System.out.println(flag);
        while (flag) {
            count++;
            //② 添加内存屏障  UnsafeUtils.getUnsafe().storeFence();
            //③ 输出语句底层有synchronized  System.out.println();
            /**
             * ⑦Thread.sleep(1)
             *   try {
             *      Thread.sleep(200);
             *   } catch (InterruptedException e) {
             *       e.printStackTrace();
             *   }
             **/
            //⑧Thread.yield() 切换上下文 Thread.yield();
            //⑨增长循环内代码执行时间 增长循环内代码执行时间,使得缓存过期,再次读取时就会重新去主内存中读取数据,即通过缓存淘汰,保证不同线程之间的可见性 shortWait(50000);
            // 增长循环内代码执行时间,使得缓存过期   shortWait(10000);
        }
        System.out.println(flag);
        System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityTest test = new VisibilityTest();

        // 线程threadA模拟数据加载场景
        Thread threadA = new Thread(() -> test.load(), "threadA");
        threadA.start();

        // 让threadA执行一会儿
        Thread.sleep(1000);
        // 线程threadB通过flag控制threadA的执行时间
        Thread threadB = new Thread(() -> test.refresh(), "threadB");
        threadB.start();

    }


    public static void shortWait(long interval) {
        long start = System.nanoTime();
        long end;
        do {
            end = System.nanoTime();
        } while (start + interval >= end);
    }
}

无论是通过内存屏障还是通过上下文切换,其底层原理就是需要满足两点
1、对线程本地变量的修改可以立刻刷新回主内存
2、同时使得其他线程中该变量的缓存失效

内存屏障:

①通过 volatile 关键字保证可见性。 字节码解释器实现的volatile方法中,会先对字段类型判断,然后执行下面这行代码 OrderAccess::storeload() 这是其实就是内存屏障


②通过 内存屏障保证可见性。
UnsafeFactory.getUnsafe().storeFence();


③通过 synchronized 关键字保证可见性。 在使用synchronized关键字时,JVM会插入不同类型的内存屏障来保证临界区内代码的顺序执行以及变量的可见性和原子性,所以其底层还是通过内存屏障来保证线程之间的可见性的
synchronized 或者 System.out.println(); 输出语句底层也是加了synchronized


④通过 Lock保证可见性。
确保后续指令执行的原子性操作
类似内存屏障的功能(lock前缀指令不是内存屏障的指令)
确保其他副本失效
具体来说,当一个线程获取ReentrantLock锁时,它会将自己工作内存中的数据刷新到主内存中,这样其他线程就能够看到最新的值。而当一个线程释放ReentrantLock锁时,它会将主内存中的数据刷新到自己的工作内存中,这样其他线程就能够读取到最新的值。


⑤通过 final 关键字保证可见性
对于使用final关键字修饰的变量或对象引用,在写入操作时会生成相应的内存屏障指令,以确保该变量或对象引用的值对其他线程可见(会在写入操作之后插入一个StoreStore屏障和一个StoreLoad屏障),
所以其底层还是通过内存屏障来保证线程之间的可见性的 int->Integer
Integer 底层是final修饰 public final class Integer extends Number implements Comparable


⑥LockSupport.unpark()
可以看到LockSupport.unpark()底层是通过调用UnSafe类实现的,调用的是unpark()方法,而非storeFence()方法


⑦Thread.sleep(1); //内存屏障

上下文切换:

⑧Thread.yield()
Thread.yield()方法让出CPU时间片,即通过上下文切换(需要保存上下文)保证了不同线程之间的可见性。在Java中,从一个线程转到另一个线程,当一个线程被切换出去时,它的本地内存中的数据会被刷新到主内存中,而当另一个线程被切换进来时,它的本地内存会从主内存中加载最新的数据,这个过程保证了不同线程之间的可见性

缓存过期:

⑨增长循环内代码执行时间
增长循环内代码执行时间,使得缓存过期,再次读取时就会重新去主内存中读取数据,即通过缓存淘汰,保证不同线程之间的可见性

二、关键字volatile

2.1、volatile实现原理

底层实现原理主要通过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行的锁定)并回写到主内存。
有volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。

1、将当前处理器缓存行数据刷写到系统主内存。

2、这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。

这样就保证了多个处理器的缓存是一致的,对应的处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器缓存行设置无效状态,当处理器对这个数据进行修改操作的时候会重新从主内存中把数据读取到缓存里

2.2、volatile的特性:

1、可见性 定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
2、不保证原子性
原子性 定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
3、禁止指令重排
对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory
Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证

2.3、volatile使用场景:

1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义
4、多线程读单线程写:在这种情况下,Volatile可以用来作为标识完成、中断或状态变化的标记。它的主要作用是确保在读取那个时刻读到的确实是最新值,利用其可见性特性

实例(创建单例模式的时候使用valatile修饰对象):
双重检查锁定是一种常见单例模式方式,保证多线程环境下只有一个实例被创建。在双重检查锁定中使用volatile修饰单例对象,保证多线程的可见性
双重检查锁:就是对创建对象进行枷锁

public class VolatileTest {
	//1.使用volatile修饰单例对象,保证多线程的可见性
    private static  volatile VolatileTest instance = null;

    public static VolatileTest getInstance(){
    //2.双重检查锁定
        if(instance == null){
            synchronized (VolatileTest.class){
                if(instance == null){
                    instance = new VolatileTest();
                }
            }
        }
        return instance;
    }
}

三、基本数据类型和引用数据类型在内存中的对比

在这里插入图片描述

四、linux安装jdk的两种方法

3.1 压缩包安装

1.上传jdk-8u221-linux-x64.tar.gz到服务器指定目录,例如我放在/usr/local/jdk下
2.解压 tar -zxvf jdk-8u221-linux-x64.tar.gz
3.配置环境:vim /etc/profile

export JAVA_HOME=/usr/local/jdk/jdk1.8.0_221
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JRE_HOME=${JAVA_HOME}/jre

4.重新加载配置:source /etc/profile

此时即安装完毕,验证:java -version 或者 jps

3.2 yum源安装

1.查询包:yum list java-1.8*
2.查询安装路径: rpm -ql java-1.8.0-openjdk-devel.x86_64
3.yum安装:yum -y install java-1.8.0-openjdk-devel.x86_64
4.配置环境变量:vim /etc/profile

export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.362.b08-1.el7_9.x86_64
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JRE_HOME=${JAVA_HOME}/jre

5.重新加载:source /etc/profile

五、虚拟机网络配置

虚拟机的网络设置:

5.1.打开vmware的 编辑–>虚拟网络编辑
5.2.选择VMnet8

可以看到
子网Ip 192.168.164.0
子掩网码 255.255.255.0

5.3.修改ifcfg-ens33
vim /etc/sysconfig/network-scripts/ifcfg-ens33

需改动:

BOOTPROTO="static"
ONBOOT="yes"
IPADDR=192.168.164.130  // 与上面第二步的ip一致,最后一位不一样
NETMASK=255.255.255.0   //与第二步的子掩网码一致
GATEWAY=192.168.164.2   // 把ip中最后一个数字改为.2即可
DNS1=192.168.164.2       //与GATEWAY一样即可
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

神雕大侠mu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值