一篇文章彻底搞懂JMM和并发编程三大特性(深度剖析,java并发编程必须了解)


前言

并发编程就是充分利用多核CPU的计算能力,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升。


一、想知道为什么需要JMM,首先需要知道下面几个问题

1、什么是并发和并行、串行?

他们2个的目标都是最大化CPU的使用率。
并发:
同一时刻,多个任务在同一个CPU核上, 只能有一条指令在执行,但是多个线程指令快速的轮动交替执行,给人感觉任务是同时执行的。
在这里插入图片描述

并行:
同一时刻,多个处理器或多核处理器同时处理多个任务,是真正意义上的同时进行
在这里插入图片描述

串行:
有N个任务,由一个线程按顺序执行,由于任务、方法都在一个线程执行,所以不存在线程安全问题。

打个比方
并发=2个队列和一台咖啡机。
并行=2个队列和二台咖啡机。
串行=1个队列和一台咖啡机。


2、并发编程会贷来什么问题

我们知道并发编程为了保证数据的安全,必须满足以下三个特性
原子性,指的是在一个操作中CPU 不可以在中途暂停然后再调度,要么不执行,要么就执行完成
可见性,指的是多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改后的值
有序性,指的是程序执行的顺序按照代码的先后顺序执行,而不能随便重排,导致程序出现不一致的结果。

总结下来就是:并发编程会带来原子性问题、可见性问题、有序性问题
缓存一致性问题其实就是可见性问题
处理器优化是可以导致原子性问题的;
指令重排即会导致有序性问题;


3、CPU和缓存一致性

1、要知道计算机在执行程序员的时候,每条指令都是在CPU中执行的,而执行的时候,又免不了要和数据打交道,而计算机上的数据,都是存放在计算机的物理内存上的。
2、当内存的读取速度和CPU的执行速度相比差别不大的时候,这样的机制是没有任何问题的,可是随着CPU技术发展,CPU的执行速度和内存的读取速度,差距越来越大,导致CPU的每次操作内存都要耗费很多时间等待。
3、所以在CPU在物理内存上增加了高速缓存,这样程序执行过程也发生了改变,变成了程序在运行过程中,会将运算所需要的数据从主内存复制到一份到CPU的高速缓存中,当CPU进行计算时就直接从高速缓存中读取数据和和写数据了,当运算结束时再将数据刷新到主内存就可以了。
4、但是当CPU出现多核概念后,每个核都有自己的一套缓存并且还可以支持多线程,最终演变成多个线程访问进程中的某个共享内存,且多个线程在不同的核心上,则每个核心都会在各自的缓存中保留一份共享 内存的缓冲,因为多核是可以并行的,这样就会出现多个线程各自写各自的缓存,导致各自的缓存内容出现不一致

注意:正是因为这个现象,所以造成了并发编程时出现可见性的问题。


4、处理器优化和指令重排

为了使处理器内部的运算单元能够被充分利用,处理器可能会对程序代码进行乱序的执行,这就是处理器优化。
除了主流的的处理器会对代码进行乱序处理,很多编程语言也会有类似的变化,如java虚拟机的即时编译器(JIT)也会做指令重排。
可想而知,如果任由处理器优化和编译器的指令重排,就可能会在并发编程实际开发中,出现各种各样的问题。
如:原子性问题和有序性问题。


二、JMM到底是什么(java内存模型)?

1、java为了保证并发编程中可以满足原子性、可见性、有序性、于是诞生了一个重要的概念,那就是java内存模型(注意,他和JVM内存模型不是一个东西),java内存模型定义了共享内存系统中多线程多谢操作的行为规范
2、通过这些规则来对内存的多写操作,从而保证指令的正确性,它解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性、有序性。

简单总结就是,java 内存模型定义了共享系统中多线程中多谢操作的行为规范,从而解决并发编程的问题而存在。

1、JMM工作交互图

在这里插入图片描述

2、JMM组成部分

1、主内存

主要存储的是java对象实例,所有线程创建的线程都存放在主内存中,不管是实例对象是成员变量还是方法中的局部变量,当然也包括共享类的信息、常量、静态变量。由于这是共享数据区域,多条线程对同一变量进行访问访问就可能会发生线程安全问题。


2、工作内存

主要储存当前方法的所有本地变量信息(工作内存中储存着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程都是不可见的,所以无法互相访问工作内存,因此存储在工作内存的数据是不存在安全问题的。


3、JMM存在的必要性

在这里插入图片描述

假设主内存中存在一个共享变量X,现在A和B线程2个线程分别对该变量赋值,接下来A对x进行赋值为2,而B线程想要读取x的值,那么这时候B线程读取到的值到底是A线程更新后的值还是原本主内的值呢?
答案是不确定的,即有可能读取到A更新的值,也有可能读取的更新之前的值。

原因
是因为每个线程的工作内存是私有的,如线程A改变x值时,首先是将变量从主内存COPY到A线程的工作内存中,然后对变量进行操作,操作完成后,再将变量x写回主内存,而B线程也是类似的情况,这样就有可能造成主内存和工作内存的数据存在一致性问题

假设1:
A线程修改完后正要将数据写回主内存,而B线程此时正在读取主内存,即将x=1,COPY到自己B线程的工作空间,这样B线程就读取到的x值=1.

假设2:
但是如果A线程已经将结果写回了主内存,这时候B才开始读取的话,拿到的值就是x=2。


4、数据同步的八大原子操作(即主内存和工作内存之间的交互协议)

1、lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
2、read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
3、load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
4、use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
5、assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值,赋给工作内存的变量
6、store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
7、write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量
8、unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
在这里插入图片描述


三、可见性、原子性、有序性问题

1、原子性

指的是在一个操作中CPU 不可以在中途暂停然后再调度,要么不执行,要么就执行完成

原子性案例:
对基础属性赋值属于原子操作

X=10;  //原子性(简单的读取、将数字赋值给变量)

以下均不属于原子操作,在没有处理的情况下,在多线程环境均不安全

Y = x;  //变量之间的相互赋值,不是原子操作
X++;  //对变量进行计算操作
X = x+1;
User user = new User();

java里如何解决原子性问题?
通过 synchronized 关键字保证原子性。
通过 Lock保证原子性。
通过 CAS保证原子性。


2、可见性

指的是多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改后的值

java如何保证可见性
通过 volatile 关键字保证可见性。
通过 内存屏障保证可见性。
通过 synchronized 关键字保证可见性。
通过 Lock保证可见性。
通过 final 关键字保证可见性

可见性案例:
可以看到2个线程都执行了,但是B线程修改了flag的值为true,但是A线程依旧无法感知到。

/**
 * @author ljc
 *
 * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
 */
public class VisibilityTest {

    private boolean flag = true;

    public void refresh() {
        flag = false;
        System.out.println(Thread.currentThread().getName() + "修改flag");
    }

    public void load() {
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
        int i = 0;
        while (flag) {
            i++;
            //TODO  业务逻辑

        }
        System.out.println(Thread.currentThread().getName() + "跳出循环: i=" + i);
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityTest test = new VisibilityTest();

        // 线程threadA模拟数据加载场景
        Thread threadA = new Thread(() -> test.load(), "threadA");
        threadA.start();

        // 让threadA执行一会儿
        Thread.sleep(1000);
        // 线程threadB通过flag控制threadA的执行时间
        Thread threadB = new Thread(() -> test.refresh(), "threadB");
        threadB.start();

    }
}

输出结果:
threadA开始执行.....
threadB修改flag

3、有序性

指的是程序执行的顺序按照代码的先后顺序执行,而不能随便重排,导致程序出现不一致的结果。
一条线程串行执行是不会出现有序性问题,但是如果是多线程环境就可能会出现。

java如何保证有序性?
通过 volatile 关键字保证可见性。
通过 内存屏障保证可见性。
通过 synchronized关键字保证有序性。
通过 Lock保证有序性。


总结

总的来说JMM就是为了解决三大特性、原子性、有序性、可见性的;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

未闻花名丶丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值