前置

计算机内存模型

每条指令都是在 CPU 中执行的,而执行指令过程中,势必涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题:由于 CPU 执行速度很快,而从内存读取数据和向内存写入数据的过程跟 CPU 执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此,在 CPU 里面就有了 高速缓存(寄存器)。 在程序运行过程中,会将运算需要的数据从主存复制一份到 CPU 的高速缓存当中,那么, CPU 进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

但这种模式下就会存在 缓存一致性问题 ,假设在主内存中有一个变量(n=0),这时有两个线程(a,b)同时读取了这个变量,其中 a 线程修改了这个变量(n=2),但这时新的值只是写在 a 线程的缓存中,b 线程是感知不到这个变化的,所以 b 线程依然在使用 n 的旧值(n=0),所以这时 b 线程可能在进行一个错误的过程。

计算机内存模型.png计算机内存模型.png

为了解决缓存不一致性问题,在 硬件层面 上通常来说有以下两种解决方法:

  • 通过在 总线加 LOCK# 锁 的方式 (在软件层面,效果等价于使用 synchronized 关键字);
  • 通过 缓存一致性协议 (在软件层面,效果等价于使用 volatile 关键字)。

相较于缓存一致性协议,总线锁在锁住总线期间,其他 CPU 无法访问内存,导致效率低下。而缓存一致性协议在 CPU 写数据时,如果发现操作的变量是共享变量,即在其他 CPU 中也存在该变量的副本,会发出信号通知其他 CPU 将该变量的缓存行置为无效状态。因此,当其他 CPU 需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。


并发编程的核心概念

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可见性

当多个线程访问同一个共享变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性

即程序执行的顺序按照代码的先后顺序执行。

在处理器运行时,为了提高效率可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。(最终一致性

在多处理器环境下,倘若两条程序存在数据状态的依赖,但这两条程序不再同一个处理器中执行,那么就可能出现错误。

所以,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想使并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。


Java 内存模型

Java 内存模型 规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。因此也存在着像硬件层一样的情况。

原子性

在 Java 中,对 基本数据类型 的变量的 读取赋值 操作是原子性操作,即这些操作是不可被中断的:要么执行,要么不执行。
Java 内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过 synchronized 和 Lock 来实现。由于 synchronized 和 Lock 能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

可见性

Java 提供了 volatile 关键字 来保证可见性。

当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为 普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

另外,通过 synchronized 和 Lock 也能够保证可见性,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且 在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性。

有序性

在 Java 内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在 Java 中,可以通过 volatile 关键字来保证 一定的“有序性”。另外可以通过 synchronized 和 Lock 来保证 按顺序执行,但不是说明是保证“有序性”,也就是说,不能由于 synchronized 和 Lock 可以让线程串行执行同步代码,就说它们可以保证指令不会发生重排序,这根本不是一个粒度的问题。

happens-before 原则

Java 内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

    需要强调的是这里所说的是在 一个线程里,也就是说这句话要表达的是最终一致性,所以在并发层面,这个条规则是不能保证有序性的。

  • 锁定规则:一个 unLock 操作先行发生于后面对同一个锁额 lock 操作;

    同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行 lock 操作。

  • volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;

    如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

  • 传递规则:如果操作 A 先行发生于操作 B,而操作 B 又先行发生于操作 C,则可以得出操作 A 先行发生于操作 C ;

    传递性

  • 线程启动规则:Thread 对象的 start()方法先行发生于此线程的每个一个动作;

  • 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;

  • 对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始。

volatile 关键字

语义

  • 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是 立即可见

  • 禁止进行指令重排序