Java内存模型(JMM)

JMM即为 Java Memory Model ,他定义了主存(多个线程所共享的空间、例:成员变量)、工作内存(线程的私有空间,例:局部变量)的抽象概念,对应着底层的CPU寄存器、缓存、硬件内存、CPU指令优化等;

概要:我们通过操作java这些抽象概念,间接的操作复杂底层(化繁为简)

JMM体现在以下的几个方面 :

  • 原子性:保证指令不会受到线程的上下文切换的影响
  • 可见性:保证指令不会受到CPU缓存的影响
  • 有序性:保证指令不会受到CPU指令优化的影响

可见性

退不出的循环问题

看一个现象:

public class VisibleTest {static boolean isrun = true ;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {while(isrun){}}, "T1");t1.start();Thread.sleep(1000);System.out.println("T1线程停止");isrun = false ;//线程t1并不会如预想的一样停下来!}
}

测试结果:

为什么会这样?分析一下:

1、初始状态,T1线程从主存当中读取了run的值到工作内存;

2、因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率(JIT : Just In Time Compiler,一般翻译为即时编译器,)

1秒之后,main线程修改了run的值,并同步至主存,而+是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

解决:

方法一:为变量添加修饰:volatile(易变化关键字)

volatile static boolean isrun = true ;

这样做的目的是:加上volatile 的变量,每次循环都是只能在主存当中获取,不会从高速缓存区中获取!

测试结果:T1线程停止

方法二:使用synchronized

在Java内存模型中,synchronized规定,线程在加锁时, 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。(线程获得对象锁后,会清空工作区内存,重新在主存中获取!)

可见性VS原子性

重点区分:volatile和synchronized ;

  • 我们的volatile只能保证线程看到的变量是实时的,但是并不能保证是安全的!
  • 多个线程同时访问,即使被volatile修饰,仍然可能会出现指令交错问题!

有序性

JVM会在不影响正确的条件下,调整语句的执行顺序!这种特性称作【指令重排】

//如下i和j的++操作调换顺序不影响结果!
public class ReSortTest {static int i = 0 ;static int j = 0 ;public static void main(String[] args) {i++;   //修改为j++j++;   //修改为i++}
}

思考:正常执行是正确的,而且多线程条件下指令重排可能是会出现问题的,为什么要进行指令重排的优化呢?

指令重排序优化

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令—指令译码—执行指令—内存访问—数据写回这5个阶段

重排之前:指令串行执行!

现代CPU支持多级指令流水线,例如支持同时执行取指令~指令译码–执行指令–内存访问–数据写回的处理器,就可以称之为五级指令流水线。这时CPU可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC =1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。

重拍之后:指令并行执行 !

总结:指令级别的优化,我们线程的不同指令的不同阶段可同时进行!【指令级别的并发】

重排序的目的:为的是一个指令执行某一个阶段的时候,通过重排序,让其他执行执行其他的阶段!达到最大的指令并发!

当然前提是:重排互不影响结果 !

public class ReSortTest {static int i = 0 ;static int j = 0 ;public static void main(String[] args) {i++;   //2条指令可重排序!j++;   i= j - 10 ;   //不可重排序,会影响结果j++ ;}
}

禁止指令重排序

可以使用volatile实现,因为volatile可以使得被修饰的变量之前的操作是不会被重排序的

Volatile原理 *

以上可以了解到Volatile可以保证共享变量的有序性、可见性 , 我们接下来了解一下原理 ;

volatile的底层实现原理是内存屏障,Memory Barrier (Memory Fence)

  • 对volatile变量的写指令后会加入写屏障 ;
  • 对volatile变量的读指令前会加入读屏障 ;

1、如何保证的可见性

  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
public class ReSortTest {static int i = 0 ;volatile static int j = 0 ;public static void main(String[] args) {i++;   j++;   //由于j是volatile修饰的,此处对j进行写操作,所以插入写屏障 !//所以j++ 以及之前的代码全部会被同步到主存当中}
}
  • 读屏障(lfence)保证的是在该屏障之前的,对共享变量的改动,都同步到主存当中!
public class ReSortTest {volatile static int j = 0 ;public static void main(String[] args) {    if(j > 1){   //读取j ,在此之前的代码插入读屏障!(保证读取到的是主存中最新的数据!)}}
}

2、如何保证有序性

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
public class ReSortTest {static int i = 0 ;volatile static int j = 0 ;public static void main(String[] args) {i++;   j++;   //由于j是volatile修饰的,此处对j进行写操作,所以插入写屏障 !//所以j++ 以及之前的代码全部会被同步到主存当中  //写屏障 , 之前的代码不会发生指令重排序!}
}
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
public class ReSortTest {volatile static int j = 0 ;public static void main(String[] args) {  //读屏障:之后的代码不会被指令重排序 if(j > 1){   //读取j ,在此之前的代码插入读屏障!(保证读取到的是主存中最新的数据!)}}
}

总结:

  • 读屏障之后的代码不会发产生指令重排序、而且读到的都是主存中的数据
  • 写屏障之前的代码不会发生指令重排序、而且之前的代码会全部更新在主存当中!

虽然能解决可见性和有序性,但是仍然不能解决指令交错问题(原子性) ;

3、DCL问题的分析、纠正、解决

DCL : Double Check Locking 双检锁

看如下代码:

 // 最开始的单例模式是这样的public final class Singleton {private Singleton() { }private static Singleton INSTANCE = null;public static Singleton getInstance() {// 首次访问会同步,而之后的使用不用进入synchronizedsynchronized(Singleton.class) {if (INSTANCE == null) { // t1INSTANCE = new Singleton();}}return INSTANCE;}}
// 但是上面的代码块的效率是有问题的,因为即使已经产生了单实例之后,之后调用了getInstance()方法之后还是会加锁,这会严重影响性能!因此就有了模式如下double-checked lockin:public final class Singleton {private Singleton() { }private static Singleton INSTANCE = null;public static Singleton getInstance() {if(INSTANCE == null) { // t2                                  #1// 首次访问会同步,而之后的使用没有 synchronized       synchronized(Singleton.class) {                             #2  if (INSTANCE == null) { // t1                         #3INSTANCE = new Singleton();                          #4}}}return INSTANCE;                                               #5}}
//但是上面的if(INSTANCE == null)判断代码没有在同步代码块synchronized中,不能享有synchronized保证的原子性,可见性。所以

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外

多线程情况下,上述代码仍然存在指令重排的问题

当我们的线程t1,执行到 if(INSTANCE == null) #3,发现此时的实例为null,就去获取锁创建对象,我们看一下new对象的的字节码指令

// 通过这个复制的引用调用它的构造方法
21: invokespecial #4 // Method "<init>":()V
// 最开始的这个引用用来进行赋值操作
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;

此时我们的发生指令重排,先执行赋值操作,先将空的实例对象返回(此时Instance实例已经有值了),然后执行构造初始化对象!

就在我们的的初始化执行一半,线程t2过来了,发现instance不为null,执行 return INSTANCE;我们此时返回还是未被初始化的对象,所

以问题就此发生!!

解决DCL

加volatile就行了。

public final class Singleton {private Singleton() { }private static volatile Singleton INSTANCE = null;public static Singleton getInstance() {// 实例没创建,才会进入内部的 synchronized代码块if (INSTANCE == null) {synchronized (Singleton.class) { // t2// 也许有其它线程已经创建实例,所以再判断一次if (INSTANCE == null) { // t1INSTANCE = new Singleton();}}}return INSTANCE;}}

对volatile修饰的变量进行些操作的时候,在写操作后加上内存屏障,使得写屏障之前的代码不会发生指令重排!

happens before规则

七大规则(保证共享变量可见性的七种方法)!

二、共享模型之无锁

CAS + Volatile 无锁实现并发,保证线程安全(乐观锁)

CAS的工作方式

CAS (Compare And Set) : 比较并设置

//测试代码!
public class CasTest02 {AtomicInteger balance2 = new AtomicInteger(100);public void withdraw(Integer amount){while(true){int pre = balance2.get() ;int next = pre - amount ;if(balance2.compareAndSet(pre,next)){System.out.println(balance2.get());   //90break ;  //比较并设置设置值}}}
}
class TestCas{public static void main(String[] args) {CasTest02 test02 = new CasTest02();test02.withdraw(10);}//多个线程访问如下方法

其中ComapreAndSet,简称就是CAS(也有Compare And Swap的说法) ,它必须是原子操作!

当CAS方法执行时,prev 会与主存的实时balance比较一次,如果发现不一致(其他线程修改了),那么就返回false ;

//源码
public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  //执行cas时expect会与自身value比较}

CAS 与 volatile

获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。即一个线程对volatile变量的修改,对另一个线程可见。

注意
volatile仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)

//在我们原子整数当中,value都是被volatile修饰过的!
private volatile int value;

CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果

为什么无锁效率高?

无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。

而且最好用于线程数少于核心数的情况,线程数多的话CAS所在线程分不到时间片依然会进行上下文切换!

总结:因为CAS无锁保证线程安全的话,线程不会说会受到其他线程的影响陷入BLOCK阻塞状态,而是多个线程都会操作共享对象,但是cas会一直比较保证线程安全,线程是不会停止的,sync有锁方式则会出现一个线程获得锁,其他线程只能陷入BLOCK状态等待!

CAS的特点

  • 结合CAS和volatile可以实现无锁并发,适用于线程数少、多核CPU的场景下。
  • CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我点再重试呗。
  • synchronized是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们想改,我改完了解开锁,你们才有机会。
  • CAS体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
    • 因为没有使用synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一·
    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

原子整数

JUC的子包 java.util.concurrent.atomic 提供了

  • AtomicInteger
  • AtomicBoolean
  • AtomicLong

AtomicInteger为例:

       AtomicInteger i = new AtomicInteger(0);
//下边方法属于原子方法,线程安全的!System.out.println(i.getAndIncrement()); // 结果为 0 等价 i ++   (线程不安全的!)System.out.println(i.incrementAndGet()); // 结果为 2 等价 ++ iSystem.out.println(i.getAndAdd(5));// 结果2    System.out.println(i.addAndGet(5));// 结果12    读取到的   要更改为System.out.println(i.updateAndGet(x -> x * 10));   //输出 50
//本质都是compare and set ;

原子引用

除了保护我们的基本类型,还可以保护BigDecimal这种引用类型 ;

//测试代码!
private AtomicReference<BigDecimal> baclace ;  //外加一层AtomicReferencepublic void withdraw(Integer amount){while(true){BigDecimal pre = balance.get() ;BigDecimal next = pre.subtract(amount) ;  //引用数据类型减法if (balance.ComapreAndSet(pre,next))  break ;  //比较并设置设置值}}BigDecimal decimal = new BigDecimal("1000");  //初始化时最好传递的时字符串!

ABA问题

我们都知道我们cas保证的时最新的值和pre是否相等来判断是否被修改,但是存在这么一种情况:值被修改但是,修改后还是跟pre一致,这种情况,cas则无法判断是否被修改过 ;(虽然对业务无影响,但是仍是个隐患!)

AtomicStampedReference

因此,为了解决ABA这种问题引入

AtomicStampedReference<String>  str = new AtomicStampedReference<>("a",0); // 0相当于版本号,只要修改过就会 + 1
//除了比较值是否相等还会比较版本号,版本号会记录改过的次数

AtomicMarkableReference

想对上面AtomicMarkableReference只关心是否被修改过,并不关心修改的次数

 AtomicMarkableReference<String> s = new AtomicMarkableReference<>("123",false);

原子数组

保护数组里面的元素、有点复杂没看懂涉及JDK8新特性

原子更新器

保护某个对象里的属性、保证多个线程访问对象中属性的安全性!

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;public class AtomicField {public static void main(String[] args) {Student student = new Student();  //多个线程修改其中的name属性//为Student的name属性设置更新器AtomicReferenceFieldUpdater updater =            //类         属性类型        属性名AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");updater.compareAndSet(student,null,"张三");System.out.println(student);    //Student{name='张三'}}
}
class Student{volatile String name ;  //必须volatile修饰、不然抛出异常!@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +'}';}
}

通过打断点Debug模拟其他线程提前操作,导致cas匹配失败!

原子累加器

在进行累加的时候,JDK提供如下的2个类的性能是优越于AtomicInteger、AtomicLong这些的,提高4、5倍!

  • LongAdder
  • LongAccumulator

性能提升的原因很简单,就是在有竞争时,我们的AtomicLong向一个累加单元多次尝试,会降低效率,然而LongAdder设置多个累加单元,Therad-0累加Cell[0],而Thread-1 累加Cell[1]…最后将结果汇总。这样它们在累加时操作的不同的Cell变量,因此减少了CAS重试失败从而提高性能。

缓存伪共享行为

其中的Cell为累加单元

//防止缓存行伪共享
@sun.misc.contended
public static final class cell {volatile long value;cell( long x) { value = x; }
//最重要的方法,用来 cas.方式进行累加,prev表示旧值,next表示新值final boolean cas( long prev,long next) {return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);}
//省略不重要代码

解释这个需要从CPU的缓存说起


  • 因为CPU与内存的速度差异很大,需要靠预读数据至缓存来提升效率。
  • 而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64 byte (8 个long)
  • 缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中
  • CPU要保证数据的一致性,如果某个CPU核心更改了数据,其它CPU核心对应的整个缓存行必须失效

因为Cell 是数组形式,在内存中是连续存储的,一个Cell为24字节(16字节的对象头和8字节的value),因此缓存行可以存下2个的Cell对象。这样问题来了:

无论谁修改成功,都会导致对方Core的缓存行失效,比如Core-0中ce11[0]=6000,Cell[1]=800。要累加cell[e]=6001,cell[1]=800e,这时会让Core-1的缓存行失效
@sun.misc.Contended用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,从而让CPU将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效

总结:

然而缓存行也存在问题:假设CPU的第一个核需要操作a变量,第二个核需要操作b变量,表面看a和b是没有任何关系的,但是a和b在同一个cache line中,这样假设核心一修改了变量a的值,那么它将会刷新所有和a相关的缓存的数据,b变量也就会受到牵连,最后导致核心二再去缓存读取b变量的时候出现cache miss,需要重新到主存加载新数据,这就是所谓的false share(伪共享) !

我们可以用Contended注解使得我们的累加单元分别保存在不同的缓存行!

add方法解析

//源码
public void add(long x) {Cell[] as; long b, v; int m; Cell a;if ((as = cells) != null || !casBase(b = base, b + x)) {boolean uncontended = true;if (as == null || (m = as.length - 1) < 0 ||(a = as[getProbe() & m]) == null ||!(uncontended = a.cas(v = a.value, v + x)))longAccumulate(x, null, uncontended);}}

总结一个add的流程图

LongAccumulate

总结LongAccumulate流程图

sum方法分析

我们获取最终的累加结果

//源码
public long sum() {Cell[] as = cells; Cell a;long sum = base;if (as != null) {for (int i = 0; i < as.length; ++i) {if ((a = as[i]) != null)sum += a.value;}}return sum;}

JVM_06 内存模型(JMM)篇相关推荐

  1. 全面理解Java内存模型(JMM)及volatile关键字

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...

  2. Java并发编程:Java内存模型JMM

    简介 Java内存模型英文叫做(Java Memory Model),简称为JMM.Java虚拟机规范试图定义一种Java内存模型来屏蔽掉各种硬件和系统的内存访问差异,实现平台无关性. CPU和缓存一 ...

  3. 深入理解并发内存模型||JMM与内存屏障||多核并发缓存架构 ||JMM内存模型||volatile 关键字的作用 ||JMM 数据原子操作||JMM缓存不一致的问题

    深入理解并发内存模型||JMM与内存屏障 多核并发缓存架构 JMM内存模型 volatile 关键字的作用 JMM 数据原子操作 JMM缓存不一致的问题

  4. java内存模型(JMM)和happens-before

    文章目录 重排序 Happens-Before 安全发布 初始化安全性 java内存模型(JMM)和happens-before 我们知道java程序是运行在JVM中的,而JVM就是构建在内存上的虚拟 ...

  5. Java 内存模型 JMM 详解

    转载自 Java 内存模型 JMM 详解 JMM简介 Java Memory Model简称JMM, 是一系列的Java虚拟机平台对开发者提供的多线程环境下的内存可见性.是否可以重排序等问题的无关具体 ...

  6. JVM——Java内存模型(JMM)

    关注微信公众号:CodingTechWork,一起学习进步. 软硬件发展概述 Amdahl定律和摩尔定律 1)Amdahl定律:通过系统中并行化和串行化的比重来描述多处理器系统能获得的运算加速能力. ...

  7. Java内存模型(JMM)详解

    在Java JVM系列文章中有朋友问为什么要JVM,Java虚拟机不是已经帮我们处理好了么?同样,学习Java内存模型也有同样的问题,为什么要学习Java内存模型.它们的答案是一致的:能够让我们更好的 ...

  8. java 内存模型JMM解析

    java 内存模型JMM解析 一.CPU多核并发缓存架构解析    1.以往的内存读取    2.后来的内存读取 二.java内存模型实现原理    1.验证上图模型      1)案列代码      ...

  9. java虚拟机jvm与Java内存模型(JMM)

    Java内存模型(JMM) Java 内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存.Java 内存模型(JMM) 抽象了线程和主内存之间的关系,就比如说线程之间的共享变量必须存 ...

  10. JUC进阶之路-Java的内存模型JMM

    本文源自转载:JUC进阶之路-Java的内存模型JMM 目录 一.大厂常见的JMM面试题 二.什么是JAVA内存模型JMM(Java Memory Model) 三.JMM的三大特性 3.1 可见性 ...

最新文章

  1. Mesos各种存储处理方式
  2. [译]Vulkan教程(05)Instance
  3. Spring松耦合的实现
  4. java之spring mvc之文件上传
  5. LinkedList专题1
  6. pycharm python部署_使用PyCharm配合部署Python的Django框架的配置纪实
  7. 很基本的权限功能小结
  8. CentOS7.3编译安装php7.1
  9. OpenGL学习笔记:画点、直线和多边形(第一讲)
  10. 基桩测试软件,智博联ZBL-U5700/5600机内软件测桩模块更新软件
  11. 海湾汉字编码表全部_汉字区位码对照查询表-汉字区位码对照表大全下载pdf打印版-西西软件下载...
  12. 网页抽奖程序(年会,开幕式等)
  13. MAC 如何快捷截图
  14. 服务器防御DDOS攻击的方法
  15. python爬取wifi密码完整代码_WIFIpass – Python获取本机保存的所有WIFI密码(附源代码)...
  16. android应用推荐
  17. 软件测试必学的16个高频数据库操作及命令
  18. 生态愿景与险企数字化进度——保险科技生态建设
  19. golang 隐藏启动其他程序,包含cmd窗口(黑窗口)程序,GUI程序隐藏
  20. 真香,50行Java代码爬取妹子套图!

热门文章

  1. win10系统如果更改战网服务器,win10系统无法登录战网的四种解决方法
  2. 程序员的呐喊--读书感悟
  3. 计算机运算器发展趋势,2020计算器市场发展现状及及前景分析
  4. Spark:reduceByKey与groupByKey进行对比
  5. 大数据「杀熟」:冤枉,也不冤枉
  6. MacBook Pro(13 英寸,2011 年末)A1278 安装Winows11无声音问题解决(WIN10和WIN11同样的解决方法)
  7. 知云文献翻译打不开_一款好用的文献英中翻译软件
  8. 网上收集的一些程序员笑话
  9. B端产品:通过线上渠道增长
  10. 阅读《精通Python爬虫框架Scrapy》