目录
1.什么是JAVA内存模型?
Java内存模型即Java Memory Model,简称JMM。
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。
JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。
Java内存模型定义了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。
2.关于并发编程
在并发编程领域,有两个关键问题:线程之间的通信和同步。
2.1 线程之间的通信
线程的通信是指线程之间以何种机制来交换信息。
在命令式编程中,线程之间的通信机制有两种:
1.共享内存
2.消息传递
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间只通过写-读内存中的公共状态来隐式进行通信,(典型的共享内存通信方式就是通过共享对象进行通信)。
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。
2.2 线程之间的同步
同步是指程序用于控制不同线程之间操作执行顺序的机制。
在共享内存并发模型里,同步是显式进行的。
程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型。
Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
因此很多编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,就会遇到各种奇怪的内存可见性问题。
3.Java内存模型
上面讲到了Java线程之间的通信采用的是过共享内存模型.
这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存(main memory)中。
每个线程都有一个私有的本地内存(local memory)。
本地内存中存储了该线程以读/写共享变量的副本。
本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:
1. 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2. 然后,线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图来说明这两个步骤:
如上图所示,本地内存A和B有主内存中共享变量x的副本。
假设初始时,这三个内存中的x值都为0。
线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。
当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。
随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。
JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
上面也说到了,Java内存模型只是一个抽象概念,那么它在Java中具体是怎么工作的呢?
为了更好的理解上Java内存模型工作方式,下面就JVM对Java内存模型的实现、硬件内存模型及它们之间的桥接做详细介绍。
4.JVM对Java内存模型的实现
在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区
JVM中运行的**每个线程都拥有自己的线程栈**,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。
随着代码的不断执行,调用栈会不断变化。
线程栈还包含了当前方法的所有本地变量信息。
一个线程只能读取自己的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。
即使两个线程执行的是同一段代码,它们也会各自在自己的线程栈中创建本地变量,因此,每个线程中的本地变量都会有自己的版本。
比如所有原始类型(boolean,byte,short,char,int,long,float,double)的本地变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。
在两个不同线程中,一个线程可以传递一个副本给另一个线程,当它们之间是无法共享的。
堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。
不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。
5.共享对象的可见性
聊到这里,相比各位天马行空的看官都应该知道了多线程之间的对象相互之间并不可见。
那么当多个线程同时操作同一个共享对象时,如果没有合理的使用volatile和synchronization关键字,一个线程对共享对象的更新有可能导致其它线程不可见。
想象一下我们的共享对象存储在主存当中。
一个CPU中的线程读取主存数据到CPU缓存,然后对共享对象做了更改。
但CPU缓存中的更改后的对象还没有flush到主存,此时线程对共享对象的更改对其它CPU中的线程是不可见的。
最终就是每个线程最终都会拷贝共享对象,而且拷贝的对象位于不同的CPU缓存中。
下图展示了上面描述的过程。
左边CPU中运行的线程从主存中拷贝共享对象obj到它的CPU缓存,把对象obj的count变量改为2。
但这个变更对运行在右边CPU中的线程不可见,因为这个更改还没有flush到主存中:
要解决共享对象可见性这个问题,我们可以使用java volatile关键字。
Java’s volatile keyword. volatile 关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。
volatile原理是基于CPU内存屏障指令实现的。
6.不同线程对与对象的竞争
如果多个线程共享一个对象,如果它们同时修改这个共享对象,这就产生了竞争现象。
如下图所示,线程A和线程B共享一个对象obj。
假设线程A从主存读取Obj.count变量到自己的CPU缓存,同时,线程B也读取了Obj.count变量到它的CPU缓存,并且这两个线程都对Obj.count做了加1操作。
此时,Obj.count加1操作被执行了两次,不过都在不同的CPU缓存中。
如果这两个加1操作是串行执行的,那么Obj.count变量便会在原始值上加2,最终主存中的Obj.count的值会是3。
然而下图中两个加1操作是并行的,不管是线程A还是线程B先flush计算结果到主存,最终主存中的Obj.count只会增加1次变成2,尽管一共有两次加1操作。
6.volatile和 synchronized区别
volatile和synchronized特点
首先需要理解线程安全的两个方面:执行控制和内存可见。
执行控制的目的是控制代码执行(顺序)及是否可以并发执行。
内存可见控制的是线程执行结果在内存中对其它线程的可见性。
根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。
synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。
volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。
对于volatile关键字,当且仅当满足以下所有条件时可使用:
1. 能确保只有单个线程更新变量的值。
2. 该变量没有包含在具有其他变量的方法当中。
volatile和synchronized的区别:
1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
2.synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
3.volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
4.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
5.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
6.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。