初涉Java内存模型


最近在继续加紧准备春招,加强一下Java多线程这一块,这篇文章是对于Java内存模型的一些总结,只是简单地谈谈

Java内存模型

Java实现会带来不同的“翻译”,不同CPU平台的机器指令又千差万别,无法保证并发安全的效果一致。

JVM内存结构 VS Java内存模型 VS Java对象模型

三个截然不同的概念,容易混淆

JVM内存结构

和Java虚拟机的运行时区域有关

  • 组成:堆,虚拟机栈,方法区,本地方法栈,程序计数器

Java对象模型

和Java对象在虚拟机中的表现形式有关

  • 是Java对象自身的存储模型
  • JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类
  • 当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据

Java内存模型(JMM)

为什么需要JMM(Java Memory Model)

  • C语言不存在内存模型的概念
  • 依赖处理器,不同处理器结果不一样,可能一个程序在不同处理器运行结果不同
  • 无法保障并发安全
  • 需要一个标准,让多线程运行的结果可以预期

JMM是一种规范

即这是一组规范,需要各个JVM的实现来遵循JMM规范,以便于开发者可以利用这些规范,更加方便地开发多线程程序。如没有这样的规范,那么可能经过不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样,就会产生问题。

JMM是工具类和关键字的原理

  • volatile、synchronized、Lock等的原理都是JMM
  • 若没有JMM,就需要我们自己制定什么时候用内存栅栏,即什么时候同步,很麻烦

最重要的3点内容:重排序、可见性、原子性

重排序

package jmm;


import java.util.concurrent.CountDownLatch;


/**
* @Auther: Bob
* @Date: 2020/2/15 15:41
* @Description: 重排序的演示
* 直到达到某个条件才停止,用来测试小概率时间
*/
public class OutOfOrderExecution {
	private static int x = 0, y = 0;
	private static int a = 0, b = 0;
	public static void main(String[] args) throws InterruptedException {
		int i = 0;
		for (; ; ) {
			i++;
			x = 0;
			y = 0;
			a = 0;
			b = 0;
			CountDownLatch latch = new CountDownLatch(1);
			Thread one = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					latch.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
					}
				a = 1;
				x = b;
				}
			});
		Thread two = new Thread(new Runnable() {
			@Override
			public void run() {
				try {
					latch.await();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				b = 1;
				y = a;
				}
			});
			one.start();
			two.start();
			latch.countDown();
			one.join();
			two.join();
			String result = "第" + i + "次 (" + x + "," + y + ")";
			if (x == 1 && y == 1) {
				System.out.println(result);
				break;
			} else {
				System.out.println(result);
			}
		}
	}
}

执行结果:第453次才出现了1,1

什么是重排序

在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序,这里被颠倒的是y = a 和 b = 1 这两行语句

重排序的好处:提高处理速度
重排序的3种情况
  • 编译器优化:包括JVM,JIT编译器等
  • CPU指令重排:就算编译器不发生重拍,CPU也可能对指令进行重拍
  • 内存的“重排序”:线程A的修改线程B却看不到,引出可见性问题(不是真正的重排序)

可见性

什么是可见性
package jmm;

/**
 * @Auther: Bob
 * @Date: 2020/2/15 16:37
 * @Description: 演示可见性带来的问题
 */
public class FieldVisibility {
    volatile int a = 1;
    volatile int b = 2;
    private void change() {
        a = 3;
        b = a;
    }

    private void print() {
        System.out.println("b = " + b + ", a = " + a);
    }
    public static void main(String[] args) {
        while (true) {
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }

}

有可见性问题的原因:

  • 高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层
  • 线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的
  • 如果所有核心都只用一个缓存,那么就不存在内存可见性问题了
  • 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待输入到主存中。所以会导致读取的值是一个已经过期的值

“利用volatile关键字可以解决问题”

volatile关键字
volatile是什么
  • voltile是一种同步机制,比synchronized或者Lock相关类更轻量,因为适用vilatile并不会发生上下文切换等开销很大的行为。
  • 如果一个变量修改成volatile,那么JVM就知道了这个变量可能被并发修改
  • 开销小,相应能力也小,volatile做不到synchronized那样的原子保护,volatile只在有限场景下才能发挥作用
volatile适用场景与不适用场景
  • 不适用:a++
  • 适用场景1:boolean flag,若一哥共享变量在程序中只是被各个线程赋值,而无其他操作,那么可以用volatile来代替synchronized或者代替原子变量,因为赋值自身具有原子性,而volatile又保证了可见性,所以足以保证线程安全。
  • 适用场合2:作为刷新之前变量的触发器
volatile的两点作用
  1. 可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须读取到主内存读取最新值,写一个volatile属性会立即刷入到主内存。
  2. 禁止指令重排序优化:解决单利双重锁乱序问题
volatile和synchronized的关系

如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么久可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全,此时volatile可以看做轻量版的synchronized。

用volatile可以修正重排序问题
volatile小结

在这里插入图片描述

在这里插入图片描述

除了volatile可以让变量保证可见性外,synchronized、Lock、并发集合、Thread.join()和Thread.start()等都可以保证的可见性

对synchronized可见性的正确理解
  • synchronized不仅保证了原子性,还保证了可见性
  • synchronized不仅让被保护的代码安全,还近朱者赤(解锁之前的所有操作另一个线程都能看到)

原子性

什么是原子性

一系列的操作,要么全部执行成功,要么全部不执行,是不可分割的

Java中的原子操作有哪些
  • 除了long和double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作,根据Oracle的官方文档,在32位的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的,对于64位的值的写入 ,可以分为两个32位的操作进行写入,读取错误,使用volatile解决。(在实际开发中,商用虚拟机中不会出现)
  • 所有引用reference的赋值操作,不论是32位还是64位的操作系统
  • java.concurrent.Atomic.*包中所有类的原子操作
原子操作 + 原子操作 != 原子操作
  • 简单地把原子操作组合在一起,并不能保证整体依然具有原子性
  • 全同步的HashMap也不完全安全

总结得不太好,多请见谅,多数都只是涉及概念层面的东西。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章