JVM内存模型

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对象在字节码指令中:

  1. new 为对象分配内存空间

  2. dup 创建两个对象引用指针

  3. invokespecial 一个交给线程工作内存初始构造方法

  4. putstatic 另一个对象引用指针放入主内存

    3 4两步顺序不是固定的,也许jvm会优化为先将引用地址赋值给instance变量后,再执行构造方法

前景:假设线程1进行判断为空后,线程2判断为空进行锁资源、释放锁住的资源;线程2实例化对象,线程1锁住资源,再判断一次是否为空,结果不为空,获取得到线程2创建的对象。

问题 :若线程2创建对象时指令重排,3 4步骤调换,即先将引用地址赋值给instance变量后,再执行构造方法,那么线程1拿到的将是未初始化完毕的单例

解决:使用volatile修饰 - > private static volatile Singleton instance 防止指令重排

volatile实现过程:

  1. 源码 volatile
  2. 字节码 ACC_volatile
  3. jvm内存屏障:指令不可重排,保障有序
  4. 底层代码: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在执行过程锁的等级会根据具体情况自动升级与降级:

  1. new 态:即虽然加上了synchronized关键字,但实际运行并不需要锁 ,即不上锁

  2. 偏向锁:类似于贴上自身线程ID的标签,让其他线程无法执行;反映到现实生活,上厕所时贴上自己的名字表示里面有人了,你不能进来。

  3. 自旋锁(无锁态):CAS(jdk 1.8 :compare and swap jdk 1.11 :compare and set),先比较再赋值。

    • 概述:当线程要修改某个值时,线程先读取该值,经过一系列需要进行的操作后,再读取一遍,如果该值没有变,则赋予新值;如果值改变,则再进行一次(读取、系列操作、再读取),直至值前后没有变化。这是一个不断自旋的过程,不断地消耗CPU。

    • 在这个过程可能涉及到了一个ABA问题,ABA问题就是该值虽然前后没有改变,但已经经过某个线程的一系列操作,这一系列操作可能对后来其他线程的执行产生影响,可以通过添加版本号的方式解决,虽然该值没有变化,但是版本号改变。

  4. 重量级锁:当一个线程执行时,其他线程不能抢占,而是进入一个等待队列,只有等到上一个线程执行完毕,其他线程才能接着执行。

  5. 减少上锁时间

    同步代码块中尽量断

  6. 减少锁的粒度

    将一个锁拆分为多个锁提高并发度

在这里插入图片描述

  1. 锁粗化

    多次循环进入同步块不如同步块内多次循环

  2. 锁消除

    jvm会进行代码的逃逸分析,例如某个加锁对象是方法内局部变量,不会被其他线程所访问到,这时被即时编译器忽略掉所有同步操作

  3. 读写分离

    CopyOnWriteArrayList

    CopyOnWriteSet

注明:图片来源于b字母站黑马jvm教程视频

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值