并发编程之并发理论篇--内存模型

一、Java内存模型的介绍

线程安全是指在多个线程同时访问同一个对象时,无论线程调度和交替运行的方式如何,以及是否需要额外的同步或协调操作,该对象的行为都能够正确地获得预期的结果。

根据《深入理解Java虚拟机》所提供的定义,线程安全的对象可以保证在多线程环境下的正确性。这意味着对象的方法或操作可以被多个线程并发地调用,而不会导致数据的不一致性或产生竞态条件等问题。

线程安全问题通常由于主内存和工作内存之间的数据不一致性和指令重排序导致。主内存是所有线程共享的内存区域,用于存储对象的实例和变量等数据。而工作内存是每个线程私有的内存区域,用于存储对主内存中数据的副本。

  • 为了提高性能,编译器和处理器可能会对指令进行重排序,这可能导致在不同的线程中看到的指令执行顺序不一致。此外,在多线程环境下,线程之间相互协作需要进行通信,用来告知彼此的状态和当前的执行结果。
  • 为了解决线程安全问题,理解Java内存模型(JMM)至关重要。JMM定义了线程之间如何交互以及如何与主内存进行数据交互的规则。它提供了原子性、可见性和有序性等概念,以确保线程之间对共享变量的访问是正确、可见和有序的。

通过理解JMM的规则以及主内存和工作内存之间的交互机制,开发者可以采取适当的同步手段,如使用锁、volatile关键字、原子类等,来实现线程安全的程序设计,避免数据不一致性和竞态条件的问题。

二、内存模型抽象结构

内存模型是计算机系统中用来描述线程间通信和同步的抽象结构。在并发编程中,线程之间需要通过某种机制来进行通信和同步,而内存模型定义了线程如何访问和操作共享变量的规则。

共享变量是指在多个线程之间可以被访问和修改的变量。在Java程序中,所有实例域、静态域和数组元素都属于共享变量,它们存储在堆内存中,可以被所有线程访问到。而局部变量、方法定义参数和异常处理器参数等则不属于共享变量,它们是线程私有的,不会在线程间共享。

Java内存模型(Java Memory Model,JMM)是一种共享内存模型,它规定了线程如何与主内存和工作内存进行交互。每个线程都有自己的工作内存,其中包含了从主内存中读取的共享变量的副本。线程对共享变量的读写操作都是在工作内存中进行的,并在适当的时候将变量的值同步回主内存。

JMM定义了线程对共享变量的读写操作具有原子性、可见性和有序性这三个特征。原子性保证了对于单个共享变量的读写操作是不可分割的,要么完成,要么不完成,没有中间状态。可见性保证了一个线程对共享变量的修改对其他线程是可见的,即当一个线程修改了共享变量的值后,其他线程能够立即看到最新的值。有序性保证了程序执行的顺序与代码的顺序一致,即程序的执行结果是可以预测的。

为了解决线程间的通信和同步问题,JMM提供了一些机制,如锁和volatile关键字。锁机制可以控制不同线程之间对共享变量的访问顺序,从而实现线程间的同步。而volatile关键字可以保证对于每次对volatile变量的读写操作都能强制刷新到主内存,从而对所有线程都可见。

总之,内存模型是描述线程间通信和同步的抽象结构,Java内存模型是一种共享内存模型,定义了线程如何访问和操作共享变量,以及如何保证线程间通信和同步的正确性。

三、主内存与工作内存

主内存是计算机系统中存储所有变量的地方,它由物理内存构成,并存储程序的代码和数据。由于主内存的访问速度相对较慢,无法与处理器的速度保持一致。

为了解决速度矛盾问题,引入了高速缓存。高速缓存位于处理器内部,读写速度比主内存快得多。它用于缓存主内存中经常使用的数据和指令,以提高处理器的读写操作速度。

然而,引入高速缓存也带来了一个新问题,即缓存一致性。当多个缓存共享同一块主内存区域时,如果它们的缓存副本不一致,就会导致数据不一致的情况。因此,需要一些协议来解决这个问题,例如MESI(修改、独占、共享、无效)协议。

所有的变量都存储在主内存中,每个线程还有自己的工作内存。工作内存可以存储在高速缓存或寄存器中,保存了该线程使用的变量的主内存副本拷贝。

线程只能直接操作工作内存中的变量,对变量的读写操作都是在工作内存中进行的。如果线程需要与其他线程共享变量的值,需要通过主内存来进行变量值的传递。

当线程需要读取变量时,它首先从主内存中获取变量的副本到自己的工作内存中操作。修改后的值在合适的时机刷新回主内存,使其他线程能够获取到最新的值。

通过主内存和工作内存之间的数据交互以及缓存一致性协议的配合,可以保证多线程环境下对共享变量的操作的一致性和正确性。

四、内存间交互操作

主内存和工作内存之间进行数据交互的操作主要涉及变量的读取、修改和写回。下面是一些常见的内存间交互操作:

  • lock(锁定):作用于主内存中的变量,将一个变量标记为线程独占状态,确保只有一个线程可以访问该变量。
  • unlock(解锁):作用于主内存中的变量,释放一个被锁定的变量,使其他线程可以访问该变量。
  • read(读取):作用于主内存中的变量,从主内存中读取一个变量的值,并将其传输到线程的工作内存中。它为后续的 load 操作提供数据。
  • load(载入):作用于工作内存中的变量,将读取操作获取到的值放入线程的工作内存中的变量副本中。
  • use(使用):作用于工作内存中的变量,将工作内存中的变量值传递给执行引擎,在执行引擎中使用该值。
  • assign(赋值):作用于工作内存中的变量,将执行引擎接收到的值赋给工作内存中的变量。在遇到变量赋值指令时执行该操作。
  • store(存储):作用于工作内存中的变量,将工作内存中的变量值传输到主内存中,以便后续的 write 操作使用。
  • write(写操作):作用于工作内存中的变量,将store(存储)操作获取到的值放入主内存的变量中。

这些操作保证了在多线程环境中对变量的读写和操作的一致性和可见性。通过使用锁和内存屏障等机制,Java 内存模型确保了线程间的数据同步和正确的执行顺序,从而避免了由于多线程并发访问导致的数据不一致或错误的问题。

五、内存模型三大特性

Java内存模型(Java Memory Model,JMM)是一种规范,用于描述多线程程序中的内存访问和操作行为。它确保了原子性、可见性和有序性这三个重要的特性。

1、原子性:

原子性指的是一个操作要么全部执行完毕,要么完全不执行,不存在中间状态。在Java内存模型中,read、load、use、assign、store、write、lock和unlock等操作都具有原子性。但是对于64位数据(如long和double),虚拟机允许将其读写操作分为两次32位的操作,因此这些操作可能不具备原子性。

需要注意的是,int等原子类型的变量在多线程环境中也可能出现线程安全问题。例如,在多个线程对一个int类型变量进行自增操作时,由于自增操作不是原子操作(包含多个步骤:读取变量值、加一、写回变量),可能导致结果不正确。

2、可见性:

可见性指的是当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性。可以使用volatile关键字、synchronized关键字或final字段来实现可见性。

使用volatile修饰的变量可以保证内存可见性,但并不能保证操作的原子性。对于保证变量的原子性,需要满足两个条件:运算结果不依赖于变量的当前值,或只有一个线程修改变量的值;变量不与其他状态变量共同参与不变约束。

3、有序性:

有序性指的是在一个线程内观察,所有操作都是有序的;但在多线程并发执行时,操作可能会被重排序。Java内存模型允许编译器和处理器对指令进行重排序,这不会影响单线程程序的执行,但可能影响多线程并发执行的正确性。

为了保证有序性,可以使用volatile关键字或synchronized关键字。volatile关键字通过添加内存屏障来禁止指令重排;synchronized关键字则保证每个时刻只有一个线程执行同步代码,从而实现顺序执行。

总之,Java内存模型通过原子性、可见性和有序性这三个特性来确保多线程程序的正确性和可靠性。在编写多线程程序时,需要合理地应用这些特性,避免出现线程安全问题。

总结

  • synchronized:具有原子性,有序性和可见性
  • volatile:具有有序性和可见性
  • final:具有可见性

六、内存屏障

在Java内存模型(JMM)中,为了保持多线程程序的正确性,JMM允许编译器和处理器对指令序列进行重排序,前提是不能改变程序的语义。然而,如果我们希望阻止重排序,可以添加内存屏障(也称作内存栅栏或内存栅障)。

JMM定义了四种类型的内存屏障:

  • LoadLoad屏障:禁止下面的普通读操作和上面的普通读操作重排序。确保上面的读操作先于下面的读操作。
  • StoreStore屏障:禁止上面的普通写操作和下面的普通写操作重排序。确保上面的写操作先于下面的写操作。
  • LoadStore屏障:禁止下面的普通写操作和上面的普通读操作重排序。确保上面的读操作先于下面的写操作。
  • StoreLoad屏障:是一个全能型屏障,它禁止了上面的普通写操作和下面的volatile读/写操作重排序。同时,它还保证了上面的所有数据对其他处理器可见,避免了内存可见性问题。

这些内存屏障通过在指令序列中插入适当的屏障指令,来限制编译器和处理器对指令序列的重排序,从而保证多线程程序的正确性和一致性。 

Java编译器会根据volatile内存语义的需求,在适当的位置插入内存屏障指令来禁止特定类型的处理器重排序。具体地,

  • 在每个volatile写操作的前面插入一个StoreStore屏障;
  • 在每个volatile写操作的后面插入一个StoreLoad屏障;
  • 在每个volatile读操作的后面插入一个LoadLoad屏障;
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

需要注意的是:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障,以确保volatile变量的可见性和有序性。

然而,请注意,由于编译器无法找到最优的指令插入位置,JMM采取了保守策略,为每个volatile写操作和读操作插入不同类型的内存屏障,以最大程度地确保volatile内存语义的正确实现。

你可能感兴趣的:(Java进阶篇,java,学习)