juc并发编程,jmm内存模型

juc并发编程,jmm内存模型

内存、缓存、cpu

jmm三大特性:原子性、有序性、可见性。

 happen-before原则

 

 

volicate为啥可以保证有序,可见性?内存屏障

 ..

 

 cas是靠硬件实现提示效率的,来保证原子性和可见性,是基于汇编指令cmpxchg指令。

自定义原子类:

atomicreference<User> ato=new Atomicereence();

 自旋锁

unsafe类

cas的aba问题

 

Java内存模型

JMM内存模型是什么?

内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象描述,不同架构下的物理机拥有不一样的内存模型。

JMM(Java内存模型)源于CPU架构的内存模型(用于解决多处理器架构系统中的缓存一致性问题)。JVM为了屏蔽各个硬件平台和操作系统对内存访问机制的差异化,提出了JMM概念。因此它不是对物理内存的规范,而是在虚拟机基础上进行的规范从而实现平台一致性。

Java内存模型(Java Memory Model,JMM)是一种抽象的概念,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。

注意:局部变量不存在线程之间共享,它属于方法内定义的参数,不受内存模型影响。

JMM工作流程:

JMM区决定了一个线程对共享变量的写入何时对另一个线程可见。线程之间的共享变量都存在主内存中,而每一个线程包含了一个工作内存(本地),在本地内存中存储了共享变量的副本。

线程之间工作内存中的变量是不能相互访问的,必须通过主内存获取

抽象工作流程如下:

1.若线程1修改了本地内存中的共享变量,将共享变量最新结果刷新到主内存中

2.线程2到主内存中读取线程1修改之后共享变量。

三大特性:

可见性、有序性、原子性

可见性:

Java中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

有序性:

对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。

指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生"脏读",简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性,多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

public void mySort()
{
    int x = 11; //语句1
    int y = 12; //语句2
    x = x + 5;  //语句3
    y = x * x;  //语句4
}

JMM规范下,多线程先行发生原则之happens-before

在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在happens-before关系

x = 5 线程A执行

y = x 线程B执行

上述称之为:写后读

y是否等于5呢?

如果线程A的操作(x= 5)happens-before(先行发生)线程B的操作(y = x),那么可以确定线程B执行后y = 5 一定成立;

如果他们不存在happens-before原则,那么y = 5 不一定成立。

原子性:

指一个操作是不可中断的,即在多线程环境下,操作不能被其他线程干扰。

案例分析:

public class Test {
    public static void main(String[] args) {
        MyThread a = new MyThread();
        a.start();
        for (; ; ) {
             {
                if (a.isFlag()) {
                    System.out.println("进来了吗?");
                }
            }
        }
    }
}

class MyThread extends Thread {
    private  boolean flag = false;
 
    public boolean isFlag() {
        return flag;
    }
 
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag=" + flag);
    }
}

 

上面讲到了JMM工作流程,我们来结合这个程序具体来看看它是怎么工作的!

1. read(读取):从主内存读取数据

2. load(载入):将主内存读取到的数据写入工作内存

3. use(使用)从工作内存读取数据来计算

4. assign(赋值):将计算好的值重新赋值到工作内存中

5. store(存储):将工作内存数据写入主内存

6. write(写入:将store过去的变量值赋值给主内存中的变量

我们会发现线程2对flag变量的值修改了之后线程1其实是并不知道的,导致程序一直都不会输出“进来了吗?”这句话,线程1 的工作内存中其实还一直保存着共享变量原来的值。

 

volatile的底层原理是什么

结合前面讲到的JMM工作流程,当线程对共享变量的副本数据进行了修改之后,会立马写回到主内存中,此时各个线程中工作内存的共享变量副本就失效了,需要重新去主内存中读取。

那其他线程怎么知道某个线程修改了共享变量呢?我们可不可以设置一个监听的人,只要有线程改变了值我就去主内存中读取?那就要讲讲MESI了!

MESI(缓存一致性协议)—硬件方式

数据都是通过总线以流的形式传输,线程2将flag值改变之后,下一步应该是写入主存中,会经过总线,这时候线程1通过总线嗅探机制监听到flag值的改变,线程1去主存中读取flag的值,读到的值还是false,此时线程2还没有回写到主存中,此时就产生了偏差所以在数据要往主存中回写的时候store之前就加上锁,在主内存中write回写完了再释放锁。

CPU通过总线嗅探机制可以感知到数据变化从而自己缓存里的数据失效重新读取

总结

通过加锁和volatile我们可以解决多核cpu并发线程出现数据不一致、可见性问题,正式因为线程之间通信对我们完全的透明,所以在项目中会出现内存可见性的问题,追根溯源去了解原理,在开发过程中除了知道怎么用,还能知道为什么这么用!

 https://www.bilibili.com/video/BV1ar4y1x727/?p=56&vd_source=f97080956039c326589b5b26607d960b

posted @   刘百会  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示