一、前言

上文讲过了虚拟机的内存划分,即,我们将内存分为线程共享和线程私有。

线程共享的即java堆,和方法区。java堆大家可能都不会陌生;而方法区中包含了常量池,他也被称为永久代。通常方法区也会被叫做非堆,但是在逻辑上,他却是java堆的一部分,而且有些虚拟机会将方法区直接与java堆合并。

线程私有的就是虚拟机栈了,而虚拟机栈,本地方法栈,以及程序计数器。这里我们就不展开讨论了。

上面我就简单的回顾了虚拟机的内存划分部分,下面开始正文。

二、java内存模型简述

1、主内存

java内存模型规定了,所有的变量都必须存储在主内存当中。

2、工作内存

每天线程私有的内存,即工作内存。

工作内存中保存了该线程所使用的变量的主内存的副本的拷贝。线程对变量所做的操作,都必须在工作内存中进行。

不同个的线程,无法访问对方的工作内存变量,只能通过主内存,来达到线程、工作内存、主内存三者之间的信息交互。

简图如下:



主内存、工作内存,与我上一篇博客中讲述的java内存区域中的堆、栈、方法区等,并不是同一个层次的内存划分。

不同,为了方便记忆,我们可以这么理解:

主内存对应的是java堆中的实例数据部分,工作内存对应的是java虚拟机栈中的部分区域。

从计算机的组织原理来说,我们也可以这么来理解,主内存对应的是物理硬件的内存,所以如果主内存与进程进行数据交互,它将是非常耗时的。

工作内存优先存储在寄存器和高速缓存中,因为程序在运行一般访问的是工作内存。

(所以我在上篇博客的开头就讲了,抛开操作系统和组织原理来讲虚拟机,就是在耍流氓 =_=)

三、关于原子性的二三事

1、从一段代码开始

No BB, show code

    private static volatile  int i = 0;
public static void add(){
i++;
} public static void main(String [] args){ for(int c=0; c<20; c++){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for(int k = 0; k<10000; k++){
add();
}
}
});
thread.start();
}
while (Thread.activeCount()>1){
Thread.yield();
}
System.out.println(i);
}

如果你看过上面的代码,那可以继续阅读,如果没有见过上面的代码,这里建议思考下,最后输出的值是多少?

显而易见,结果并不是200000。(如果最后的结果就是200000,那么我举这个例子干嘛 =。=)

2、虚拟机内部的原子操作

无论是长辈,还是其他人的建议,都提过,带着问题阅读的效率会比漫无目的阅读,效果好很多,所以上面我提出了问题,下文自然是为了解决问题而展开的额讨论和说明。这里,先从java虚拟机内存的操作开始讲起。

lock:作用于主内存,将主内存的某变量标志为一条线程独占。
unlock:作用于主内存,将主内存中的变量,从锁定状态解放出来,解放出来的变量,才可以重新被其他线程占用。
read:作用于主内存,将主内存中的变量,从主内存传输到工作内存中。
load:作用于工作内存,将read到的值,放到工作内存的副本当中。
use:作用于工作内存,将工作内存中的一个变量,传递给执行引擎。当虚拟机执行的字节码指令,运用到此值时,使用此操作。
assign:作用于工作内存,将从执行引擎接受到的值,赋值给工作内存的变量。每当执行字节码的赋值语句时,会使用此操作。
store:作用于工作内存的变量,将工作内存中的变量,传输到主内存中。
write:作用于主内存,将store中从工作内存获取到的变量,放到主内存的变量当中。

3、原子操作的划分

原子操作分为两部分,一般,通过read、load、use、write等读写操作,就可以保证数据的原子性。

但是有时候我们需要整块的业务代码,都具有原子性时,就需要使用lock与unlock。

4、volatile说明

细心的同学可能已经发现了,我上面的代码中。遍历时被volatile声明。

那么volatile的作用是什么呢?

一般来说,volatile变量对所有的线程,都是理解可见的。对于volatile变量所有的写操作,都能理解反应到其他线程中。

换言之,volatile在所有线程中都是一致的,所以,所有基于volatile变量的运算在并发下都是安全的。

其实不然,volatile变量,并不能保证并发安全。

(1)执行结果对比
变量类型 执行结果1 执行结果2 执行结果3 执行结果4 执行结果5 平均值(去掉极值)
volatile 186632 196403 193658 197305 186825 192295
一般变量 178387 179369 189835 174015 199458 182530

我记录了五次代码的执行结果。如上表格所示。都不是我们的目标值200000。那是不是说明volatile声明的变量和不进行声明,是完全一致的呢?

非也,我在去掉了volatile声明后,执行得到的结果,如上表格展示。

最后得出的结论是,加了volatile声明,结果更加趋近目标值。造成这一现象的原因是什么呢?

(2) 从字节码开始说明

查看字节码的方式有一般有两种。

一是找到生产的class文件,执行 javap指令,查看编译的代码。

二是,如果你用的是idea编辑器(idea天下第一),你可以在选中要查看的java类后,点击view菜单 点击 Show Bytecode。

这两种方式,我一般选择方式二,方式二方便,且查看的代码格式符合我的阅读习惯。

 public static void add();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field i:I
3: iconst_1
4: iadd
5: putstatic #2 // Field i:I
8: return
LineNumberTable:
line 12: 0
line 13: 8
volatile声明对象的字节码指令执行流程说明

volatile变量声明的对象,的的确确是,当他在主内存中的值发生变化,会立即反应到工作内存中。

这里就有一个节点,也就是我们字节码中的

getstatic

getstatic指令,此指令,是获取了当前最新的实时的变量值。后续的对此值进行+1操作,然后返回。但是可能存在一个情况,就是在执行+1操作或者返回操作时,其他线程对这个值进行了处理,导致此线程返回的值并不是正确值了。

可能还是不太理解,我们模拟一下场景。

  • 场景①

    时刻1:线程A获取了此值1。(最快)

    时刻2:线程B获取了此值1。(次快)

    时刻3:线程C获取了此值1.(最慢)
  • 场景②

    时刻4:线程A处理完毕了值,且write了值到主内存,执行完毕后主内存的值为2.

    时刻5:线程B,在线程A修改完主内存值后,才开始执行getstatic指令,最后他执行的是2+1,执行完毕后主内存值为3

    时刻6:线程C,在执行getstatic方法时,线程A已经写完数据到了主内存,而线程B还在进行+1操作。所以此时主内存值为2。他再对2进行+1操作,执行完毕后,将3写入主内存。

    所以最后主内存的值为3,并不是我们的目标值4。这也是我们的代码执行结果了,小于200000的原因。
不加volatile声明对象的字节码流程说明

不加volatile声明,可能在进入线程后,未进行getstatic指令前,变量值发生了改变,而线程不知道。

所以,这也就是加了

四、线程安全的正确姿势

讲到这里,我想大家应该对上方的代码执行结果没有什么疑虑了。

但是问题又来了,如何确保能正确的得到目标值呢。

1、万能的synchronized

相比大家看到此关键字,就已经知道了我下面要讲什么了。

 public synchronized static  void add(){
i++;
}

对add方法,加了synchronized关键字进行修饰之后,最后得到的目标结果,就是我们的目标值20000了。

当然,越是万能,往往代表越是无能。

此方法的性能会比使用自己手动的进行lock以及unlock,性能要差很多。

特别是在1.5的jdk版本,性能差异非常大。不过在后续的jdk版本中,逐渐对synchronized在进行优化。而且官方也推荐这种方式,毕竟,他较之ReentrantLock要优雅、coooooool很多。

2、高性能的ReentrantLock

private static  int i = 0;
private static ReentrantLock lock = new ReentrantLock(); public static void add(){
lock.lock();
try{
i++;
}finally {
lock.unlock();
}
}

即使是i++,我们也要进行try,这是为了养成良好的语义习惯 =_=

每一次加锁,必然要进行一次解锁。不然....嘿嘿嘿嘿

需要说明的是,ReentrantLock(重入锁)比之synchronized,多了其他的高级功能,等待可中断、实现公平锁、所可以绑定多个条件。这里就不进行展开讨论。

3、狭隘的AtomicInteger

 private static AtomicInteger i =new AtomicInteger(0);
public static void add(){
i.addAndGet(1);
}

Atomic对象有很多,如AtomicBoolean、AtomicLong等。

他保证了数据操作的原子性,实现原理是通过CAS原理。

何为CAS?即比较和交换:

获取主内存值(A),将获取到的值(A)与新的值(B)放入参数。在此获取其值,如果,获取到的值与传输的值A一致,就修改主内存值为新的值B。

这也就是CAS

当然在Atimic的实现中,还是用了Unsafe类,他可以直接操作物理内存!!!!

这里我们不对他详细的展开论述。

五、总结

内存模型中,分为工作内存与主内存。

这么讲其实没意义,我换个说法,为什么要区分工作内存和主内存??

线程是程序运行的基础,而线程需要与计算机进行数据交换,而由于计算机的组成,进行数据交换会,有的内存区域传输快,有的传输慢。而且也为了保证数据的安全性,我们区分出了主内存(可以狭义的理解为物理内存)与工作内存(寄存器即高速缓存,当量大时,也会存储到物理内存中)

在重温JVM时,我多次的是思考了为什么?也就是为什么要这么设计,这么设计有什么好处,收益颇多。

六、参考

《深入理解Java虚拟机》

深入理解JVM(二)JVM内存模型的更多相关文章

  1. JVM 系列(二)内存模型

    02 JVM 系列(二)内存模型 一.JVM 内存区域 JVM 会将 Java 进程所管理的内存划分为若干不同的数据区域.这些区域有各自的用途.创建/销毁时间: 一. 线程私有区域 线程私有数据区域生 ...

  2. (转载)JVM中的内存模型与垃圾回收

    转载自微信公众号:Java高级架构(Java-jiagou)-----看完这篇文章,我奶奶都知道JVM中的内存模型与垃圾回收了! 六.内存模型 6.1  内存模型与运行时数据区 Java虚拟机在执行J ...

  3. JVM学习笔记——内存模型篇

    JVM学习笔记--内存模型篇 在本系列内容中我们会对JVM做一个系统的学习,本片将会介绍JVM的内存模型部分 我们会分为以下几部分进行介绍: 内存模型 乐观锁与悲观锁 synchronized优化 内 ...

  4. 深入理解JVM(二)——内存模型、可见性、指令重排序

    上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存 ...

  5. 轻松学JVM(二)——内存模型、可见性、指令重排序

    上一篇我们介绍了JVM的基本运行流程以及内存结构,对JVM有了初步的认识,这篇文章我们将根据JVM的内存模型探索java当中变量的可见性以及不同的java指令在并发时可能发生的指令重排序的情况. 内存 ...

  6. 深入理解JVM(6)——Java内存模型和线程

    Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM)用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果(“即Ja ...

  7. 深入理解JAVA虚拟机(内存模型+GC算法+JVM调优)

    目录 1.Java虚拟机内存模型 1.1 程序计数器 1.2 Java虚拟机栈 局部变量 1.3 本地方法栈 1.4 Java堆 1.5 方法区(永久区.元空间) 附图 2.JVM内存分配参数 2.1 ...

  8. 深入理解JVM(二)Java内存区域

    2.1 C.C++内存管理是由开发人员管理,而Java则交给了JVM进行自动管理 2.2 JVM运行时数据区:方法区.堆(运行时线程共享),虚拟机栈.本地方法栈.程序计数器(运行时线程隔离,私有) 1 ...

  9. 理解JVM之java内存模型

    java虚拟机规范中试图定义一种java内存模型(JMM)来屏蔽掉各种硬件和操作系统内存访问差异,以实现让java程序在各种平台都能打到一致的内存访问效果.所以java内存模型的主要目标是定义程序中各 ...

  10. 【JVM】JVM系列之内存模型(六)

    一.前言 经过前面的学习,我们终于进入了虚拟机最后一部分的学习,内存模型.理解内存模型对我们理解虚拟机.正确使用多线程编程提供很大帮助.下面开始正式学习. 二.Java并发基础 在并发编程中存在两个关 ...

随机推荐

  1. Java生成压缩文件(zip、rar 格式)

    jar坐标: <dependency> <groupId>org.apache.ant</groupId> <artifactId>ant</ar ...

  2. BZOJ 2013 : [Ceoi2010]A huge tower / Luogu SP6950 CTOI10D3 - A HUGE TOWER

    传送门 菜鸡.jpg CODE #include <bits/stdc++.h> using namespace std; const int MAXN = 620005; int n, ...

  3. More development resources

    社区 名称 官网 google https://www.google.com/ github https://github.com/ StackOverflow https://stackoverfl ...

  4. c/c++读取一行可以包含空格的字符串(getline,fgets用法)

    1.char[]型 char buf[1000005]; cin.getline(buf,sizeof(buf)); 多行文件输入的情况: while(cin.getline(buf,sizeof(b ...

  5. 写简单的tb(testbench)文件来测试之前的FSM控制的LED

    先上我之前写的状态机控制的led代码led_test.v module led_test(clk,led_out); input clk; :] led_out; initial begin led_ ...

  6. Linux下MongoDB非正常关闭启动异常解决方法

    1.将配置信息写入一个文件中 vim mongo.conf 里面写如下内容: dbpath=/usr/local/mongodb/data/ logpath=/usr/local/mongodb/lo ...

  7. 年视图,选择月份 ---bootstrap datepicker

    $.fn.datepicker.dates['cn'] = { //切换为中文显示 days: ["周日", "周一", "周二", &qu ...

  8. js获取整个屏幕的尺寸

    原文 首先获取屏幕宽度:window.screen.width;    //整个屏幕的宽度. 然后获取屏幕高度:window.screen.height;     //整个屏幕的高度. 获取可用工作区 ...

  9. 深度学习变革视觉计算总结(CCF-GAIR)

    孙剑博士分享的是<深度学习变革视觉计算>,分别从视觉智能.计算机摄影学和AI计算三个方面去介绍. 他首先回顾了深度学习发展历史,深度学习发展到今天并不容易,过程中遇到了两个主要障碍: 第一 ...

  10. Raspberry PI 2上的802.11ac网卡驱动编译

    Raspberry PI 2上的802.11ac网卡驱动编译 最近在树莓派2上折腾视频,用来做FPV,但是发现2.4G的控会严重干扰2.4G WIFI,在开控的时候我的台式机+外置USB网卡都频频掉线 ...