java内存模型(Java Memory Model)规范:
JMM规定了所有的变量都存储在主内存(Main Memory)中,这里所说的变量指的是实例变量和类变量。不包含局部变量,局部变量的线程私有的,不存在竞争问题。
每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝。
线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量(volatile变量仍然有工作内存的拷贝,但是由于它特殊的操作顺序性规定,所以看起来如同直接在主内存中读写访问一般)。
不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。
在Java中,所有实例对象、静态变量和数组元素都存储在堆内存中,堆内存在线程之间共享,局部变量,方法定义参数和异常处理器参数不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。
Java线程之间的通信有Java内存模型(JMM)控制,JMM决定了一个线程对共享变量的写入何时对另一个线程可见。Java Memory Model(Java内存模型), 围绕着在并发过程中如何处理可见性、原子性、有序性这三个特性而建立的模型。
JMM的抽象示意图如下:
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
备注:
对于JMM和JVM本身的内存模型,参照《深入理解java虚拟机》的解释,这两者没有关系。如果一定要勉强对应,那么从变量、主内存、工作内存的定义来看,主内存主要对应java堆中的对象实例数据部分,而工作内存则对应于虚拟机栈中的部分数据。从更低层次上说,主内存就是物理内存,而为了获取更好的读写速度,虚拟机(甚至是硬件本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存中,因为运行时主要访问---读写的是工作内存。
本地内存并不是把主内存中所有对象拷贝出来,而是存放的该线程使用到的变量。线程对变量的所有操作(读取、赋值等)都只能在工作内存中,完了之后写入主内存中。
其实volatile修饰的变量也是在工作内存中有副本的,所以在各个线程的工作内存中,volatile变量也是可能存在互不相同的时候。只不过volatile变量在每次使用之前都会先刷新,也就是说对于执行引擎而言是看不到刷新前的不一致现象的。这样的操作顺序很特殊,让volatile变量看起来就像是在直接操作主内存一样。
变量在内存中的工作过程:
内存间的交互操作
lock、unlock、read、load、use、assign、store、write,8个操作具有原子性(除开64位的数据类型:long和double需要划分两次32的操作外)
lock和unlock区别:lock作用于主内存变量,把一个变量标识为一条线程独占的状态;unlock也作用于主内存变量,把一个处于锁定状态的变量释放
read和load区别:read作用于主内存变量,将它的值从主内存中取出并传输至线程的工作内存;load作用于工作内存变量,将read读出的变量值放入工作内存的变量副本中
use和assign的区别:use作用于工作内存变量,把它的值传递给执行引擎,每当虚拟机遇到需要使用这个变量的字节码指令的时候就会执行这个操作;assign也作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存变量,每当虚拟机遇到给这个变量赋值的字节码指令的时候就会执行这个操作。
store和write区别:store作用于工作内存变量,将它的值传输到主内存;write作用于主内存变量,将store传过来的值放入主内存变量中。
volatile关键字(没想到一个volatile关键字这么多内容):
保证此变量对所有线程是立即可见的、禁止指令重排序,保证代码的执行顺序和程序的执行顺序一致。
详细分析:
假设有线程T和volatile变量V和volatile变量W,那么在进行read、load、use、assign、store、write的时候会有如下的规则:
同一个线程操作同一个变量时,use的前一个动作必须是load,同时只有在马上要use的时候才能进行load(这就保证了每次使用变量前,都能从主内存得到最新的值)
同一个线程操作同一个变量时,store的前一个动作必须是assign,同时只有在马上要store的时候才能进行assign(这就保证了每次变量一更改,主内存马上就能得到更新)
假设动作A是线程T对变量V实施的use或者assign,假设动作F是和A相关的load或者store,假设动作P是和F相关的read或者write;假设动作B是线程T对变量W实施的use或者assign,假设动作G是和B相关的load或者store,假设动作Q是和G相关的read或者write;那么,如果A先于B,则P先于Q(也就是此时不会出现指令重排序,代码的执行顺序与程序的执行顺序一致)。
volatile和synchronized对比
关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法和代码块。之前一直说尽量少用synchronized修饰方法,但是现在新版的Java之后,这个效率得到了很大程度提升。
多线程访问volatile不会发生阻塞、而synchronized会出现阻塞
volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数锯做同步
volatile解决的是变量在多个线程之间的访问性,synchronized解决的是多个线程之间访问资源的同步性
内存可见性:
内存可见性是指当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化。也就是说,如果线程 A 修改了共享变量 V 的值,那么线程 B 在使用 V 的值时,能立即读到 V 的最新值。
内存可见性的解决方案:
加锁(synchronized)或使用volatile关键字。
为什么加锁可以保证内存的可见性:
1.线程获取锁。
2.清空工作内存
3.从主内存拷贝共享变量最新的值到工作内存为副本
4.执行代码
5.将修改后的副本的值刷新回主内存
6.线程释放锁。
为什么volatile可以保证内存的可见性:
使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。
volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。
volatile的原子性问题:
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
在多线程环境下,volatile 关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性。也就是说,多线程环境下,使用 volatile 修饰的变量是线程不安全的。
要解决这个问题,我们可以使用锁机制,或者使用原子类(如 AtomicInteger)。
AtomicInteger中保存的值value是用volatile修饰的,但是这并不能保证原子性。AtomicInteger中保证原子性是通过使用的CAS算法实现。例如:atomicInteger.incrementAndGet()方法:
这里需要特别说明一下:
volatie修改的变量i在多个线程中执行i++操作是不能保证数据的正确性的!!!!
i++不是一个原子操作。