JMM介绍
JMM即为JAVA 内存模型(java memory model)
JMM定义了程序中各个共享变量的访问规则,即在虚拟机中将变量存储到内存和从内存读取变量这样的底层细节
JMM作用
主内存 - 工作内存 - 线程之间的关系
如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存。
缓存一致性协议,是用于定义数据读写的规则
缓存一致性 ( MESI ) 协议
缓存一致性协议发展背景
现在的CPU基本都是多核CPU,服务器更是提供了多CPU的支持,而每个核心也都有自己独立的缓存,当多个核心同时操作多个线程对同一个数据进行更新时,如果核心2在核心1还未将更新的数据刷回内存之前读取了数据,并进行操作,就会造成程序的执行结果造成随机性的影响,这对于我们来说是无法容忍的。而总线加锁是对整个内存进行加锁,在一个核心对一个数据进行修改的过程中,其他的核心也无法修改内存中的其他数据,这样对导致CPU处理性能严重下降。
缓存一致性协议提供了一种高效的内存数据管理方案,它只会对单个缓存行(缓存行是缓存中数据存储的基本单元)的数据进行加锁,不会影响到内存中其他数据的读写。
MESI协议
状态 | 描述 | 监听任务 |
---|---|---|
M 修改(Modify) | 该缓存行有效,数据被修改了,和内存中的数据不一致,数据只存在于本缓存行中 | 缓存行必须时刻监听所有试图读该缓存行相对应的内存的操作,其他缓存须在本缓存行写回内存并将状态置为E之后才能操作该缓存行对应的内存数据 |
E 独享、互斥(Exclusive) | 该缓存行有效,数据和内存中的数据一致,数据只存在于本缓存行中 | 缓存行必须监听其他缓存读主内存中该缓存行相对应的内存的操作,一旦有这种操作,该缓存行需要变成S状态 |
S 共享(Shared) | 该缓存行有效,数据和内存中的数据一致,数据同时存在于其他缓存中 | 缓存行必须监听其他缓存是该缓存行无效或者独享该缓存行的请求,并将该缓存行置为I状态 |
I 无效(Invalid) | 该缓存行数据无效 | 无 |
JMM三大特性
- 可见性
- 原子性
- 有序性
JMM八大原子操作
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM八大指令
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
Happen-Before(先行发生规则)
Happen-Before被翻译成先行发生原则,意思就是当A操作先行发生于B操作,则在发生B操作的时候,操作A产生的影响能被B观察到,“影响”包括修改了内存中的共享变量的值、发送了消息、调用了方法等。
一般分析一个并发程序是否安全,其实都依赖Happen-Before原则进行分析。
Happen-Before的规则有以下几条
- 程序次序规则(Program Order Rule):在一个线程内,程序的执行规则跟程序的书写规则是一致的,从上往下执行。
- 管程锁定规则(Monitor Lock Rule):一个Unlock的操作肯定先于下一次Lock的操作。这里必须是同一个锁。同理我们可以认为在synchronized同步同一个锁的时候,锁内先行执行的代码,对后续同步该锁的线程来说是完全可见的。
- volatile变量规则(volatile Variable Rule):对同一个volatile的变量,先行发生的写操作,肯定早于后续发生的读操作
- 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的没一个动作
- 线程中止规则(Thread Termination Rule):Thread对象的中止检测(如:Thread.join(),Thread.isAlive()等)操作,必行晚于线程中所有操作
- 线程中断规则(Thread Interruption Rule):对线程的interruption()调用,先于被调用的线程检测中断事件(Thread.interrupted())的发生
- 对象中止规则(Finalizer Rule):一个对象的初始化方法先于一个方法执行Finalizer()方法
- 传递性(Transitivity):如果操作A先于操作B、操作B先于操作C,则操作A先于操作C
JMM原子性
例如上面八项操作,在操作系统里面是不可分割的单元。被synchronized关键字或其他锁包裹起来的操作也可以认为是原子的
JMM可见性
每个工作线程都有自己的工作内存,所以当某个线程修改完某个变量之后,在其他的线程中,未必能观察到该变量已经被修改,volatile可以保证可见性。除了volatile以外,synchronized和final也能实现可见性
volatile: volatile要求被修改之后的变量要求立即更新到主内存,每次使用前从主内存处进行读取
synchronized: synchronized保证unlock之前必须先把变量刷新回主内存
final: final修饰的字段在构造器中一旦完成初始化,并且构造器没有this逸出,那么其他线程就能看到final字段的值。
JMM有序性
因为JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序
volatile和synchronized可以保证程序的有序性
解决JMM共享对象的可见性问题: ==使用volatile==
为什么要用volatile而不用synchronized?
个人理解: volatile 可以修饰变量, 共享变量,保证了变量在所有的线程中的可见性;而synchronized不能修饰变量
volatile
Java内置关键字
volatile 是Java虚拟机提供的轻量级的同步机制
特点
可见性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28public class VolatileTest {
public static int v = 1;
// public volatile static int v = 1;
public static void main(String[] args) {
visibilityT();
v = 2;
System.out.println("main方法修改了v ->: " + v);
}
public static void visibilityT() {
new Thread(() -> {
while(v == 2) { // volatile 保证共享变量可见性
System.out.println("获取到主存的v ->:" + v);
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
不保证原子性
禁止指令重排
先看不保证原子性:
1 | public class VolatileTest { |
结果:
累加后的数值v:9109当理论v=10001
结果显示volatile不保证原子性
解决不保证原子性问题: 可以使用 synchronizd关键字或者 原子类解决
synchronized
1 | public class AtomT { |
结果:
累加后的数值v:10001当理论v=10001
原子类
位置:java.util.concurrent.atomic包下
这里使用
AtomicInteger
类java.util.concurrent.atomic
Class AtomicIntegerjava.lang.Object
java.lang.Number
java.util.concurrent.atomic.AtomicIntegerAll Implemented Interfaces:
Serializable
public class AtomicInteger
extends Number
implements Serializable一个int可能原子更新的值。
1 | public class AtomT { |
结果:
累加后的数值v:100000当理论v=10000\
使用 synchronized 或 lock 锁或原子类可以解决volatile不保证原子性问题
使用 原子类 底层都和操作系统相联系,是在内存中修改值, SafeUser类!
原子类底层是用到了CAS算法, Compare and Swap(比较并交换)
JMM有序性解释:
指令重排
源代码-> 编译器优化重排->指令并行也会重排->内存系统也会重排->执行
就是程序可以不是按你写代码的程序来跑的;
volatile可以禁止指令重排
利用:内存屏障,是一个CPU指令
内存屏障特点:
- 保证特定操作的执行顺序;
- 保证某些变量的可见性 ( 利用volatile特性) ;
使用volatile + 锁/原子类实现 JMM的可见性,原子性,有序性
volatile使用场景: 单例模式
3 个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点;
1 | // 饿汉式 |
饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。
缺点: 浪费空间
1 | // 懒汉式 |
每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点
volatile的使用 双重检锁
1 | public class Singleton { |
探究原子类的CAS底层实现
CAS
比较当前工作内存中的值跟主内存中的值做比较
如果这个值是期望值,则继续执行操作
否则一直循环, 也会导致另一个 ABA问题 (中间操作不可见)
1 | public static void main(String[] args) { |
结果:
true
true
false
1 | atomicInteger.getAndIncrement();// unsafe类 <- |
进入getAndIncrement()方法
1 | /** |
进入 vlueOffset
1 | // setup to use Unsafe.compareAndSwapInt for updates |
compareAndSet利用JNI来完成CPU指令的操作
CAS就先到这里了
ABA
丢失了中间的操作
由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题
使用原子引用解决
java.util.concurrent.atomic
Class AtomicReference
java.lang.Object
java.util.concurrent.atomic.AtomicReference参数类型
V - 此引用引用的对象的类型
All Implemented Interfaces:
Serializable
public class AtomicReference
extends Object
implements Serializable可以原子更新的对象引用
小唠嗑:
本章到这里就结束了,谢谢耐心看到这里的各位Boss,如果觉得有哪里说的不好的地方,还请高抬贵手多多原谅,不惜指教。
最后,谢谢!