硬件内存模型:

Java内存模型:

每个线程都有一个工作内存,线程只可以修改自己工作内存中的数据,然后再同步回主内存,主内存由多个内存共享。

下面 8 个操作都是原子的,不可再分的:

1)  lock:作用于主内存的变量,它把一个变量标识为一个线程独占的状态。

2)  unlock:作用于主内存的变量,他把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

3)  read:作用于主内存变量,他把一个变量的值从主内存传输到线程的工作内存,以便随后的 load 操作使用。

4)  load:作用于工作内存的变量,他把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。

5)  use:作用于工作内存的变量,他把工作内存中一个变量的值传递给执行引擎,每 当虚拟机遇到一个需要使用到变量的值得字节码指令时将会执行这个操作。

6)  assign:作用于工作内存的变量,他把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

7)  store:作用于工作内存的变量,他把工作内存中一个变量的值传送到主内存中,以便随后 write 使用。

8)  write:作用于主内存的变量,他把 store 操作从工作内存中得到的变量的值放入主内存的变量中。

JVM对Java内存模型的实现

在JVM内部,Java内存模型把内存分成了两部分:线程栈区和堆区,下图展示了Java内存模型在JVM中的逻辑视图: 

JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。

线程栈还包含了当前方法的所有本地变量信息。一个线程只能读取自己的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。

堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。

下图展示了调用栈和本地变量都存储在栈区,对象都存储在堆区: 
 
一个本地变量如果是原始类型,那么它会被完全存储到栈区。 
一个本地变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区

对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。 
对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区

Static类型的变量以及类本身相关信息都会随着类本身存储在堆区

堆中的对象可以被多线程共享。如果一个线程获得一个对象的应用,它便可访问这个对象的成员变量。如果两个线程同时调用了同一个对象的同一个方法,那么这两个线程便可同时访问这个对象的成员变量,但是对于本地变量,每个线程都会拷贝一份到自己的线程栈中。

支撑Java内存模型的基础原理

指令重排序

在执行程序时,为了提高性能,编译器和处理器会对指令做重排序。但是,JMM确保在不同的编译器和不同的处理器平台之上,通过插入特定类型的Memory Barrier来禁止特定类型的编译器重排序和处理器重排序,为上层提供一致的内存可见性保证。

  1. 编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  2. 指令级并行的重排序:如果不存l在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序:处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

数据依赖性

如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。 编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。

as-if-serial

不管怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义。

内存屏障(Memory Barrier )

内存屏障是一组处理器指令,作用:

1.先于这个内存屏障的指令必须先执行,后于这个内存屏障的指令必须后执行。

2.使得内存可见性。

volatile是基于Memory Barrier实现的。

如果一个变量是volatile修饰的,JMM会在写入这个字段之后插进一个Write-Barrier指令,并在读这个字段之前插入一个Read-Barrier指令。这意味着,如果写入一个volatile变量,就可以保证:

  1. 一个线程写入变量a后,任何线程访问该变量都会拿到最新值。
  2. 在写入变量a之前的写入操作,其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入。

volatile 禁止指令重排序有两层意思:1)当程序执行到 volatile 变量的读操作或者写操作时,在其前面操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没执行。2)在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

happens-before

在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。

与程序员密切相关的happens-before规则如下:

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
  2. 监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
  3. volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
  4. 传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。

注意:两个操作之间具有happens-before关系,并不意味前一个操作必须要在后一个操作之前执行!仅仅要求前一个操作的执行结果,对于后一个操作是可见的,且前一个操作按顺序排在后一个操作之前。

synchronized关键字【具有锁重入特性,不具有继承性】:

1)在两个线程访问同一个对象中的同步方法时,一定是线程安全的。什么是两个线程访问同一个对象呢?例子如下:

  1. public static void main(String[] args) {
  2. HasSelfPrivateNum numRef = new HasSelfPrivateNum();
  3. ThreadA athread = new ThreadA(numRef);
  4. athread.start();
  5. ThreadB bthread = new ThreadB(numRef);
  6. bthread.start();
  7. }

2)在两个线程分别访问同一个类的两个不同实例的相同名称的同步方法时,是以异步的方式执行的,所以synchronized在这里似乎没什么用。两个线程分别访问同一个类的两个不同实例?例子如下:

  1. public static void main(String[] args) {
  2. HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
  3. HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
  4. ThreadA athread = new ThreadA(numRef1);
  5. athread.start();
  6. ThreadB bthread = new ThreadB(numRef2);
  7. bthread.start();
  8. }

总结【都是针对1)的情况,即多个线程访问同一对象的情况】:

(1)synchronized取得的锁是对象锁,而不是把一段代码或方法当作锁,所以在1)中,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。而如果多个线程访问多个对象,则JVM会创建多个锁。

(2)对于同步方法,线程间只能同步调用,即同一时间只有一个线程可以执行同步代码块中的代码,且对当前类中的其他同步代码块具有阻塞作用;对于非同步方法,线程可以以异步方式调用,尽管可能这个对象锁已经被占用。

(3)由于字符串常量池的特殊性,一般不对字符串常量进行加锁,即类似synchronized("aa")这种。

对于2)的情况的解决办法是:将synchronized关键字从方法范围上,拿到代码块中,细化同步范围,且进行全局锁,就是对类加锁而不是对象,即synchronized (Sync.class),或者利用synchronized static加在方法上。

synchronized的锁可重入特性:

当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞.

我们来看看synchronized,它拥有强制原子性的内置锁机制,是一个重入锁,所以在使用synchronized时,当一个线程请求得到一个对象锁后再次请求此对象锁,可以再次得到该对象锁,就是说在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以拿到锁,如下:

  1. public class Child extends Father {
  2. public static void main(String[] args) {
  3. Child child = new Child();
  4. child.doSomething();
  5. }
  6.  
  7. public synchronized void doSomething() {
  8. System.out.println("child.doSomething()");
  9. doAnotherThing(); // 调用自己类中其他的synchronized方法
  10. }
  11.  
  12. private synchronized void doAnotherThing() {
  13. super.doSomething(); // 调用父类的synchronized方法
  14. System.out.println("child.doAnotherThing()");
  15. }
  16. }
  17.  
  18. class Father {
  19. public synchronized void doSomething() {
  20. System.out.println("father.doSomething()");
  21. }
  22. }

运行结果:

child.doSomething()
father.doSomething()
child.doAnotherThing()

这里的对象锁只有一个,就是child对象的锁,当执行child.doSomething时,该线程获得child对象的锁,在doSomething方法内执行doAnotherThing时再次请求child对象的锁,因为synchronized是重入锁,所以可以得到该锁,继续在doAnotherThing里执行父类的doSomething方法时第三次请求child对象的锁,同理可得到,如果不是重入锁的话,那这后面这两次请求锁将会被一直阻塞,从而导致死锁。

所以在java内部,同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。因为java线程是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的(java中线程获得对象锁的操作是以每线程为粒度的,per-invocation互斥体获得对象锁的操作是以每调用作为粒度的)

我们再来看看重入锁是怎么实现可重入性的,其实现方法是为每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。

volatile和synchronized的区别

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
  3. volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
  4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

解决办法:

1.使用synchronized同步锁。

缺点:同步锁对程序性能的影响太大了。

2.使用volatile关键字。

优点:轻量级。保证用volatile修饰的变量对所有线程的可见性(当一个线程修改了变量的值,新的值会立刻同步到主内存当中,其他线程读取这个变量的时候,会从主内存中拉取最新的变量值)。

缺点:不能保证数据的原子性,比如有i++操作时还是有可能出现错误。

volatile正确使用

JMM内存模型+volatile+synchronized+lock的更多相关文章

  1. 并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则

    前言 楼主这个标题其实有一种作死的味道,为什么呢,这三个东西其实可以分开为三篇文章来写,但是,楼主认为这三个东西又都是高度相关的,应当在一个知识点中.在一次学习中去理解这些东西.才能更好的理解 Jav ...

  2. JMM内存模型相关笔记整理

    JMM 内存模型是围绕并发编程中原子性.可见性.有序性三个特征来建立的 原子性:就是说一个操作不能被打断,要么执行完要么不执行,类似事务操作,Java 基本类型数据的访问大都是原子操作,long 和 ...

  3. JMM内存模型详解(一)

    本文开始死磕JMM(Java内存模型)由于知识点较多,分来写 该文为JMM第一篇 技术往往是枯燥的,本文文字较多 1. JMM是什么? 其实JMM很好理解,我简单的解释一下,在Java多线程中我们经常 ...

  4. JVM学习(七)JMM内存模型

    一.什么是JMM 概念:Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能 ...

  5. 基础篇:深入JMM内存模型解析volatile、synchronized的内存语义

    目录 1 java内存模型,JMM(JAVA Memory Model) 2 CPU高速缓存.MESI协议 3 指令重排序和内存屏障指令 4 happen-before原则 5 synchronize ...

  6. JMM 内存模型 与 volatile 关键字

    内存模型 线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory). 本地内存中存储了该线程以读/写共享变量的副本. 不同线程之间无法相互 ...

  7. 多线程与Java的JMM内存模型

    共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见.从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存( ...

  8. JMM内存模型、CPU缓存一致性原则(MESI)、指令重排、as-if-serial、happen-before原则

    JMM三大特性原子性 汇编指令 --原子比较和交换在底层的支持 cmp-chxg 总线加锁机制 Synchronized Lock锁机制 public class VolatileAtomicSamp ...

  9. java内存模型-volatile

    volatile 的特性 当我们声明共享变量为 volatile 后,对这个变量的读/写将会很特别.理解 volatile 特性的一个好方法是:把对 volatile 变量的单个读/写,看成是使用同一 ...

随机推荐

  1. spark练习---ip匹配以及广播的特性

    今天,我们还是在介绍spark的小练习,这次的小练习还是基于IP相关的操作,我们可以先看一下今天的需求,我们有两个文件, 第一个文件,是IP的字典,也就是我们上一篇介绍过的,就是表明了所有IP字段所属 ...

  2. issubclasss/type/isinstance/callable/super

    issubclass() : 方法用于判断第一个参数是否是第二个参数的子子孙孙类. 语法:issubclass(sub, super) 检查sub类是否是 super 类的派生类 class A: p ...

  3. istringstream输入数据到数组

    istringstream iss(line); ; while (!(iss >> dat[n]).fail()) n++;

  4. 关于代码通过API操作阿里云RDS的巨坑

    由于项目原因,要通过API操作阿里云的数据库,于是简单研究了一下阿里云提供的相关文档,发现官方提供了.NET的SDK,而且还提供了github开源代码,这个要为阿里点赞! 于是到github上弄了一份 ...

  5. 我给女朋友讲编程CSS系列(4) CSS盒子模型

    什么是CSS盒子模型?如何学习CSS的盒子模型? 这篇文章,以 [分享 + 结论]  的方式来写. 1,  看w3school的[CSS 框模型概述] 网址为: http://www.w3school ...

  6. webstrom11 vue插件配置

    直接上图 1. 安装vue插件 2.添加模板 3.指定模板类型 最新的是插件 是 vue.js 创建完 Vue File 文件后 需要在 下面这里关联一下

  7. 牛客网暑期ACM多校训练营(第一场):J-Different Integers(分开区间不同数+树状数组)

    链接:J-Different Integers 题意:给出序列a1, a2, ..., an和区间(l1, r1), (l2, r2), ..., (lq, rq),对每个区间求集合{a1, a2, ...

  8. web知识清单

    声名随笔中的实例链接到另一个博客是我本人的另一个博客号 模块一:HTML 1.html是什么: hyperText markup language超文本标记语言 超文本:比文本更丰富的内容 所有的浏览 ...

  9. 系统编程--标准IO

    1.流和FILE对象 对于国际字符集,一个字符可以由一个以上的字节来表示.标准I/O文件流可以用来操作单字节和多字节(宽,wide)字符集.一个流的方向(orientation)决定了字符是以单字节还 ...

  10. linux备忘录-vi和vim

    知识点 vi的三种模式 一般模式 按 ESC 可回到一般模式 相关按键 j 代表 向下按钮 k 代表 向上按钮 h 代表 向左按钮 l 代表 向右按钮 20j 等代表 向下移动20行 Ctrl + f ...