java内存模型JMM

1、java代码到CPU指令的过程

(1)我们最开始编写的是.java文件

(2)通过javac可以将其编译成与平台无关字节码文件(.class文件)

(3)各个平台的JVM会将字节码翻译成各个平台的机器指令。

(4)机器指令可以直接在CPU上运行,也就是最终程序的执行

2、JMM是什么?

JMM是一个标准,在不同的平台上JVM会将相同的字节码翻译成不同的机器指令,由于最终依赖处理器,不同处理器结果不一样,这样无法保证并发安全,所以需要一个标准来让多线程运行的结果可预期。

3、JMM的主要内容:重排序、可见性、原子性

4、重排序

(1)重排序发生的3种情况:编译器优化,CPU指令重排,内存的重排序(并不是真正的重排序,而是由于可见性问题导致数据没有及时的写回主存,另一个线程看到的还是原来的数据,效果看起来和重排序一样)。

(2)发生重排序的代码实例分析:

在正常情况下只可能出现的以下3种情况:x=1,y=0   x=0,y=1   x=1,y=1

而从运行结果的结果看到出现了x=0,y=0的情况,这是由于重排序造成的,既可能是由于真正的重排序(编译器,CPU的指令顺序修改),也可能是假的重排序(可见性问题)导致的。

package jmmtest;

import java.util.concurrent.CountDownLatch;

/**
 * 重排序测试
 */
public class OutOfOrderExecutionTest {

    private static int  x=0,y=0;
    private static int  a=0,b=0;
    private static CountDownLatch countDownLatch;

    public static void main(String[] args) throws InterruptedException {
        int i=0;
        while(true){
            i++;
            countDownLatch=new CountDownLatch(3);
            x=0;y=0;a=0;b=0;
            Thread th1=new Thread(new Runnable() {
                public void run() {
                    try {
                        countDownLatch.countDown();
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    b=1;
                    x=a;
                }
            });
            Thread th2=new Thread(new Runnable() {
                public void run() {
                    try {
                        countDownLatch.countDown();
                        countDownLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a=1;
                    y=b;
                }
            });
            th1.start();
            th2.start();
            countDownLatch.countDown();
            th1.join();
            th2.join();
            System.out.println("运行次数:"+i+",x="+x+",y="+y);
            if(x==0&&y==0){
                break;
            }

        }
    }
}

 

(3) 重排序可能是编译器和CPU的优化,这种优化能够有效的提高代码的运行速率。

 5、可见性

(1)可见性问题存在的原因:

CPU的多级缓存结构是导致可见性问题的根本原因

a、CPU具有多级的缓存结构,从主存(RAM)向上的各级缓存,缓存的内容越来越少,缓存的查询速率越来越快

b、各个线程之间本质上是通过主存来进行通信的,它们的缓存是独占的,不能直接进行数据的交互

c、因此当一个线程堆数据进行修改时如果只是修改自己的缓存(上层的缓存)而没有将该数据更新的更新及时的flash到主存,就会导致数据的变化对于其他线程是不可见的。

6、JMM对多级缓存的抽象

(1)JMM将CPU的多级缓存结构进行了抽象,抽象成了两层:本地内存和主存

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

(2)JMM中规定,所有的共享变量都是存储在主内存中的,同时每个线程有自己的本地内存,本地内存中的变量是主内存中变量的拷贝,线程不能直接读取主内存中变量,只能操作本地内存中的变量,然后将其同步到主内存中。线程间的数据的交互是通过线程各自的本地内存与主内存的数据同步来完成的。

7、Happens-Before原则

(1)定义:Happens-Before用于解决可见性问题,动作A发生在动作B之前,动作B能保证看见动作A,就具有happens-before性质。

 (2)具有happens-before的情景

a、单线程,单线程中前面的操作对后面一定是可见的

b、锁(synchronized和Lock),前一个同步代码块中的代码运行完成后,其修改的内容对请求到相同锁的下一个同步代码块是可见的,否则加锁就没有意义

c、 Volatile,volatile修饰的变量具有可见性

d、线程启动,子线程能够看见主线程启动子线程之前的全部操作

e、join()后的操作一定能看到等待线程的所有操作

f、传递性,如果hb(A,B)且hb(B,C),则hb(A,C)

g、中断,线程A调用了线程B的interrupt()方法,那么B线程一定能检测到中断

h、工具类:线程安全容器的get方法、CountDownLatch、Semphare、CycliBarrier、

Futrue、线程池

8、Happens-before案例分析

package jmmtest;

public class VolatileTest {
    private static int a=1;
    private static volatile  int b=2;
    public static void main(String[] args){
        while(true){
            a=1;
            b=2;
            Thread th1=new Thread(new Runnable() {
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a=3;
                    b=a;
                }
            });
            Thread th2=new Thread(new Runnable() {
                public void run() {
                    //3 3
                    //2 1
                    //2 3
                    //3 1
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("b="+b+",a="+a);
                }
            });
            th1.start();
            th2.start();
            try {
                th1.join();
                th2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

代码运行结果1:如果b=3,那么a也一定等于3

分析:因为如果线程1先运行,线程2再运行,此时由于b是volatile修饰的所以b=3 happens-before打印语句,即b=3对线程2是可见的,又在线程1中同一个线程中前面的操作a=3对后一个操作b=a具有happens-before性质,所以a=3对b=a是可见的,又由于传递性,所以a=3对打印也是可见的,因此当b=3时,a一定也等于3

代码运行结果2:即使用volatile修饰a,a=3时,b也可能等于2

分析:volatile字段只有可见性和防止重排序的功能,不具有原子性,因此代码的执行顺序可能时a=3,打印,b=a,此时的结果为a=3,b=2。

8、Volatile

(1)作用:可见性和禁止重排序

(2)volatile可见性的原理:是一种同步机制不会发生上下文的切换。

缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。

(3)而在运行synchronized代码块时,由于要等待Monitor锁,会让线程进入Blocked状态,让出CPU,发生上下文切换,在获取锁后又处于Runnable状态,当被分配到CPU时再次进行上下文的切换,上下文的切换会使缓存失效,会将修改的数据刷到主存然后再从主存中读取到最新的数据,因此synchronized也具有可见性。

(4)volatile只具有可见性而不具有原子性所以,当类似a++这种非原子操作的场景是不适用的,而如果是修饰的变量只有如a=1这种原子操作时,其起到的效果就和synchronized一样,能够保证线程安全。

(5)volatile做类似触发器的作用,来保证线程的安全:

下列的代码:一定能保证a=1,b=2不存在可见性的问题。

9、原子性

(1)定义:一组操作要么全部成功要么全部失败

(2)原子操作有哪些:

a、基本类型的赋值操作

b、引用reference的赋值操作

c、Java.concurrent.Atomic.*中所有类的原子操作

(3)原子操作+原子操作!=原子操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值