JMM: Java内存模型
是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式,决定一个线程对共享变量的写入何时对另一个线程可见。
JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。
对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。解决这种共享变量在多线程模型中的不可见性问题,较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,比较合理的方式其实就是volatile关键字。
需要注意的是,JMM是个抽象的内存模型,所以所谓的本地内存,主内存都是抽象概念,并不一定就真实的对应cpu缓存和物理内存
Java对象内存模型

一个Java对象由,对象头(对象标记,类型指针,数组长度),真实数据,内存对齐三部分组成。
对象头含有三部分:Mark Word(存储对象自身运行时数据)、Class Metadata Address(存储类元数据的指针)、Array length(数组长度,只有数组类型才有)
对象标记也称Mark Word字段,用于存储对象自身运行时的数据,如hashcode,GC分代年龄,锁状态标志位,线程持有的锁,偏向线程ID,等等。
32位虚拟机
64位虚拟机 java对象头会被压缩成32位
类型指针:JVM根据该指针确定该对象是哪个类的实例化对象。
数组长度:只有数组类型才有
真实数据自然是对象的属性值。
内存补齐,是当数据不是对齐数的整数倍的时候,进行调整,使得对象的整体大小是对齐数的整数倍方便寻址。典型的以空间换时间的思想。
8种原子操作
关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:
lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
1 read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
2 load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
3 use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
4 assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
5 store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
6 write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
(1)不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
(2)不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
(3)不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
(4)一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
(5)一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
(7)如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
(8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
JMM缓存一致问题
总线加锁(性能太低)早期
cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其它cpu没法去读或写这个数据,直到这个cpu使用完数据并释放锁之后其他cpu才可能读取数据
MESI缓存一致性协议
多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,垓数据会马上同步回主内存,其它cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效
三大特性
-
原子性(Atomicity) 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:
a. 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。
b.所有引用reference的赋值操作
c.java.concurrent.Atomic.* 包中所有类的一切操作 通过CAS循环的方式来保证其原子性的
-
Atomic包 AtomicInteger
/** * Atomic保证原子性 是利用CAS来实现原子性操作的(Compare And Swap) * @author 张 /public class AtomicDemo { private static AtomicInteger i = new AtomicInteger(0); public static void main(String[] args) {// 以原子方式将当前值增加一个,相当于i++ i++非原子操作 i.getAndIncrement(); System.out.println(i); }}源码:// Unsafe java提供的类,实现了硬件级别的原子操作 private static final Unsafe unsafe = Unsafe.getUnsafe();// 变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的 private static final long valueOffset;// value是用volatile修饰的,保证线程之间的可见性 private volatile int value; /* * 使用给定的初始值创建一个新的原子整数 * @param initialValue the initial value / public AtomicInteger(int initialValue) { value = initialValue; } /* * 原子性的将当前值增加一 * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } public final int getAndAddInt(Object var1, long var2, int var4) { int var5; // compareAndSwapInt方法 CAS do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } public native int getIntVolatile(Object var1, long var2); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
CAS操作(比较并交换)
CAS 操作包含四个操作数 —— 对象内存位置(V)、对象变量的偏移量,变量预期值(A)和新的值(B)。如果内存偏移量位置的值(对象变量的内存值)与预期值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作。
CAS操作的ABA问题 通过添加时间戳、版本号进行解决。
当然CAS也并不完美,它存在"ABA"问题,假若一个变量初次读取是A,在compare阶段依然是A,但其实可能在此过程中,它先被改为B,再被改回A,而CAS是无法意识到这个问题的。CAS只关注了比较前后的值是否一致,而无法清楚在此过程中变量的变更明细,这就是所谓的ABA漏洞。
- 使用 synchronized 互斥锁来保证操作的原子性
-
-
可见性 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
-
volatile,会强制将该变量自己和当时其他变量的状态都刷出缓存。用来保证对变量修改后,能立即写回主存,从而保证共享变量的修改对所有线程是可见的。
-
synchronized,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
-
final,被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
-
-
有序性 即程序执行的顺序按照代码的先后顺序执行。
源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 ->最终执行的命令
Java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。
在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
锁的互斥和可见性
锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。
(1)互斥即一次只允许一个线程持有某个特定的锁,一次就只有一个线程能够使用该共享数据。保证了操作的原子性
(2)可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。也即**当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的。**如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
a.对变量的写操作不依赖于当前值。
b.该变量没有包含在具有其他变量的不变式中。
实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上就是保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。