初涉Java内存模型

Java内存模型

Java实现会带来不同的“翻译”,不同CPU平台的机器指令又千差万别,无法保证并发安全的效果一致。

JVM内存结构 VS Java内存模型 VS Java对象模型

三个截然不同的概念,容易混淆

Java内存结构

和Java虚拟机的运行时区域有关

组成:堆,虚拟机栈,方法区,本地方法栈,程序计数器

Java对象模型

和Java对象在虚拟机中的表现形式有关

1.是Java对象自身的存储模型

2.JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类

3.当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据

Java内存模型(JMM)

为什么需要JMM(Java Memory Model)

1.C语言不存在内存模型的概念

2.依赖处理器,不同处理器结果不一样,可能一个程序在不同处理器运行结果不同

3.无法保障并发安全

4.需要一个标准,让多线程运行的结果可以预期

JMM是一种规范

即这是一组规范,需要各个JVM的实现来遵循JMM规范,以便于开发者可以利用这些规范,更加方便地开发多线程程序。如没有这样的规范,那么可能经过不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样,就会产生问题。

JMM是工具类和关键字的原理

1.volatile、synchronized、Lock等的原理都是JMM

2.若没有JMM,就需要我们自己制定什么时候用内存栅栏,即什么时候同步,很麻烦

最重要的3点内容:重排序、可见性、原子性

重排序

package jmm;

import java.util.concurrent.CountDownLatch;

/**

* @Auther: Bob

* @Date: 2020/2/15 15:41

* @Description: 重排序的演示

* 直到达到某个条件才停止,用来测试小概率时间

*/

public class OutOfOrderExecution {

private static int x = 0, y = 0;

private static int a = 0, b = 0;

public static void main(String[] args) throws InterruptedException {

int i = 0;

for (; ; ) {

i++;

x = 0;

y = 0;

a = 0;

b = 0;

CountDownLatch latch = new CountDownLatch(1);

Thread one = new Thread(new Runnable() {

@Override

public void run() {

try {

latch.await();

} catch (InterruptedException e) {

e.printStackTrace();

}

a = 1;

x = b;

}

});

Thread two = new Thread(new Runnable() {

@Override

public void run() {

try {

latch.await();

} catch (InterruptedException e) {

e.printStackTrace();

}

b = 1;

y = a;

}

});

one.start();

two.start();

latch.countDown();

one.join();

two.join();

String result = "第" + i + "次 (" + x + "," + y + ")";

if (x == 1 && y == 1) {

System.out.println(result);

break;

} else {

System.out.println(result);

}

}

}

}


执行结果:第453次才出现了1,1

什么是重排序

在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序,这里被颠倒的是y = a 和 b = 1 这两行语句

重排序的好处:提高处理速度

重排序的3种情况

1.编译器优化:包括JVM,JIT编译器等

2.CPU指令重排:就算编译器不发生重拍,CPU也可能对指令进行重拍

3.内存的“重排序”:线程A的修改线程B却看不到,引出可见性问题(不是真正的重排序)

可见性

什么是可见性

package jmm;

/**

* @Auther: Bob

* @Date: 2020/2/15 16:37

* @Description: 演示可见性带来的问题

*/

public class FieldVisibility {

    volatile int a = 1;

    volatile int b = 2;

    private void change() {

        a = 3;

        b = a;

    }

    private void print() {

        System.out.println("b = " + b + ", a = " + a);

    }

    public static void main(String[] args) {

        while (true) {

            FieldVisibility test = new FieldVisibility();

            new Thread(new Runnable() {

                @Override

                public void run() {

                    try {

                        Thread.sleep(1);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                    test.change();

                }

            }).start();

            new Thread(new Runnable() {

                @Override

                public void run() {

                    try {

                        Thread.sleep(1);

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                    test.print();

                }

            }).start();

        }

    }

}


有可见性问题的原因:

高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层

线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的

如果所有核心都只用一个缓存,那么就不存在内存可见性问题了

每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待输入到主存中。所以会导致读取的值是一个已经过期的值

“利用volatile关键字可以解决问题”

volatile关键字

volatile是什么

1.voltile是一种同步机制,比synchronized或者Lock相关类更轻量,因为适用vilatile并不会发生上下文切换等开销很大的行为。

2.如果一个变量修改成volatile,那么JVM就知道了这个变量可能被并发修改

3.开销小,相应能力也小,volatile做不到synchronized那样的原子保护,volatile只在有限场景下才能发挥作用

volatile适用场景与不适用场景

1.不适用:a++

2.适用场景1:boolean flag,若一哥共享变量在程序中只是被各个线程赋值,而无其他操作,那么可以用volatile来代替synchronized或者代替原子变量,因为赋值自身具有原子性,而volatile又保证了可见性,所以足以保证线程安全。

3.适用场合2:作为刷新之前变量的触发器

volatile的两点作用

1.可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须读取到主内存读取最新值,写一个volatile属性会立即刷入到主内存。

2禁止指令重排序优化:解决单利双重锁乱序问题

volatile和synchronized的关系

如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么久可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全,此时volatile可以看做轻量版的synchronized。

用volatile可以修正重排序问题

volatile小结


除了volatile可以让变量保证可见性外,synchronized、Lock、并发集合、Thread.join()和Thread.start()等都可以保证的可见性

对synchronized可见性的正确理解

1.synchronized不仅保证了原子性,还保证了可见性

2.synchronized不仅让被保护的代码安全,还近朱者赤(解锁之前的所有操作另一个线程都能看到)

原子性

什么是原子性

一系列的操作,要么全部执行成功,要么全部不执行,是不可分割的

Java中的原子操作有哪些

1.除了long和double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作,根据Oracle的官方文档,在32位的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的,对于64位的值的写入 ,可以分为两个32位的操作进行写入,读取错误,使用volatile解决。(在实际开发中,商用虚拟机中不会出现)

2.所有引用reference的赋值操作,不论是32位还是64位的操作系统

3.java.concurrent.Atomic.*包中所有类的原子操作

原子操作 + 原子操作 != 原子操作

1.简单地把原子操作组合在一起,并不能保证整体依然具有原子性

全同步的HashMap也不完全安全

总结得不太好,多请见谅,多数都只是涉及概念层面的东西。。。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Java内存区域 Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有的用途以及创建销毁...
    架构师springboot阅读 1,801评论 0 5
  • 除了充分利用计算机处理器的能力外,一个服务端同时对多个客户端提供服务则是另一个更具体的并发应用场景。衡量一个服务性...
    胡二囧阅读 1,384评论 0 12
  • 目录: 1. 指令重排 2. 顺序一致性 3. volatile 4. final 1.指令重排 要了解指令重排,...
    西部小笼包阅读 782评论 0 1
  • 前言 Java内存模型(Java Memory Model,简称JMM),是针对Java在多线程并发下可能出现的各...
    juconcurrent阅读 429评论 0 1
  • 本文属使用Prisma构建GraphQL服务系列。 当搞定了GraphQL服务端开发,且经过充分测试,那么接着需要...
    guog阅读 2,694评论 0 1