Day281&282&283.Java内存模型 -Juc

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day281&282&283.Java内存模型 -Juc相关的知识,希望对你有一定的参考价值。

Java内存模型

一、Java代码到CPU指令

什么是底层原理?

Java代码层—>Class层—>CPU指令

image-20210528210634628

平台指的是:操作系统


编译执行流程:↓↓↓

image-20210528210756178

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

二、JVM内存结构 & Java内存模型 & Java对象模型

1、整体方向:

  • JVM内存结构 ,和Java虚拟机的运行时区域有关

  • Java内存模型 ,和Java的并发编程有关

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


2、JVM内存结构

image-20210528211915174

  • 堆Heap
    • 整个内存占用最大的,内存占用最多的
    • 存放对象的实例对象
    • 运行时动态分配
  • 虚拟机栈(VM stack)Java栈
    • 保存基本数据类型
    • 保存了对象的引用
    • 编译时就确定了大小,在运行时这个大小不会改变
  • 方法区(Method Area)
    • 存储已加载的static静态变量
    • 类信息
    • 常量信息
    • 包含永久引用—>如新建一个由static修饰的Student类
  • 本地方法栈
    • 包括了native方法
  • 程序计数器
    • 占内存区域最小
    • 保存当前线程执行到的字节码的行号数,上下文切换的时候,也会被保存
    • 包括下次执行 指令、分支、循环等异常处理

3、Java对象模型

对象自身的存储模型

因为Java是面向对象的,所以每一个对象的存储都有一定的存储结构

image-20210528214413778

  1. 首先在方法区创建出类的信息,instanceKlass
  2. 每个对象的实例都会放到堆中
  3. 若对象被调用了,那就会在栈中保存这个对象的引用

image-20210528220035934


三、Java内存模型,JMM

1、JMM是什么?

JMM: Java Memory Model

JMM是一种规范!!!

是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序


2、为什么需要JMM?

不存在JMM

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

  • 依赖处理器本身的内存一致性模型,不同处理器的运行结果不一样;

  • 无法保证并发安全

因此需要一个标准,让多线程运行的结果可预期


3、JVM是工具类和关键字的原理

image-20210528221712476


四、重排序

  • 重排序的例子演示

1、 第一种情况

/******
 @author 阿昌
 @create 2021-05-28 22:23
 *******
 *  演示重排序的现象
 *      重排序不是100%发生,所以需要多次重复,直到达到某个条件才停止
 */
public class OutOfOrderExecution {
    private static int x,y=0;
    private static int a,b=0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                a = 1;
                x = b;
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                b = 1;
                y = a;
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println("x:"+x);
        System.out.println("y:"+y);
    }
}

image-20210528223005089

线程thread1和线程thread2,他们的运行顺序会影响到最后的x、y的值;

2、第二种情况

/******
 @author 阿昌
 @create 2021-05-28 22:23
 *******
 *  演示重排序的现象
 *      重排序不是100%发生,所以需要多次重复,直到达到某个条件才停止
 */
public class OutOfOrderExecution {
    private static int x,y=0;
    private static int a,b=0;

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

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                a = 1;
                x = b;
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                b = 1;
                y = a;
            }
        });


        thread2.start();
        thread1.start();
        
        thread1.join();
        thread2.join();
        System.out.println("x:"+x);
        System.out.println("y:"+y);
    }
}

image-20210528223414717

3、第三种情况:

使用工具类CountDownLatch();

countDown()放开闸门

await()设置闸门

/******
 @author 阿昌
 @create 2021-05-28 22:23
 *******
 *  演示重排序的现象
 *      重排序不是100%发生,所以需要多次重复,直到达到某个条件才停止
 */
public class OutOfOrderExecution {
    private static int x,y=0;
    private static int a,b=0;

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

        CountDownLatch latch = new CountDownLatch(1);

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //加上栅栏
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                a = 1;
                x = b;
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                b = 1;
                y = a;
            }
        });


        thread2.start();
        thread1.start();
        //放开闸门
        latch.countDown();
        thread1.join();
        thread2.join();

        System.out.println("x:"+x);
        System.out.println("y:"+y);
    }
}

阿昌这里多次执行了10多次才出现我们需要的效果:↓↓↓,两个方法交替执行

image-20210528223931610


因为上面↑,执行了多次,这里我们对程序进行优化,让他直接执行到y=1,x=1的情况再退出

/******
 @author 阿昌
 @create 2021-05-28 22:23
  *******
  *  演示重排序的现象
  *      重排序不是100%发生,所以需要多次重复,直到达到某个条件才停止
 */
public class OutOfOrderExecution {
    private static int x, y = 0;
    private static int a, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int count = 0;//计数
        CountDownLatch latch = new CountDownLatch(1);
        for (; ; ) {
            count++;

            //数据重置
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //加上栅栏
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;
                    x = b;
                }
            });

            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    b = 1;
                    y = a;
                }
            });


            thread2.start();
            thread1.start();
            //放开闸门
            latch.countDown();
            thread1.join();
            thread2.join();

            String result = "第"+count+"次 "+ "(x:"+x+", y:"+y+")";
            //死循环结束条件
            if (x==1&&y==1){
                System.out.println(result);
                break;
            }else {
                System.out.println(result);
            }
        }
        
    }
}

image-20210528224929040


image-20210528223201991


4、重排序分析

上面的条件成立在 :↓

a=1;
x=b;

这代码的顺序不会改变的情况下才出现的情况,只存在线程之间的交替改变,而不存在线程内部的代码可能会改变的情况

image-20210528225141527

接下来,我们想要让他出现x=0,y=0的情况

//修改代码部分
if (x == 0 && y == 0) {
    System.out.println(result);
    break;
} else {
    System.out.println(result);
}

image-20210528225521272

他的极有可能发生了重排序的情况:

也就是说,出现了如下情况

y=a;
a=1;
x=b;
b=1;

他先的y赋值,再对a进行赋值,出现了代码内部的顺序颠倒问题!!

那么!!!什么是重排序

image-20210528225823878

人话版程序执行的流程跟代码书写的流程出现了不一致的情况


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

  • 对比重排序前后的指令优化

  • 没有发生重排序指令的情况:↓

image-20210528230300804


  • 进过重排序后的指令的优化情况:↓↓↓

减少了对a的读取,对a的写入指令次数

image-20210528230347959


6、重排序的3种情况

image-20210528230606109


五、可见性

1、代码演示

/******
 @author 阿昌
 @create 2021-05-29 21:24
 *******
 *      演示可见性带来的问题
 */
public class FielidVisibility {
    int a = 1;
    int b = 2;

    public static void main(String[] args) {
        while (true){
        FielidVisibility test = new FielidVisibility();
        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();

        }
    }

    private void print() {
        System.out.println("b:"+b+",a:"+a);
    }

    private void change() {
        a=3;
        b=a;
    }
}

image-20210529214255304

第四种情况是 b=3,a=1的情况,就是因为可见性发生的;

因为当线程可以读到b=3后,如果在主存中读不到线程a=3这个赋值操作,那么就会去读原来的初始化值

image-20210529214238452

那么如何解决这个线程可见性出现的问题???


2、解决方案

使用 volatile 修饰变量,强制每次线程读取的时候都是被修改后的值

/******
 @author 阿昌
 @create 2021-05-29 21:24
 *******
 *      解决可见性问题方案:使用 volatile
 */
public class FielidVisibility {
    volatile int a = 1;
    volatile int b = 2;

    public static void main(String[] args) {
        while (true){
        FielidVisibility test = new FielidVisibility();
        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();

        }
    }

    private void print() {
        System.out.println("b:"+b+",a:"+a);
    }

    private void change() {
        a=3;
        b=a;
    }
}

image-20210529215140569

那为什么使用了 volatile 就可以解决可见性问题呢?

image-20210529215335715

image-20210529215354680


3、为什么会有可见性问题

越上面的缓存容量越小,但是执行速度越快;反之越下面的缓存容量越大,但执行速度越慢

image-20210529215442419

另外,L1cache缓存L2cache的一部分数据,当core4核心去修改L1cache的值后,可能core1核心的L1cache的值是从L2cache里面取,所以就会导致各核心的值会出现 不一致性


因此,最主要的原因是:

由于CPU有多层缓存,导致读数据过期问题

image-20210529220019373

image-20210529220038278


4、Java通过JMM来解决可见性问题

1)什么是主内存&本地内存

image-20210529220303315

image-20210529220413078


线程工作在WorkingMemory中,他不与主内存直接沟通,后事通过Buffer缓冲区

他们的交互只能通过主内存,不能直接互通;

image-20210529220443792


↓↓下图各个线程通过操作指令区和主内存交互,这样子就可以是的多个线程都能互通

image-20210529220659899


2)主内存和本地内存的关系

image-20210529221033773


5、能保证 可见性 的措施

image-20210530193912276


六、happens-before原则

1、什么是happens-before

也就是说,如果第一份代码运行了,那么第二份代码就一定能看到第一份代码运行的行为

image-20210529221312883

image-20210529221700191


2、什么不是happens-before

image-20210529221449978

image-20210529221724172


3、happens-before原则的内容

他可以帮助自己可见性,也可以帮助其他周围的变量等也具有可见性

image-20210529224747098

image-20210529225032915


  • 单线程原则

若是单线程执行,那么后面的语句肯定能看到前面的语句做了的行为结果,保证的情况是 没有发生重排序问题

image-20210529222142569

image-20210529222058438

image-20210529222312402


  • 锁操作(synchronized 和 Lock)

image-20210529222511578

image-20210529222759152


  • volatile变量

image-20210529222930273


  • 线程启动

image-20210529223130483


  • 线程join

让下面的代码等待one、two两个线程执行完,再执行

image-20210529223225353

image-20210529223350202


  • 传递性

如果hn(A,B)而且hb(B,C),那么可以退出hb(A,C)

也就是第一行运行,第二行能看到第一行,那第二行运行,第三行能看到第二行;

那么第三行就能看到第一行运行


  • 中断

一个线程被其他线程interrupt时,那么检测中断(isInterrupted)或者抛出InterruptedException一定能看到。

就是说,A被中断了,那么B线程就能检测可见性看到A被中断了。


  • 构造方法

对象构造方法的最右一行指令 happens-before于finalize()放啊的第一行指令


  • 工具类的Happens-Before原则

image-20210529224220345


七、volatile关键字

1、什么是volatile

  • volatile是一种同步机制,类似与Lock和Synchronized相关,但是他更轻量级,因为使用volatile并不会发生上下文切换等开销很大的行为

  • 如果一个变量被volatile修饰,那么JVM会认为这个变量可能会被并发修改

  • 因为开销小,所有能力小;他做不到像synchronized那样原子保护,使用的场景比较有限


2、不适用场景

  • a++ 场景
/******
 @author 阿昌
 @create 2021-05-30 17:27
 *******
 *      不适用volatile场景
 */
public class NoVolatile implements Runnable {

    volatile int a;

    AtomicInteger realA = new AtomicInteger以上是关于Day281&282&283.Java内存模型 -Juc的主要内容,如果未能解决你的问题,请参考以下文章

(字典树3道水题)codeforces 665E&282E&514C

人工智能深度学习机器学习常见面试题281~300

#282(div2) B. Modular Equations

《安富莱嵌入式周报》第282期:CMSIS-DSP手册引入计算图,树莓派单片机RP2040超频到1GHz,COBS字节编码算法,纯手工为PS1打造全新亚克力外壳

《安富莱嵌入式周报》第282期:CMSIS-DSP手册引入计算图,树莓派单片机RP2040超频到1GHz,COBS字节编码算法,纯手工为PS1打造全新亚克力外壳

Hadoop家族