Java多线程基础与使用详细篇(四)----Java内存模型

前言

继续学习Java多线程基础与使用详细篇(三)----多线程可能导致安全、性能问题下的知识。本篇会涉及Java的内存模型

1. Java内存模型的底层原理是什么

1.1 从Java 代码到CPU指令

image.png

Java 程序的编译与运行
(1). 最开始,我们编写的Java代码,是.Java文件
(2). 在编译(Javac命令)后,从刚出的
.JAVA文件会变成一个新的Java字节码文件(.class)
(3). JVM会执行刚出生成的字节码文件(
.class),并把字节码转化为机器指令
(4). 机器指令可以直接在CPU上执运行,也就是最终的 程序执行
1.2 3. JVM实现会带来不同的 "翻译",不同的CPU 平台的机器指令又千差万别,无法保证并发安全的效果一致

2. JVM 内存结构 、JAVA 内存模型、JAVA 对象模型

2.1. 容易混淆: 三个截然不同的概念

整体方向:
JVM内存结构,和JAVA 虚拟机的运行时区域有关
JAVA 内存模型,和Java的并发编程有关。
JAVA对象模型,和Java对象有虚拟机中的表现形式有关
JVM 内存结构图
https://img-blog.csdnimg.cn/20191025093302354.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ppYW5naGFvMjMz,size_16,color_FFFFFF,t_70
Java 对象模型图

image.png

(1). Java 对象自身的存储模型
(2). JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类
(3). 当我们在JAVA 代码中,会用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据。

3. JMM是什么

为什么需要JMM
(1).C语言不存在内存模型的概念
(2).依赖处理器,不同处理器结果不一样
(3)无法保证并发安全
(4) 需要一个标准,让多线程运行的结果可预期

4. JMM是规范

JAVA Memory Model
(1).是一组规范,需要各个JVM的实现来蹲守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。
(2).如果没有这样的一个JMM内存模型来规范,JVM的不同规则的重排序之后,导致不同的
虚拟机上运行的结果不一样,那是很多大问题
是工具类和关键字的原理
(3). volatile、synchronized、Lock等的原理都是JMM
如果没有JMM,那就需要我们自己制定什么时候用内存
栅栏等,那是相当麻烦的,幸好有了JMM,让我们只需要用 同步工具和关键字就可以开发程序。

4. 重排序

4.1. 什么是重排序

在线程1内部的量代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严哥按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序。

4.2 重排序的代码案例

直到达到某个条件才停止,测试小概率事件,
最会结果会出现x=0,y=0?那是因为重排序发生代码的执行顺序的其中一种可能:
y=a;a=1;x=b;b=1;

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.countDown();
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;
                    x = b;
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.countDown();
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    b = 1;
                    y = a;
                }
            });
            two.start();
            one.start();
            latch.countDown();
            one.join();
            two.join();

            String result = "第" + i + "次(" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}
4.3. 重排序的好处;提高处理速度
 * 对比重排序前后的指令优化
 *
 * a  = 3;    -> Load a
 *            -> Set to 3
 *            -> Store a
 *
 * b = 2 ;     -> Load b
 *             -> Set to 2
 *             -> Store b
 *
 *
 * a = a + 1;   -> Load a
 *              -> Set to 4
 *              -> Store a

对比后面,明显提高的速度

 * 对比重排序前后的指令优化
 *
 * a  = 3;         -> Load a
 * a = a + 1 ;   -> Set to 3
 *                    -> Set to 4
 *                    -> Set to a
 *
 * b = 2;   -> Load b
 *              -> Set to 2
 *              -> Store b
4.4. 重排序的3种情况:

编译器优化:包括JVM,JIT编译器等
CPU指令重排:
所以就算编译器不发生重排,CPU 也可能对指令进行重排。
内存的“重排序”:
线程A的修改线程B却看不到,引出可见性问题

5. 可见性

5.1. 案例:演示什么是可见性问题

下面在打印结果的过程中,分析出现这几种情况
a = 3 , b =2
a = 1 , b = 2
a = 3 , b =3
b = 3 , a = 1 罕见, b 看见了a , 但是 a 还没同步来 ,此时就发生可见性问题了,
它们是直接通过 共享缓存来进行读写的
最后可加上volatile解决问题

public class FieldVisibility {

      int a = 1;
      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();
        }
    }
}
5.2. 为什么会有可见性的问题

image.png

(1). CPU 有多级缓存,导致读的数据过期
高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU
(2). 和主内存之间就多了Cache层
线程间的对于共享变量的可见性问题不是直接由多核引起的,而是
由多缓存引起的
假如:
如果所有的核心都只用一个缓存,那么也就存在内存可见性问题了
每个核心都会将自己需要的数据读到独占缓存中,数据修改
后也是写入到缓存中,然后等待刷入到主存中,所以会导致有些核心读取的值是一个过期的值

5.3. JMM的抽象:主内存和本地内存

什么是主内存和本地内存
Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,
虽然我们不再需要关心一级缓存和二级缓存的问题
但是,JMM抽象了主内存和本地内存的概念

这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM的一个抽象
是对于寄存器、一级缓存、二级缓存等的抽象。

image.png

image.png

5.4. 主内存和本地内存的关系

(1). JMM有以下规定:
所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存
工作内存中的变量内容是主内存中的拷贝
线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量
然后再同步到主内存中
主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信
必须借助主内存中转来完成
(2). 所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据
也是通过本地内存交换的,所以才导致了可见性的问题。

5.5. Happens-Before原则

(1). 单线程规则
只要是单线程内的线程数据一定能看见。

image.png

(2). 锁操作(synchronized和Lock)
下图有线程A和线程B . 线程A最后一步是解锁, 线程B第一步是加锁.
B加锁之后, 一定能够看得到A线程解锁之前的所有操作.

image.png

再例如下图中, 线程A的synchronized 代码块中, 如果释放了锁lock ,那么线程B获得锁之后, 能够看得到线程A操作的所有结构.
image.png

(3). volatile 变量 关注
volatile 只加载一个变量,但是与它相关的也会一样可见

image.png

(4). 线程启动
如下图所示, 线程a为主线程, 线程b为子线程.
那么在启动线程b,调用start方法的时候, a线程执行的所有语句, 对于线程b都是可见的.

image.png

(5). 线程JOIN
一旦执行join了, 那么join之后的语句, 一定能够看得到等待的线程执行的所有的语句.
即下图中 ,statement1中的代码, 可以看得到线程b的所有的执行语句.

image.png

(6). 传递性
如果hb(A,B)而且hb(B,C),那么可以推出hb(A,C)
(7). 中断
一个线程被其他线程interrupt,那么检测洪端(isInterrupted)或者抛出InterruptedException一定能看到
(8).构造方法
对象构造方法的最后一行指令happens-before于finalize()方法的第一行指令
(9).工具类的Happens-Before原则 关注

             1.   线程安全的容器get一定能看到在此之前的put等存入动作
             2.   CountDownLatch
             3.   Semaphone
             4.   Future
             5.   线程池
             6.   CyclicBarrier
             7.  .....

这些都需要后面再继续了解到。

6.总结

大致上就把Java 多线程的Java内存模型学习了解,这是用看某学习视频总结而来的个人学习文章。希望自己也能对Java多线基础巩固起来。

你可能感兴趣的:(Java多线程基础与使用详细篇(四)----Java内存模型)