Java虚拟机--内存模型与线程

高速缓存:处理器要与内存交互,如读取、存储运算结果,而计算机的存储设备和处理器的运算速度差异巨大,所以加入一层读写速度和处理器接近的高速缓存来作为内存和处理器之间的缓冲——将运算所需数据复制到缓存中,使得运算能快速进行;当运算结束后再将缓存同步回内存中,这样处理器无需等待缓慢的内存读写。

每个处理器都有自己的高速缓存,它们都共享同一个主内存,当多个处理器的运算任务都涉及同一块主内存区域时,将导致各自的缓存数据不一致,此时要同步数据到主内存以哪个处理器的缓存为主?这就是缓存一致性。为了解决这个问题,需要遵循一些缓存一致性协议。

Java内存模型

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储带内存以及从内存中取出变量这样的底层细节。这里的变量指的是非线程私有的实例字段、静态字段和构成数组对象的元素,不包括局部变量和方法参数,因为它们是线程私有、不被共享的。

Java内存模型中,所有的变量都存储在主内存中,每条线程还有自己的工作内存(与高速缓存类比),线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量;不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需通过主内存完成

如果将Java内存模型和Java堆、栈比较,主内存对应Java堆中的对象实例部分,工作内存对应虚拟机栈中的部分

内存交互

主内存和工作内存之间需要交互,Java内存模型中有8种原子操作:

  • lock:作用于主内存变量,将其标识为线程独占。
  • unlock:作用于主内存变量,将其从锁定状态释放,释放后才可被其他线程锁定。
  • read:作用于主内存的变量,将一个变量从主内存中传输到工作内存中,以便随后的load动作使用。
  • load:作用于工作内存中的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use:作用于工作内存的变量,把工作内存中的一个变量的值传递给执行引擎,当虚拟机需要使用到变量的值的字节码指令时会执行这个操作。
  • assign:作用于工作内存的变量,把一个从执行引擎接受到的值赋给工作内存中的变量。当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store:作用于工作内存中的变量,把工作内存中的一个变量的值传送到主内存中,以便之后write操作使用。
  • write:作用于主内存中的变量,把store操作从工作内存中得到的变量值放入主内存的变量中。

如果一个变量从主内存复制到工作内存,必须先执行read然后执行load操作(read和load之间允许插入其他操作,只要保证这个顺序即可);如果要把变量从工作内存同步回主内存中,需要先执行store操作然后执行write操作(store和write之间允许插入其他操作,只要保证这个顺序即可)。

volatile关键字

推荐阅读这篇blog

一个变量被定义为volatile后,具备两个特性:

可见性。当一条线程修改了这个变量的值,新值能被其他线程立刻观察到。这是说:volatile的作用是在本CPU对变量的修改直接写入主内存中,同时这个写操作使得其他CPU中对应比变量的缓存行无效,这样其他线程在读取这个变量时候必须从主内存中读取,因为读取到的是最新的,这就是上面说得能被立即“看到”。

而且volatile在其汇编代码中有一个lock操作,这个操作相当于一个内存屏障,保证了有序性,禁止了指令重排:具体来说在执行到volatile变量时,其之前的语句一定被执行过了且结果对后面是已知的,而其后面的语句一定还没执行到;在volatile变量之前的语句不能被重排后其之后,相反其后的语句也不能被重排到之前。

而对于普通变量,普通变量在线程间的传递必须通过主内存这个“桥梁”,即须线程A修改了变量值后,向主内存回写,线程B在等回写完成后再从主内从中读取,新变量这时才对线程B可见。

volatile指令只能保证可见性、有序性,不能保证原子性。比如多个线程执行i++时。i++等价于i = i + 1这语句分为三步,首先读取i的值,作加1操作,将新值赋给i。有种情况是,即使i被volatile修饰,保证读取到的值是最新的正确的,假如i初始值为0,现在A、B线程都正确读取到后i的值为0,B现在加1直接写入主内存中,回到A,因为A读取过了,且读到的是0,这是A线程加1操作写入主内存中,i还是1,进行了两次自增,最后i的值却值增加了1。所以在多线程中即使使用了volatile也不能保证线程安全。

原子性、可见性、有序性

  • 原子性。Java内存模型能直接保证原子性变量操作有:read、load、assign、use、store、write,Java的基本数据类型的访问读写大致是具备原子性的。而lock和unlock这样的原子操作是由Java中的同步块——synchronized块,被其包围的块也具有原子性。
  • 可见性。Java内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量,这些操作都以来主内存这个“桥梁”。普通变量和volatile变量都是如此,不过volatile可保证新值能立即同步到主内存;使用前立即从主内存刷新,可以说volatile保证了多线程操作时变量的可见性。synchronized和final关键字也可实现可见性,前者:对一个变量执行unlock之前,必须先把次变量同步回主内存中(store、write);后者:final修饰的字段在构造器中一旦初始化完成且this引用没有被传递出去(this引用逃逸),其他线程中能看见final字段的值。
  • 有序性。在本线程内观察,所有操作都是有序的,这表现为“线程内表现为串行的语义”;在一个线程中观察另一个线程,所有操作都是无序的,表现为“指令重排”和“工作内存与主内存的延迟”。volatile本身禁止了指令重排,而synchronized保证了一个变量在同一个时刻只允许一条线程对其进行lock操作

先行发生原则

如果操作A先于操作B发生,其实就是说发生操作B之前,操作A产生的影响(修改了共享变量的值、发送了消息、调用了方法等)能被操作B观察到。Java内存模型自身具备以下先行发生关系,这些原则是指令重排不可违背的。

  • 程序次序原则:一个线程内保证语义的串行性
  • 锁规则:一个unlock操作先行发生于后面对同一个锁的lock操作
  • volatile规则:对一个volatile变量的写操作先行发生于对这个变量的操作
  • 线程启动规则:线程的start()方法先于次线程的任何动作
  • 线程终止规则:线程的所有操作都先行发生于线程终结
  • 线程中断规则:对线程的interrupt()方法的调用先于被中断线程的代码
  • 对象终结规则:对象的初始化完成先于它的finalize()方法
  • 传递性:如果A先于B,B先于C,那么A先于C

线程

线程的实现主要有三种方式。

使用内核线程实现

内核线程就是直接由操作系统内核支持的线程,由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。

程序一般不直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程——也就是通常意义所讲的线程,每个轻量级进程都有一个内核线程支持,是一一对应的关系

每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作。但是轻量级进程基于内核线程,需要进行系统调用,而系统调用的代价高,需要在用户态和内核态中来回切换,所以轻量级进程要消耗内核资源。

使用用户线程实现

狭义上的用户线程是无需内核帮助的,完全建立在用户空间的线程库上,进程与用户线程的关系是一对多的线程模型。

用户线程无需内核支援,但这也是个缺点,所有的操作都需要用户程序自己处理,所以使用用户线程实现的程序一般比较复杂。

混合实现

将轻量级进程和用户线程混合。这种模式下,既存在用户线程又存在轻量级进程,轻量级进程成为了用户线程和内核线程的桥梁。用户线程和轻量级进程的数量比不定,可以称为是多对多的模型。

Java线程调度

大体上有两种调度方式。

  • 协同式线程调度:线程的执行时间由线程自己决定,当执行完自己的工作后,主动通知系统切换到另一个线程上。
  • 抢占式线程调度:每个线程的执行时间是由系统分配的,线程的切换不由线程本身决定,不会出现一个线程导致整个进程阻塞的问题。

Java使用的线程调度方式就是抢占式线程调度

Java的线程调度是系统自动完成的,但是可以通过设置线程的优先级,优先级越高的线程越容易被系统选择执行。

Java线程状态

共有6种线程状态。

  • 新建(New):创建后尚未启动的线程;
  • 运行(Runnable):包括了操作系统线程状态中的Running和Ready,此状态的线程有可能正在执行也可能正等待着CPU为它分配执行时间
  • 无限期等待(Waiting):此状态下不会被分配CPU执行时间,需要等待被其他线程唤醒
  • 限期等待():此状态下也不会被分配CPU时间,但无需被其他线程显式唤醒,在一定时间后会有系统自动唤醒
  • 阻塞(Blocked):线程被阻塞,阻塞状态中的线程在等待着获取到一个排他锁,这个事件将在其他线程放弃这个锁时发生;和“等待状态”不同,等待是在等待一段时间或者其他的线程的显式唤醒,在程序进入同步区域时,线程会进入阻塞状态。
  • 结束(Terminated):已终止线程的状,表示线程已经结束执行。


by @sunhaiyu

2018.6.20

Java虚拟机--内存模型与线程的更多相关文章

  1. Java虚拟机内存模型及垃圾回收监控调优

    Java虚拟机内存模型及垃圾回收监控调优 如果你想理解Java垃圾回收如果工作,那么理解JVM的内存模型就显的非常重要.今天我们就来看看JVM内存的各不同部分及如果监控和实现垃圾回收调优. JVM内存 ...

  2. Java虚拟机 - 内存模型

    本文主要介绍Java虚拟机的内存分布以及对象的创建过程. 一.Java虚拟机的内存分布 文章开始前读者需要了解Java虚拟机的运行时数据区是怎样划分的.如下图所示: 1.程序计数器(Program C ...

  3. JAVA虚拟机内存模型

    一.对于Java程序员来说,在虚拟机的自动内存管理机制下,我们不需要为每一个new操作去写匹配的delete/free操作 但是当我们对于内存的管理了解有能够帮助我们理解Java虚拟机的垃圾回收机制. ...

  4. (三)java虚拟机内存管理和线程独占区和线程共享区

    一.内存管理 二.线程独占区之程序计数器(Program Counter Register) 程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器.在虚拟机的概念模型里,字节 ...

  5. 深入理解Java虚拟机内存模型

    前言 本文中部分内容引用至<深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)>第12章,如果有兴趣可自行深入阅读,文末放有书籍PDF版本连接. 一.物理机中的并发 物理机遇到的并 ...

  6. 面试常问的Java虚拟机内存模型,看这篇就够了!

    一.虚拟机 同样的java代码在不同平台生成的机器码肯定是不一样的,因为不同的操作系统底层的硬件指令集是不同的. 同一个java代码在windows上生成的机器码可能是0101.......,在lin ...

  7. java 虚拟机内存模型

    [声明] 欢迎转载,但请保留文章原始出处→_→ 文章来源:[http://www.cnblogs.com/smyhvae/p/4748392.html] 文章来源:[http://www.cnblog ...

  8. Java虚拟机03(Java虚拟机内存模型)

    根据 JVM 规范,JVM 内存共分为虚拟机栈.堆.方法区.程序计数器.本地方法栈五个部分. 其实最需要Java程序员关注的是堆,栈,还有方法区,因为啊: 如果代码又问题的话,可能回出现栈溢出 然后说 ...

  9. 作业1:java虚拟机内存模型图示

    看了很多篇文章,整理成一幅图,但仍然有许多不解的地方,以后再接着完善,哪位大神看到不正确的地方,请指出,谢谢.

随机推荐

  1. 【推荐】Win7任务栏增强工具 7+ Taskbar Tweaker 强大的任务栏标签管理工具

    我曾经推荐过一款XP的任务栏管理工具 Taskix,这是一款在XP系统中拖动任务栏内标签的小工具. XP 32位可以下载我汉化的版本 http://www.cnblogs.com/clso/archi ...

  2. ServiceStack 错误处理

    抛出C#异常 在大多数情况下,您不需要关心ServiceStack的错误处理,因为它为抛出C#异常的正常用例提供本机支持,例如: public object Post(User request) { ...

  3. Linux下安装MySQL以及一些小坑

    第一次写博客,各位凑合着看吧(假装有人看). 我这里使用的是centos7. 1.首先打开终端,查看有没有安装过MySQL: [root@localhost lyp]# rpm -qa | grep ...

  4. Atom 清空残余数据

    rm -rf ~/.atom rm /usr/local/bin/atom rm /usr/local/bin/apm rm ~/Library/Preferences/com.github.atom ...

  5. 在ubuntu中我们使用sudo apt-get install 或者dpkg -i *.deb安装软件时,常常提示“有未能满足的依赖关系“,解决方法

    很早之前在ubuntu安装软件时遇到的问题,今天打开ubuntu看到了,总结如下: 在ubuntu中我们使用sudo apt-get install 或者dpkg -i *.deb安装软件常常提示“有 ...

  6. redis之事务

    一.是什么 可以一次执行多个命令,本质是一组命令集合.一个事务中的所有命令都会序列化,按顺序的串行化执行而不被其他命令插入,不许加塞.一个队列中,一次性.顺序性.排他性的执行一系列命令. 二.事务常用 ...

  7. 什么是NAT端口映射?

    背景:我们访问百度的时候输入www.baidu.com出现的网站首页.发生了什么事情?百度它有服务器,IP地址是公网的,有自己的域名,所以你可以正常访问到百度(实际上访问的是IP地址+服务端口).如果 ...

  8. Nginx单向认证的安装配置

    Nginx单向认证的安装配置 首先系统要已经安装了openssl,以下是使用openssl安装配置单向认证的执行步骤与脚本: #------------------------------------ ...

  9. 【源码分析】HashMap源码再读-基于Java8

    最近工作不是太忙,准备再读读一些源码,想来想去,还是先从JDK的源码读起吧,毕竟很久不去读了,很多东西都生疏了.当然,还是先从炙手可热的HashMap,每次读都会有一些收获.当然,JDK8对HashM ...

  10. Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二):数据库设计

    数据库设计 系统主要包含用户(sys_user).组织(sys_dept).角色(sys_role).菜单(sys_menu).角色组织(sys_role_dept).角色菜单(sys_role_me ...