Java内存模型与线程(一)

Java内存模型与线程

TPS:衡量一个服务性能的标准,每秒事务处理的总数,表示一秒内服务端平均能够响应的总数,TPS又和并发能力密切相关。

在聊JMM(Java内存模型)之前,先说一下Java为什么要定义出JMM,那就要从Java内存模型的作用谈起,Java内存模型是用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。在此之前,C++/C直接使用物理硬件和操作系统的内存模型,因此,会由于平台或者操作系统的不同,有可能导致在一个平台上内存访问正常但是在另一台并发访问内存却会出错。

在JDK1.5之后,Java内存模型已经成熟和完善起来了。

主内存和工作内存

JMM主要的目标是定义程序中各个变量的访问规则,这里的变量包含实例对象,静态变量,数组但不包含方法上的参数和局部变量,因为后者是线程私有的,不存在共享问题。

主内存:JMM规定了所有的变量存在主内存中,他这个名字和操作系统中的主存一样但是意义不一样,此处指的是虚拟机内存的一部分,两者的对比图在下文。
工作内存:线程的工作内存保存了被线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都只能在工作内存中操作,不能直接在主内存操作变量。不同线程之间无法直接访问工作内存中的变量,变量的传递需要靠主内存完成。
线程、主内存和工作内存之间的关系
在这里插入图片描述

处理器、高速缓存、主内存之间的交互关系

在这里插入图片描述

内存之间的交互

对于工作内存的变量同步到主内存,主内存的变量的拷贝写入到工作内存的实现细节,全部采用以下8中原子操作来完成。

  • lock(锁定):作用于主内存的变量,他把一个变量标识为一条线程独占的状态。
  • unlock(解锁):作用于主内存的变量,他把处于锁定的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存的变量,将一个变量的值从主内存中传输到线程的工作内存中。以便以后的load操作。
  • load(载入):作用于工作内存的变量,他把read操作从主内存得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,他把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时就会使用到这个变量。
  • assign(赋值):作用于工作内存的变量,他把一个执行引擎接受到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令的时候就执行这个操作。
  • store(存储):作用于工作内存的变量,他把工作内存的变量的值传递给主内存中,一边随后的write操作。
  • write(写入):作用于主内存的变量,他把store操作从工作内存中得到的变量的值放入主内存的变量中。

如果要把变量从主内存复制到工作内存,就要顺序执行read和load操作,如果要把变量从工作内存同步到主内存就要顺序执行 store和write操作。注意顺序执行的含义是,read要在load之前,但是中间可以进行其他的操作例如read a; read b; load b ;load a; 同样store和write也是一样。

以上8中操作必须满足以下8个规则
1、不允许read和load、store和write操作之一单独出现。
2、不允许一个线程丢弃他最近的assign操作,也就是变量在工作内存中改变了之后必须把该变化同步回主内存。
3、不允许一个线程无原因(没有发生任何的assign操作)的将数据从工作内存同步回主内存。
4、不允许新的变量诞生在工作内存换句话说就是store要在load之前,assign要在use之前。
5、一个变量在同一时刻只允许一条线程对其进行lock操作,但是lock可以被同一线程执行多次,多次执行lock后需要执行相同次数的unlock,变量才能被解锁。
6、如果对一个变量执行lock,那将会清除工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assign操作初始化变量的值。
7、如果一个变量事先没有被lock,那就不允许对他执行unlock,还有不允许unlock一个被其他线程锁定住的变量。
8、在对一个变量执行unlock之前,必须先把这个变量同步回主内存中(执行store和write)

对于volatile变量的特殊规则

Java内存模型对于volatile专门定义了一些特殊的访问规则。在没有volatile之前,一个线程A修改一个变量的值,然后向主内存同步,另一个线程B才能看到这个变量新的值,因此我们可以说这个变量对于线程B可见。
volatile可以保证可见性但是无法保证原子性,所以在并发条件下被volatile修饰的变量也是线程不安全的。

由于volatile变量只能保证可见性,在不符合以下两个场景中,我们仍要通过加锁来保证原子性。

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

对于volatile变量的第二个语义是进制指令重排优化。普通变量仅仅会保证执行代码结果的地方都会得到正确的结果,但是不能保证会在执行方法的过程中会按具体操作的执行顺序和你写的代码的顺序一致。在硬件方面上讲,CPU允许多条指令不按程序规定的顺序分发给各个电路单元处理。但是并不是任意的顺序执行,是在保证最后能够正确的处理指令之间的依赖情况,也就是最后结果是正确的前提下。

那volatile是如何禁止指令重排的呢?
被volatile修饰的变量赋值后多执行了lock 地址 操作,这个操作相当于设置了一个内存屏障(内存屏障的作用就是不能把后面的指令重排序到内存屏障之前的位置),当只有一个CPU访问内存的时候,不需要内存屏障,但是如果有两个或更多的CPU访问同一块内存,且其中一个在观测另一个CPU访问,这时候就需要内存屏障保证一致性。它使得CPU中的缓存写入到内存,在这个过程中会让其他CPU或者内核无效化他们自己内存中的被volatile修饰的变量。

对于long和double变量的特殊规则

对于64位的数据类型,在模型中特别定义了 一条相对宽松的规定:允许虚拟机在没有被volatile修饰的long / double类型的变量可以不保证实现原子性(可以划分为对其进行两次32的数据操作),这就是非原子协定。

因为Java内存模型虽然允许虚拟机不把long和double变量的读写操作规定成原子性的,但是虚拟机可以选择把这些操作实现成原子性的操作,虚拟机有这个选择的权利,所以在实际开发中,各个平台的商用虚拟机几乎都选择把64位数据的读写操作作为原子性操作来对待,所以一般不需要把long和double变量专门声明为volatile。

原子性、可见性、有序性

其实Java内存模型具体围绕的就是如何处理这三个特性。
原子性:由Java内存模型保证原子性的操作包括read、 load、 assign、 use、 store、 write。我们大致可以认为这些操作对基本的数据类型都是具备原子性的。如果需要一个大范围的原子操作,Java内存模型提供了lock和unlock操作满足这种需求,还提供了更高层次的直接字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码指令反应到Java代码中就是同步块—synchronized关键字。

可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够感知这个修改。JMM是通过在变量修改后将新值同步回主存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性的,无论是普通变量还是volatile变量都是如此,但volatile修饰的变量保证了新值能够立即同步到主内存,以及每次使用前从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量不能保证这一点。除volatile外还有synchronized和final也可保证内存的可见性。

有序性:Java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身包含了禁止指令重排的语义,而synchronized是:一个变量在同一时刻只允许一条线程对其进行lock操作。这条规则规定了持有同一把锁的两个同步块只能串行执行。天然的有序性在Java中规定的是:如果在本线程观察的话,所有的操作都是有序的,如果在另外的线程观察另一个线程,所有的操作都是无序的。

先行先发生原则(happen-before原则)

先行先发生是指:
Java内存模型中定义的两项操作之间的偏序关系,如果说A先行于B,其实就是说在发生B操作之前,操作A产生的影响能被操作B观察到,至于这个影响可以是修改内存中的共享变量也可以是发送消息、调用某个方法等。

happen-before要求前一个操作的执行结果对后一个操作可见,并且前一个操作按照顺序排在第二个操作之前。

happen-before规则:
  1. 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作要先行发生于书写在后面的操作。准确的说,应该是控制流顺序而不是程序代码的顺序。
  2. 管程锁定规则:一个unlock操作要先行发生于lock。这里需要强调的是通一把锁。
  3. volatile变量规则:对一个volatile变量的写操作线性发生于后面对这个变量的读操作,后面是指时间的先后顺序。
  4. 线程启动规则:Thread对象的start方法先行发生于此线程的每个动作。
  5. 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,比如线程A中执行ThreadB.join();name线程B中的任意操作先行于A从ThreadB.join()操作成功返回。
  6. 线程中断规则:对线程Thread.interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  7. 对象终结规则:一个对象的初始化完成要先行于他的finalize()方法的开始。
  8. 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于C。

时间先后顺序与happen-before原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切以happen-before原则为准。

参考 :《深入理解Java虚拟机 》周志明

猜你喜欢

转载自blog.csdn.net/qq_43672652/article/details/106892300