java内存模型JMM-底层原理
底层原理
重点:懂了这个,才能说懂了并发,才能说懂了java
java到CPU
JVM实现会带来不同的“翻译”,不同的CPU平台的机器指令又千差万别,无法保证并发安全效果一致。所以转换过程需要有规范和原则.
JVM内存结构 Java内存模型 Java对象模型
JVM内存结构
和java虚拟机的运行时区域有关
堆:最大的一块,new 或其他创建的实例对象,数组. 优势就是动态分配
虚拟机栈:java栈.保存了各个基本的数据类型,以及对象的引用 编译的时候确定了大小,且编译的时候不会改变
方法区:static变量,类信息,常量信息,永久引用(static).
本地方法栈:和本地方法相关的,native方法
程序计数器:同计组.
Java内存模型
和java并发编程相关
java对象模型
和Java对象在虚拟机中的表现形式有关
- java对象自身的存储模型
- JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该java类
- 当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据.
JMM Java Memory Model
- C不存在内存模型的概念.
- 依赖处理器,不同处理器结果不一样.
- 无法保证并发安全.
- 需要一个标准,让多线程运行的结果可预期.
JMM是规范
如果没有这个,经过不同JVM的不同规则的重排序 之后,会导致不同的虚拟机上运行的结果不一样,这就缺失了跨平台性.
是原理
是volatile,synchronized,lock的原理都是JMM,如果没有JMM,那就需要我们自己指定什么时候用内存栅栏等…
重排序,可见性,原子性.
重排序
package com.hasson.jmm;
import java.util.concurrent.CountDownLatch;
//演示重排序,知道达到某个条件才停止,测试小概率事件
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 = y = a = 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();
System.out.println("执行了" + i + "次");
System.out.println("x = " + x + " y = " + y);
if (x == 1 && y == 1) {
break;
}
}
}
}
但是经过重排序之后,仍有可能出现x=0,y=0的情况.即以下顺序
y=a;
a=1;
x=b;
b=1;
什么是重排序:
在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令不不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序,这里被颠倒的是y=1,b=1这两行语句.
a=1 会被拆为: Load a,set to 3,Store a
好处是提高处理速度.
三种情况
- 编译器优化:包括JV吗,JIT编译器
- CPU指令重排:就算编译器不发生重排,CPU也可能重拍.
- 内存的"重排序":线程A的修改线程B却看不到,引出可见性问题
可见性
什么是可见性问题
-
-
package com.hasson.jmm; //演示可见性带来的问题 public class FieldVisibility { int a = 1; int b = 2; public static void main(String[] args) { while (true) { FieldVisibility fieldVisibility = new FieldVisibility(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } fieldVisibility.change(); } }).start(); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } fieldVisibility.print(); } }).start(); // a =3 b =3 // a= 3 b =2 // a= 2 b =1 // a= 1 b =3 } } private void print() { System.out.println("a = " + a + " b = " + b); } private void change() { a = 3; b = a; } }
-
线程b看不到(或看不完全)线程A的操作.
-
-
线程不能直接通信,需要通过shared cache
-
用volatile,可以立马刷新.
为什么会有可见性问题
-
- 由于CPU有多级缓存,会导致读的数据过期
JMM抽象:主内存和本地内存
Java作为高级语言,屏蔽了这些底层细节,用JMM定义了读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是JMM抽奖了主内存和本地内存的概念.
这里说的本地内存并不是一块给每个线程分配的内存,而是JMM的一个抽象,是对于寄存器,一级缓存,二级缓存等的抽象.
![image-20210107205149433](https://i-blog.csdnimg.cn/blog_migrate/4e42a8e01c04967c11715e3e20091222.png)
JMM规定:
- 所有的变量都存储在主内存,同时每个线程也有自己独立的工作内存,工作内存中的变量都是主内存的拷贝.
- 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后同步到主内存中
- 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成.
所有的共享变量存储在主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题.
Happens-Before原则
单线程规则
一个线程里:后面的语句一定发生在前面的语句之后,肯定看得到前面执行了什么
锁操作(synchronized和lock)
如x =1,i=x
volatile变量
![image-20210107211342595](https://i-blog.csdnimg.cn/blog_migrate/b435f3e9583eb06861ebde04ee0bc2c4.png)
加了volatile,就可以有happen-before
线程启动
![image-20210107211455107](https://i-blog.csdnimg.cn/blog_migrate/dc2c002866cbefefe0934e2db8347f1c.png)
线程join
![image-20210107211601067](https://i-blog.csdnimg.cn/blog_migrate/429d1f386666bd06a3611aaa361e20be.png)
join就是想等待。
传递性
如果hb(a,b) and hb(b,c) so hb(a,c)
中 断
一个线程被其他线程interrupt时,那么检测中断(isInterrupted)或者抛出InterruptedException一定能看到,得要检测到。
构造方法
对象构造方法的最后一行指令happeds-before于finalize方法的第一行指令
线程工具类中
- 线程安全的容器get一定能看到在此之前的put等存入动作
- CountDownLatch
- Semaphore
- Future
- 线程池
- CyclicBarrier
volatile关键字
含义
volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
如果一个变量被修饰为volatile,那么JVM就知道这个变量可能被并发修改。
但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的源自保护,volatile尽在很有限的场景瞎才能发挥作用。
适合和不适合的场景
不适用:a++
使用场合:
适用场合1: boolean flag,如果个共享变量自始至终只被各个线程赋值,而没有其他的操作(对比,取值等等),那么就可以用 volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所以就足以保证线程安全。而不取决于先前的情况
适用场合2:作为刷新之前变量的触发器
作用:
- 可见性:读个 volatile変量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个 volatile属性会立即刷入到主内存。
- 禁止指令重排序优化:解決单例双重锁乱序问题
volatile和synchronized的关系
volatile在这方面可以看做是轻量版的 synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替 synchronized或者代替原子变量,因为赋值自身是有原子性的,而 volatile又保证了可见性,所以就足以保证线程安全。
小结
-
volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。
-
volatile 属性的读写操作都是无锁的,它不能替代 synchronized因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
-
volatile只能作用于属性,我们用 volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
-
volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。 volatile属性不会被线程绶存,始终从主存中读取
-
volatile提供了 happens- before保证,对 volatile变量v的写入 happens- before所有其他线程后续对y的读操作。
-
volatile 可以使得long和 double I的赋值是原子的,后面马上会和 double的原子性。
保证可见性的措施
除了 volatile可以让变量保证可见性外, synchronized、Lock、并发集合、 Thread.join()和 **Thread. start()**等都可以保证的可见性
也就是happen-before
synchronized可见性
synchronized不仅保证了原子性,还保证了可见性
上一个synchronized可以被下一个看到,附近的也会,如下图:
如print函数可以看到change函数的所有改变。
原子性
什么是原子性
系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。–ATM取钱
Java中的原子操作有哪些
- 除了long double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作。
- 所有引用reference的赋值操作,不管是32位还是64位的机器。
- java.concurrent.Atomic.*包中所有类的原子操作
long和double 的原子性
问题描述:官方文档、对于64位的值的写入,可以分为两个32位的操作进行写入、读取错误、使用 volatile解决。
32位上的JVM不是原子的,64位的JVM是原子的。
在实际上不会出现.
原子操作+原子操作 != 原子操作
面试问题
单例模式
- 节省内存和计算
- 保证结果正确
- 方便管理
适用场景
-
无状态的工具类:比如日志工具类,不管是在哪里使用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。
-
全局信息类:比如我们在一个类上记录网站的访问次数,我们门不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。
各种写法.
饿汉式(静态变量)(可用)
//静态常量(饿汉式)(可用)
public class Singleton1 {
private final static Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return INSTANCE;
}
//简单
//加载类的时候就创建好了。
}
饿汉式(静态代码块)(可用)
//静态代码块(饿汉式)(可用)
public class Singleton2 {
private final static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return INSTANCE;
}
//和第一种类似。
}
懒汉式(线程不安全)不可用
//懒汉式(线程不安全)不可用
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (instance == null) //这里切换线程的时候,会生成两个,就不是同一个实例了。
instance = new Singleton3();
return instance;
}
}
懒汉式(线程安全,用同步)[不推荐使用]
//懒汉式(线程安全)不推荐用,效率太低了~
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
public synchronized static Singleton4 getInstance() {
if (instance == null)
instance = new Singleton4();
return instance;
}
}
懒汉式(线程不安全,同步代码块)[不可用]
//懒汉式(线程不安全,同步代码块)[不可用]
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
if (instance == null)
synchronized (Singleton5.class) { //会生成俩
instance = new Singleton5();
}
return instance;
}
}
双重检查[推荐使用] 面试的时候使用
//双重检查[推荐使用]
public class Singleton6 {
private volatile static Singleton6 instance;
private Singleton6() {
}
public static Singleton6 getInstance() {
if (instance == null)
synchronized (Singleton6.class) {
if (instance == null) //双重检查~~
instance = new Singleton6();
}
return instance;
}
}
好处:
- 线程安全,延迟加载,效率比较高
为什么要double-check
- 为了线程安全,单check的时候不安全,可以举上面的例子(同时进入第一个判断).
synchronized放到方法上可以嘛?
- 可以的,只是性能不太好.
为什么要用volatile?
- 新建对象有三个步骤(不是原子操作)
可能还是会发生重排序
- 重排序会导致空指针问题,所以要防止重排序
- 所以使用volatile
静态内部类[推荐使用]
//静态内部类[推荐使用]--属于懒汉,区别于第一、二种形式,他们有时候不需要这个实例,也创建了。
//但是静态内部类这种方法只有需要的时候才会创建。(反射的知识。)
public class Singleton7 {
private Singleton7() {
}
private static class SingletonInstance {
private static final Singleton7 INSANCE = new Singleton7();
}
public static Singleton7 getInstance() {
return SingletonInstance.INSANCE;
}
}
枚举[推荐使用]
//最佳的方法 枚举形式
//调用方法 Singleton8.INSTANCE
public enum Singleton8 {
INSTANCE;
public void func() {
}
}
不同写法的对比
- 饿汉:简单,但是没有lazy loading(需要才加载)
- 懒汉:线程不安全
- 静态内部类:可用
- 双重检查:可考察的知识点比较多.
- 枚举:最好
- Joshua Bloch大神在《 Effective Java》中明确表达过的观点:“使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton的最佳方法。
- 写法简单
- 线程安全有保障–里面的知识比较多
- 避免反序列化破坏单例
不同方法的适用场合
最好的方法是利用枚举,因为还可以防止反序列化重新创建新的对象
非线程同步的方法不能使用
如如果程序开始要加载的资源太多,那么就应该使用懒加载
饿汉式如如果是对象的创建需要配置文件就不适用。
懒加载虽然好,但是静态内部类这种方式会引入编程复杂性
各种的优缺点.
-
饿汉式的缺点?
-
懒汉式的缺点?
-
为什么要用 double- check?不用就不安全吗?
-
为什么双重检査模式要用 volatile?
- 新建一个类不是原子操作.
-
应该选择哪种?
什么是Java内存模型***
解释优点,C++没有的缺点,三兄弟的比较,是一种规范 重排序 可见性 原子性.Happen-before,volatile synchronized
volatile和synchronized 的异同
开销,适用场合,各自的作用
什么是原子操作,有哪些原子操作,生成对象的过程是不是一个原子操作.
新建个空的 Person对象
把这个对象的地址指向p
执行 Person的构造函数
什么是内存可见性
64位的double long写入的时候是原子的嘛
理论上不是,但是商用的不会