并发编程(四)JMM内存模型

 文章更新时间:2021/07/10

一、什么是JMM

  概念:Java内存模型是一种抽象的概念,并不真实存在,定义了Java程序在各种平台下对内存访问的机制及规范

  PS:线程程序运行的载体

  图示:

  解析:首先我们要明确一点:内存模型主要是影响线程共享的内存可见性问题,Java线程之间的通信由Java内存模型【JMM】控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见

  从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:

  • 线程之间的共享变量存储在主内存(main memory)
  • 每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

  PS:本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

主内存

  主要存储的是Java实例对象所有线程创建的实例对象都存放在主内存中

  PS:由于是共享数据区域,多条线程对同一个变量进行访问可能会发生线程安全问题。

工作内存

  主要存储当前方法的所有本地变量信息【工作内存中存储着主内存中的变量副本拷贝】

  每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。

  PS:由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

  PS:在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存。

二、JMM在线程通信中起到的作用

  线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

  • 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
  • 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

  下面通过示意图来说明这两个步骤:

   解析:如上图所示,本地内存A和B有主内存中共享变量x的副本,那么在实现通信时,JMM内存模型是这样展现的:

  • 假设初始时,这三个内存中的x值都为0。
  • 线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。
  • 当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。
  • 随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1,线程通信完成

三、JMM内存模型与JVM内存结构的区别

JMM【Java内存模型】指的在java程序运行过程中,计算机有主内存,每个java线程有自己的工作内存。【主要针对多线程使用内存的一种抽象概念,看做一种规范】

JVM【Java内存结构】讲的是Java虚拟机内存的结构划分,包括堆区,栈区,方法区等。主要针对java内存的管理

  PS:JMM是抽象概念,并不真实存在。

  PS:在概念理解时,尽量不要把JVM和JMM结合起来联想,这样很容易混淆,JMM是一种针对多线程的抽象概念和规范,我们应该先理解清楚了概念再整合实际内存进行理解。

四、数据同步的八大原子操作

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

五、并发编程的可见性,原子性与有序性问题

原子性

  定义:原子性指的是一个操作是不可中断的,不可分割的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。

可见性

  定义:可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值

有序性

  定义:程序执行的顺序按照代码的先后顺序执行

  对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,但对于多线程环境,则可能出现乱序现象【指令重排导致】,重排后的指令与原指令的顺序未必一致。

六、JMM如何解决原子性、可见性、有序性问题

原子性问题

  除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过synchronizedLock实现原子性。【synchronized和Lock能够保证任一时刻只有一个线程访问该代码块】

可见性问题

  • volatile关键字可以保证可见性。
    • 当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。
  • synchronizedLock也可以保证可见性。
    • 因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。

有序性问题

  可以通过synchronizedLock来保证有序性。

  synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

七、并发编程涉及的一些重要原则

as-if-serial语义

  定义:不管怎么重排序【编译器和处理器为了提高并行度】,(单线程)程序的执行结果不能被改变。

  为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序, 因为这种重排序会改变执行结果。

  PS:如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

happens-before原则

1、程序顺序原则

  在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

2、锁规则

  解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,即若对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

3、volatile规则

  volatile的可见性保证:变量的写,先发生于读

  简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻不同的线程总是能够看到该变量的最新值。

4、线程启动规则

  线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start()方法之前修改了共享变量的值,那么当线程B执行start()方法时,线程A对共享变量的修改对线程B可见。

5、传递性

  A先于B ,B先于C 那么A必然先于C。

6、线程终止规则

  场景:主线程A执行时,B线程调用Thread.join()方法。

  • 假设在线程B终止之前修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见

7、线程中断规则

  对线程interrupt()方法的调用要比代码检测中断事件先发生,可以通过Thread.interrupted()方法检测线程是否中断。

8、对象终结规则

  对象的构造函数执行,结束先于finalize()【当垃圾回收器将要回收对象时执行】方法。PS:不推荐使用finalize()方法

 

posted @ 2020-07-06 17:08  有梦想的肥宅  阅读(1048)  评论(0编辑  收藏  举报