JAVA内存模型称为JMM,指JAVA虚拟机在内存中工作的方式。

其中最主要的是理解JAVA内存模型定义多线程之间的通信方式和保证共享变量的可见性,以及如何对共享变量进行同步。

在理解java内存模型之前首先必须知道一些基础的原理。

1、数据依赖性
     两个操作访问同一个变量时,如果其中的一个为写入操作,那么称这两个操作具有数据依赖性,编译器和处理器不会对有数据依赖性的操作进行重排序,也就是不会改变这两个操作的执行顺序。

2、指令重排序
    在程序执行阶段,编译器和处理器为了提高性能,会将指令进行重排序。但是JMM确保在不同的编译器和处理器上通过插入Memory Barrier (内存屏障)来禁止特定类型的编译器和处理器重排序,确保为上层提供内存可见性。
        编译器优化重排序:在不改变单线程运行结果下(as-if-serial),对语句执行顺序进行重排序。
        指令级并行重排序:对无数据依赖性的操作,处理器可以对语句对应的机器指令进行重排序。
        内存系统的重排序:处理器使用读写缓冲区和缓存,这使得加载和存储操作看上去可能是在乱序执行。

3、as-if-serial
      无论如何进行重排序,都不能改变程序在单线程下的执行结果,编译器,处理器都需要遵守as-if-serial语义。

4、内存屏障 (Memory Barrier)
    内存屏障可以禁止编译器和处理器进行重排序, 它可以保证某些操作的执行顺序不会改变。插入一条Memory Barrier会告诉编译器和处理器,任何指令都不可以与这个Memory Barrier进行重排序。
    Memory Barrier也会强制刷出缓存,比如一个写入屏障(Write-Barrier)将刷出所有Barrier之前写入缓存的数据,这样任何线程都可以读取到这些数据的最新值。
    
5、happens-before规则
    如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间要存在happens-before关系,这两个操作可以在同一线程,也可以不同线程。
     程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
     监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作
     volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
     传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。
    并不是说一个操作必须在后一个操作之前执行,只要求执行结果对后操作是可见的,而且前一个操作的排序是在后一个操作之前。


通信和同步

线程之间的通信有两种方式,分别是共享内存,和消息传递。

1、在使用共享内存来进行线程之间通信时,线程之间通过读写内存中的公共状态来进行,最常使用的就是通过共享对象来进行通信。例如通过同步来进行。(volatile并不能保证线程安全,因为volatile保证变量的可见性,但是需要运算结果不依赖于本身,或者确保只有单一线程修改变量的值
2、消息传递时,线程必须通过明确的发送消息来显式的进行通信,常用的消息传递方式就是wait() 和 notify()。

同步是指程序控制不同的线程之间操作发生顺序的机制。
在共享内存的模型里,程序员必须显式的指定某个方法或某一段代码在线程之间的执行互斥。例如可通过synchronized关键字或者读写锁等。

JAVA内存模型
java中多线程并发所采用的是共享内存模型,也就是JMM,JMM决定了一个线程对共享变量的操作对另其他线程在何时可见,JMM定义了主内存和线程内存之间的关系。共享内存存储在主内存中,每个线程都有自己的私有线程内存,存储主内存共享变量的副本,对该副本变量进行读写操作。

该图表示了内存与线程之间的关系。
多线程-JAVA内存模型

线程A和线程B对共享变量的操作会先从主内存中获取一份变量副本存到本地内存中。然后对该副本进行操作。

如果想要线程A和线程B之间进行通信,是如何进行操作的。
1、线程A从主内存获取变量的副本
2、然后对副本进行写操作
3、完成后把最新的值刷到主内存中
4、线程B从主内存中获取最新的变量值刷新到本地内存中。
多线程-JAVA内存模型

如图所示,初始状态主内存X为0,线程A获取X的副本,然后在本地内存中,对X + 1,操作完成后刷新到主内存中,然后线程B从主内存中获取变量的最新值刷到本地内存中。这样来就完成了线程A和线程B之间的通信。

JVM的实现

JVM内部将内存分为了两部分,堆区和栈区。

JVM运行的每个线程都有自己的线程栈,包含了当前线程执行的方法调用相关信息和所有本地变量。创建程序的时候,java编译器必须知道存在栈里的所有数据大小和生存周期。一个线程只能读取自己的线程栈,线程中的本地变量对其他线程是不可见的。
所有的基本类型,int float等,都存在栈区,对于他们的值各个线程都是独立的。引用对象也存在栈中,但是实际的对象存在堆内。

堆区包含了java应用创建的所有对象信息,不管对象是由哪个线程创建的,包含基本类型的封装类,都会存在堆区。
一个对象的成员方法中包含本地变量,也要存在栈区,虽然它们所属的对象在堆区。
一个对象的成员变量,无论是什么类型都会存储在堆区。
Static类型的变量和类的信息都会存在堆区。
堆中的对象可以被多线程共享,如果一个线程获得了对象的引用,便可以访问这个对象的成员变量,如果多个线程同时调用一个对象的相同方法,那么这些线程可同时访问该对象的成员变量。
多线程-JAVA内存模型
共享对象的内存可见性

如果有多个线程对共享对象同时进行操作时,如果没有合理使用volatile和synchronize关键字可能会导致一个线程对变量的操作对其他线程不可见。

例如有一个共享对象X存在主内存当中,线程A和线程B从主内存中获得了该对象的副本(CPU缓存),此时,线程A对X进行了+1操作,并且还没有刷新到主内存时,多线程之间操作是不可见的,所以线程B还认为对象X为1。
要保证对共享变量的操作对其他线程可见,可以使用volatile关键字,可以确保变量直接从主存中读取,并且也会直接更新到主存当中。但是并不会保证操作原子性。也就是多个线程同时对变量X进行操作,只能保证他们操作的X是同一块内存, 不能保证不会写入脏数据。

共享对象的竞争与同步

例如,有一个共享对象X=0存在主内存中,线程A和线程B同时对X做CPU缓存并+1操作,如果AB两个线程串行执行,那么X的值刷新到主内存为2,如果线程A和B并行操作,AB各自+1后X都为1,无论谁先刷新到主存,最后X的值都为1。要解决这个问题可以使用synchronize关键字,为操作进行加锁,来保证当前时刻只有一个线程对变量进行操作。执行完毕后会释放锁(对所有变量刷新到主内存,无论是否是volatile修饰) 让下一个线程获得该对象的使用权。


相关文章:

  • 2021-04-26
  • 2021-09-09
  • 2021-09-19
  • 2021-08-14
  • 2022-02-28
  • 2021-09-05
  • 2021-06-13
猜你喜欢
  • 2021-12-03
  • 2022-02-03
  • 2022-01-16
  • 2021-06-02
  • 2021-07-03
  • 2021-12-29
相关资源
相似解决方案