JVM – 内存模型
JMM(java memory model)定义了一套在多线程读写共享数据时,对数据的可见性、有序性和原子性的规则和保障。
1、原子性
使用synchronized关键字保证代码块原子性,反映到现实生活,上厕所时贴上自己的名字表示里面有人了,你不能进来。
问题提出: 两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次,结果是0嘛?
问题分析: 以上结果可能是正数、负数、0;java中对静态变量的自增,自减并不是原子操作。
例如对于i++(i为静态变量),实际会产生如下的JVM字节码指令
getstatic i//获取静态变量i的值
iconst_i //准备常量1
iadd //自增
putstatic i //将修改后的值存入静态变量i
内存模型如下,完成静态变量的自增需要在主存和线程内存中进行数据交换
- 出现负数的情况
//假设i的初始值为0
getstatic i//线程1-获取静态变量i的值,线程内i=0
getstatic i//线程2-获取静态变量i的值,线程内i=0
iconst_i //线程1-准备常量1
iadd //线程1-自增 线程内i=1
putstatic i //线程1-将修改后的值存入静态变量i,静态变量i=1
iconst_i //线程2-准备常量1
isub //线程2-自减 线程内i=-1
putstatic i //线程2-将修改后的值存入静态变量i,静态变量i=-1
- 出现正数的情况
//假设i的初始值为0
getstatic i //线程1-获取静态变量i的值,线程内i=0
getstatic i //线程2-获取静态变量i的值,线程内i=0
iconst_i //线程1-准备常量1
iadd //线程1-自增 线程内i=1
iconst_i //线程2-准备常量1
isub //线程2-自减 线程内i=-1
putstatic i //线程2-将修改后的值存入静态变量i,静态变量i=-1
putstatic i //线程1-将修改后的值存入静态变量i,静态变量i=1
2、可见性
退不出的循环现象:main线程对run变量的修改对于t线程不可见,导致了t线程无法停止
static boolean run = false;
public static void main(String[] args) throws InterruptedException{
Thread t = new Thread(()->{
while(run){
//....
}
});
t.start();
Thread.sleep(1000);
run = false;//线程t不会如预想的停下来
}
分析:
- 初始状态,线程t刚开始从主内存读取了run的值到工作内存。
- 因为线程t需要频繁从主内存读取run的值,JIT即时编译器会将run的值缓存至自己工作内存的高速缓存中,减少对主存中run的访问,提高效率
- 1秒之后,main线程修改了run的值,并同步至主存,而线程t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
解决:例子体现的实际就是可见性,它保证的是多个线程之间,一个线程对volatile变量的修改对另一个线程可见,仅用在一个写线程,多个读线程的情况。使用volatile关键字修饰run静态变量
volatile关键字:
它可以用来修饰成员变量和静态成员变量,它可以避免线程从 自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile都是直接操作主存。
注意:
synchronized语句块可以保证代码块的原子性,也同时保证代码块内变量的可见性,但它是属于重量级操作,性能相对更低。
3、有序性 – 禁止指令重排
CPU存在乱序执行,使用volatile可以禁止指令重排。
以单例模式为例子:了解单例模式详情看单例模式博客
public class Singleton {
private static Singleton instance;
private String name;
public String getName() {
return name;
}
//构造函数私有化--即无法在类外通过Singleton singleton = new Singleton()创建实例化对象
private Singleton(){
}
public void setName(String name) {
this.name = name;
}
//被调用时才实例化对象
public static Singleton getInstance() {
if(instance == null) {
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
创建Singleton对象在字节码指令中:
-
new 为对象分配内存空间
-
dup 创建两个对象引用指针
-
invokespecial 一个交给线程工作内存初始构造方法
-
putstatic 另一个对象引用指针放入主内存
3 4两步顺序不是固定的,也许jvm会优化为先将引用地址赋值给instance变量后,再执行构造方法
前景:假设线程1进行判断为空后,线程2判断为空进行锁资源、释放锁住的资源;线程2实例化对象,线程1锁住资源,再判断一次是否为空,结果不为空,获取得到线程2创建的对象。
问题 :若线程2创建对象时指令重排,3 4步骤调换,即先将引用地址赋值给instance变量后,再执行构造方法,那么线程1拿到的将是未初始化完毕的单例
解决:使用volatile修饰 - > private static volatile Singleton instance 防止指令重排
volatile实现过程:
- 源码 volatile
- 字节码 ACC_volatile
- jvm内存屏障:指令不可重排,保障有序
- 底层代码:lock
4、CAS与原子类
CAS即Compare and Swap,它体现一种乐观锁的思想,比如多个线程对一个共享的整型变量执行+1操作。
当线程要修改某个值时,线程先读取该值,经过一系列需要进行的操作后,再读取一遍,如果该值没有变,则赋予新值;如果值改变,则再进行一次(读取、系列操作、再读取),直至值前后没有变化。
为了保证该变量的可见性,需要使用volatile修饰,结合CAS和volatile可以实现无锁并发,适用于竞争不激烈,多核CPU场景下。
- 没有使用synchronized 线程不会陷入阻塞
- 竞争激烈,不断自旋,不断地消耗CPU,效率反而会受影响
底层实现: 依赖于unsafe类来直接调用操作系统底层的CAS指令。
乐观锁: 不怕别的线程来修改共享变量,改了也没关系,不断重试就好了 – CAS
悲观锁: 防着其他线程来修改共享变量,独占线程 – synchronized
原子操作类,可以提高线程安全的操作,如AtomicInteger、AtomicBoolean等,底层就是采用CAS+volatile实现。
5、synchronized优化
java hotspot虚拟机中,每个对象都有对象头,平时存储对象的哈希码,分代年龄;当加锁时,这些信息就会根据情况被替换为标记位、线程锁记录指针、重量级锁指针、线程ID等内容。
synchronized在执行过程锁的等级会根据具体情况自动升级与降级:
-
new 态:即虽然加上了synchronized关键字,但实际运行并不需要锁 ,即不上锁
-
偏向锁:类似于贴上自身线程ID的标签,让其他线程无法执行;反映到现实生活,上厕所时贴上自己的名字表示里面有人了,你不能进来。
-
自旋锁(无锁态):CAS(jdk 1.8 :compare and swap jdk 1.11 :compare and set),先比较再赋值。
-
概述:当线程要修改某个值时,线程先读取该值,经过一系列需要进行的操作后,再读取一遍,如果该值没有变,则赋予新值;如果值改变,则再进行一次(读取、系列操作、再读取),直至值前后没有变化。这是一个不断自旋的过程,不断地消耗CPU。
-
在这个过程可能涉及到了一个ABA问题,ABA问题就是该值虽然前后没有改变,但已经经过某个线程的一系列操作,这一系列操作可能对后来其他线程的执行产生影响,可以通过添加版本号的方式解决,虽然该值没有变化,但是版本号改变。
-
-
重量级锁:当一个线程执行时,其他线程不能抢占,而是进入一个等待队列,只有等到上一个线程执行完毕,其他线程才能接着执行。
-
减少上锁时间
同步代码块中尽量断
-
减少锁的粒度
将一个锁拆分为多个锁提高并发度
-
锁粗化
多次循环进入同步块不如同步块内多次循环
-
锁消除
jvm会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其他线程所访问到,这时被即时编译器忽略掉所有同步操作
-
读写分离
CopyOnWriteArrayList
CopyOnWriteSet
注明:图片来源于b字母站黑马jvm教程视频