导入JDK源代码后,在你的代码里按住Ctrl并点类名或方法名就看到源代码了。
但是JDK底层实际上为了跨平台用的是C/C++调用平台本身的功能,这部分JDK的srczip不包含,但是在Oracle领导的OpenJDK这个项目里面可以看到其开源实现。 1底层由链表+数组实现
2可以存储null键和null值
3线性不安全
4初始容量为16,扩容每次都是2的n次幂(保证位运算)
5加载因子为075,当Map中元素总数超过Entry数组的075,触发扩容 *** 作
6并发情况下,HashMap进行put *** 作会引起死循环,导致CPU利用率接近100%
(1)HashMap底层实现数据结构为数组+链表的形式,JDK8及其以后的版本中使用了数组+链表+红黑树实现,解决了链表太长导致的查询速度变慢的问题。
(2)简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。HashMap通过key的HashCode经过扰动函数处理过后得到Hash值,然后通过位运算判断当前元素存放的位置,如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。当Map中的元素总数超过Entry数组的075时,触发扩容 *** 作,为了减少链表长度,元素分配更均匀。
DEFAULT_INITIAL_CAPACITY :默认的初始化容量,1<<4位运算的结果是16,也就是默认的初始化容量为16。当然如果对要存储的数据有一个估计值,最好在初始化的时候显示的指定容量大小,减少扩容时的数据搬移等带来的效率消耗。同时,容量大小需要是2的整数倍。
MAXIMUM_CAPACITY :容量的最大值,1 << 30位,2的30次幂。
DEFAULT_LOAD_FACTOR :默认的加载因子,设计者认为这个数值是基于时间和空间消耗上最好的数值。这个值和容量的乘积是一个很重要的数值,也就是阈值,当达到这个值时候会产生扩容,扩容的大小大约为原来的二倍。
TREEIFY_THRESHOLD :因为jdk8以后,HashMap底层的存储结构改为了数组+链表+红黑树的存储结构(之前是数组+链表),刚开始存储元素产生碰撞时会在碰撞的数组后面挂上一个链表,当链表长度大于这个参数时,链表就可能会转化为红黑树,为什么是可能后面还有一个参数,需要他们两个都满足的时候才会转化。
UNTREEIFY_THRESHOLD :介绍上面的参数时,我们知道当长度过大时可能会产生从链表到红黑树的转化,但是,元素不仅仅只能添加还可以删除,或者另一种情况,扩容后该数组槽位置上的元素数据不是很多了,还使用红黑树的结构就会很浪费,所以这时就可以把红黑树结构变回链表结构,什么时候变,就是元素数量等于这个值也就是6的时候变回来(元素数量指的是一个数组槽内的数量,不是HashMap中所有元素的数量)。
MIN_TREEIFY_CAPACITY :链表树化的一个标准,前面说过当数组槽内的元素数量大于8时可能会转化为红黑树,之所以说是可能就是因为这个值,当数组的长度小于这个值的时候,会先去进行扩容,扩容之后就有很大的可能让数组槽内的数据可以更分散一些了,也就不用转化数组槽后的存储结构了。当然,长度大于这个值并且槽内数据大于8时,那就转化为红黑树吧。
hashmap底层实现原理是SortedMap接口能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。
如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出javalangClassCastException类型的异常。
Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable
从结构实现来讲,HashMap是:数组+链表+红黑树(JDK18增加了红黑树部分)实现的。
扩展资料
从源码可知,HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组。Node是HashMap的一个内部类,实现了MapEntry接口,本质是就是一个映射(键值对),除了K,V,还包含hash和next。
HashMap就是使用哈希表来存储的。哈希表为解决冲突,采用链地址法来解决问题,链地址法,简单来说,就是数组加链表的结合。在每个数组元素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。
如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定哈希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞。
你安装JDK的目录下,有个srczip文件,这个就是JDK源代码的java文件。
你可以解压来查看,但,最好是关联到IDE如 eclipse 中(不需解压),然后 CTRL + 点击就可以查看到源代码了。
如下图:
Java零基础的小伙伴,我建议看Java的在线教程去学习。
第一:先学习Java的核心库(JavaSE)
JavaSE的内容包括:环境搭建、基础语法、面向对象、数组、集合、常用类、IO流、反射机制、网络编程……
第二:MySQL数据库
搞定一门数据库相关的课程,例如:MySQL、Oracle,搞定一个就可以了,目前互联网公司,例如:京东、阿里等,他们都在使用MySQL,所以建议大家学习MySQL数据库,小巧轻盈,免费。
由于互联网公司的项目访问量比较大,所以一般会搭建数据库的集群,可以一个数据库不够,所以需要搭建数据库集群,为了应付高并发。(搭建的比较多的时候,免费就很重要了。)
第三:WEB前端
以后从事Java开发,从事JavaEE开发,主要开发的系统结构是B/S结构的,B指的是Browser,S指的是Server。要开发这种系统,B端要会,S端也要精通。WEB前端的学习就是学习B端技术。
包括:HTML 、CSS、JavaScript(JS)、jQuery框架(底层对JS进行了封装)…
第四:JavaWEB
WEB后端其实可以是很多种不同的编程语言,例如:PHP、C、C++、Java,他们都可以进行WEB后端的开发,我们既然选择了比较火爆的Java,那么我们学习的后端一定是基于Java语言实现的。
包括:Servlet、Filter、Jsp、EL、JSTL、MVC架构模式、数据库连接池(阿里巴巴的Druid连接池)、代理模式(动态代理)。另外后端学习了之后,还要学习一个异步编程技术AJAX。(完成网页的局部刷新,AJAX其实不属于后端,是前端浏览器上的程序。)
学习到这里为止,表示Java基本/基础的技术已经学完了。但是这些最基层的技术在实际的开发中不会使用的,一般为了开发效率,都会使用大量的提前封装好的框架。
第五:最好能够停留下来,做一个项目。
这个项目最好能将之前所学全部串起来。(对以前的知识点进行巩固。)
这个项目最好是基于:Servlet + Jsp+AJAX+jQuery+MySQL…
在这个项目的开发过程中:大家一定要记住,目前比较好的项目自动构建工具:Maven是一定要精通的。还有一个就是团队协作开发:Git/SVN是一定要会用的。(目前使用Git比较多一些。)
第六:学习高级框架
Spring、SpringMVC、MyBatis(持久层框架,这个框架互联网公司使用比较多,因为互联网项目需要进行SQL优化,MyBatis的SQL优化很方便,所以大部分都是使用MyBatis)
Struts2(很少使用了,使用这个的肯定是很老的项目)、Hibernate(传统企业,还有政府等可能会使用Hibernate。)
SpringBoot(新项目大部分使用的都是boot了。所以在项目中遇到还在使用SSM的一般都是遗留项目。)
当你走到这里之后,基本上你可以出山了。(去找工作应该问题不大,但前提是你学得好。学习的深度够了,广度够了。)
第七:最好能有一个大型项目是使用框架来完成的。
SpringBoot做一个项目。
Spring SpringMVC MyBatis做一个项目。
这个项目最好是找几个人搭伙做一下。体验一下团队协作。(尤其是使用一些协作的工具。怎么沟通,怎么写日报,怎么开会,怎么使用Git,等等…)
第八:如果你的薪资想达到15K的话,你可能需要还要学习一些分布式相关的一些技术。
能够应付高并发的一些技术,例如:分布式框架Dubbo、SpringCloud、MQ、Nginx、Redis…
最后在啰嗦一下,一定要形成自己的编程思想。
编程思想的形成很难。需要大量代码的堆积才可以。怎么形成编程思想呢?
千万别死抄代码。不要像行尸走肉般的抄代码,没有用。只能提高指法速度,无法形成编程思路。
1、打好基础是重中之重
基础部分可能很枯燥,但是一定要耐心坚持下去。因为打基础好比是在造轮子造汽车,没有打好基础在后面的学习就好比走路,可以说后面遇到的百分之七八十的问题都是基础没学好造成的。
2、多看官方文档多读源码
一定要看JDK相关类库、常用框架各种功能的源码,去了解其底层实现的原理。总的来说这个也是在打基础的部分,Java基础非常扎实才能看懂,在我们学习一个类的源码时,肯定会衍生出其他各种各样的问题,供我们来了解和学习,这也就是我们下一步学习的目标和方向。慢慢的,我们就会学习更多的知识,并积累更多的经验。
3、系统性学习,循序渐进
不要急于求成,每个知识点都要看,并且每个知识点都要勤加练习。有的同学在学习过程中觉得这个知识点简单,就跳着学习,其实这是个很大的问题,知识点细节必须要了解。我们学习Java需要循序渐进,一步一步来,不能 *** 之过急。
4、遇到问题
遇到不会做的问题,当然不能置之不理。先在网上查,目前网络上针对很多问题都有完美的解决方案,如果网上没有类似的问题,那可能是你犯了什么小错误。也不要在这个问题上花费太多时间,这需要一定的学习技巧。Java中的知识有很强的相关性,有不懂的地方,可以用其他周边相关知识再回过头来理解,这样既不会耽误学习进度,又能理解自己不懂的地方
1)jvm有很多种,其实jvm是一个标准,sun做的那个叫hotspot,作者就是后来v8的作者lars bak,其他公司也做过jvm,其中做得比较好的有bea的jrockit,其他的包括ibm的r9,apple的jvm等在内,都做得不行,所以jvm主要是整合淘汰掉这些做得不好的jvm(s),整合成一个统一的openjdk。
2)java是典型的oop语言,其执行效率的优化,最早就是lars bak等人从smalltalk等长期优化的经验中总结出来并apply到hotspot上去滴,而smalltalk在早期apple机上搞出了那种拖拖拽拽就开发出app的做法,后来vc,delphi之类的其实都是抄袭或者说借鉴apple的smalltalk的做法,jobs说微软从头到尾都在抄袭apple是空穴来风,这里空穴来风跟王垠使用的空穴来风是一个意思,有趣的是,java的gui并没有继承这种搞法,反而对这种拖拖拽拽就作出app的做法批判有加,到今天,其实java的gui都还不能真正做到拖拖拽拽就作出来,问题很多,个人建议对于纯java的gui开发,还是以写代码为主。
3)jee也是一个或者说是一堆标准,知乎上有些人把maven,jenkins都算做jee是不对滴,jee的标准核心是ejb,其实就是一个xml配置化的java文件,这个标准在4的时候,达到了顶峰状态,几乎所有的挨踢大厂都主动支持该标准,之后开始走下坡路,支持的厂越来越少。
4)java和javascript的关系比很多人认为的要密切,javascript里面的java这四个字母可不是白叫的,比如js的版权和商标都控制在oracle手里,oracle对于js的支持甚至超过其对java的支持,并且喜欢捆绑销售,比如jvm里面就有一个js引擎。
5)jvm里面除了js engine以外还有一个浏览器排版引擎webkit,就是apple safari和google chrome用的那个那个。
6)java支持绝大多数脚本语言,你能叫得上名字的脚本语言,几乎都可以在jvm上执行,比如常见的js,ruby,python,甚至php,lua,只不过除了js以外你需要找到相关的脚本引擎。
7)spring的版权被控制在vmware手里,其实spring的那一大堆东西,本质上是一个非标准的jee实现,比如在jee里面用的inject,在spring里面就是autowire,当然spring曾经深刻滴影响了jee,所以有些东西比如di标准,是spring影响下制定出来的,所以spring的做法会比较特例一点。
8)maven上的jars数量前两天突破800万,其他语言的类库,排名第二的是npm,大概数量是maven的十分之一,也就是几十万,不知道现在突破100万没有,然后是gem,也就是ruby那个,大概是十几万,下来是python的module,大概数量级是几万,没突破十万。
9)java的标准是由一个叫做jcp的组织制定的,所有标准需要经过jcp的执行委员会通过方可执行,jcp几乎包括了你所知道的绝大多数知名挨踢公司和组织,比如google,apple,ibm,intel,arm,red hat,twitter等,还有一些教育机构,比如我国的北京大学,阿里最近一次申请jcp执行委员会成员资格,似乎投票不通过,最近一次执行委员会新增两个成员是arm和jetbrains。
10)微软也曾经是jcp甚至是java的主要贡献者,但是利益驱使下,想扩展java,从而破坏java跨平台的特性,所以跟sun闹翻,其本质原因就是想让客户写的java代码跟windows绑定,sun坚决不同意,闹翻,今天回头看这个结果,只能说:双输,sun挂了,微软的ria也离挂不太远了,silverlight已经放弃了,比起当年ie自带有jvm的支持来说,那完全就是两回事。
11)除了微软以外,jcp还缺少一个重要组织apache,因为apache跟oracle也闹翻了,oracle似乎并不在乎开源组织,而更在意商业公司的支持。
12)java曾经有一个内置的数据库,9之后被剥离。
13)j2me是j2se的子集。
14)vertx作者tim fox最早在vmware做spring时候看到了nodejs,萌生出了制作支持多核的nodex的想法,并在离开vmware后加入red hat将其实现,vmware看到后开始耍无赖,claim nodex后来改叫vertx的版权,不惜跟red hat打官司,后来各方妥协,将其交给eclipse foundation。
15)oracle在收购bea之前,一开始的目标并不是bea和bea的weblogic,而是jboss,但是jboss表现出了极为有种的一面,在oracle收购成功之前,投入了red hat的怀抱,因为都是开源组织,从此jboss成了red hat的一个子部门,oracle收购jboss失败之后,转向bea,庄思浩气死了,但是没用,最后还是被恶意收购。
16)sun在玩不下去之前最早尝试接触的目标是ibm,ibm嫌太贵,放弃之后,被转手给了oracle。(Java学习交流QQ群:589809992 我们一起学Java!)
17)vertx的作者tim fox在离开red hat之前曾经发过twitter抱怨,外人比如我们,猜测是因为red hat内部已经有了一个jboss,所以跟vertx在应用上有了重叠,所以导致tim fox的出走,但是出走之后,red hat答应对vertx做持续性的战略投入,所以vertx core的几个developers,其实拿的是red hat的工资,但是vertx的版权并不在red hat手里,而在eclipse foundation手里。
18)vertx的几个核心开发人员都是google summer of code的导师,每年年初时候会招收在校大学生搞项目。
19)教育机构相关:scala的作者马丁是德国人,eth的博导,groovy的主要是法国人,jruby背后是东京大学,jboss的作者是法国大学校x的校友,x就是伽罗瓦考不进去的那所大学,伽罗瓦进不了x,所以改读巴黎高师,tim fox毕业于帝国理工,主席去的那个,netty作者trustin lee是acm银牌,现在line工作,毕业于sky里面的延世大学,kotlin是毛子公司jetbrains的作品,看linkedin,很多人毕业自圣彼得堡大学,spring作者rod johnson是悉尼大学的音乐博士,hibernate作者gavin king是澳洲莫那什大学的数学本科毕业生,james gosling这种cmu和calgory的估计烂大街了,sun是斯坦福大学网络的意思,夹带两个私货,aspectj有一个维护小组在mcgill,hbase跟waterloo关系密切。
20)java早期被人认为慢,跟java坚持不用硬件加速渲染有关,死活就是不肯接入directx和opengl,7之后总算开窍,搞了一个图形引擎接入了directx/opengl。
21)casssandra是facebook做失败的项目,被贡献给了apache之后老树开花。
22)groovy被贡献给了apache,现在叫做apache groovy,ceylon被贡献给了eclipse,现在叫做eclipse ceylon。
23)netflix现在是java shop,之前是用net的。
先想到这么多,有空再写。
内存屏障 又称内存栅栏 是一组处理器指令 用于实现对内存 *** 作的顺序限制 本文假定读者已经充分掌握了相关概念和Java内存模型 不讨论并发互斥 并行机制和原子性 内存屏障用来实现并发编程中称为可见性(visibility)的同样重要的作用
内存屏障为何重要?
对主存的一次访问一般花费硬件的数百次时钟周期 处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存 *** 作的顺序 也就是说 程序的读写 *** 作不一定会按照它要求处理器的顺序执行 当数据是不可变的 同时/或者数据限制在线程范围内 这些优化是无害的
如果把这些优化与对称多处理(symmetric multi processing)和共享可变状态(shared mutable state)结合 那么就是一场噩梦 当基于共享可变状态的内存 *** 作被重新排序时 程序可能行为不定 一个线程写入的数据可能被其他线程可见 原因是数据 写入的顺序不一致 适当的放置内存屏障通过强制处理器顺序执行待定的内存 *** 作来避免这个问题
内存屏障的协调作用
内存屏障不直接由JVM暴露 相反它们被JVM插入到指令序列中以维持语言层并发原语的语义 我们研究几个简单Java程序的源代码和汇编指令 首先快速看一下Dekker算法中的内存屏障 该算法利用volatile变量协调两个线程之间的共享资源访问
请不要关注该算法的出色细节 哪些部分是相关的?每个线程通过发信号试图进入代码第一行的关键区域 如果线程在第三行意识到冲突(两个线程都要访问) 通 过turn变量的 *** 作来解决 在任何时刻只有一个线程可以访问关键区域
// code run by first thread // code run by second thread
intentFirst = true; intentSecond = true;
while (intentSecond) while (intentFirst) // volatile read
if (turn != ) { if (turn != ) { // volatile read
intentFirst = false; intentSecond = false;
while (turn != ) {} while (turn != ) {}
intentFirst = true; intentSecond = true;
} }
criticalSection(); criticalSection();
turn = ; turn = ; // volatile write
intentFirst = false; intentSecond = false; // volatile write
硬件优化可以在没有内存屏障的情况下打乱这段代码 即使编译器按照程序员的想法顺序列出所有的内存 *** 作 考虑第三 四行的两次顺序volatile读 *** 作 每一个线程检查其他线程是否发信号想进入关键区域 然后检查轮到谁 *** 作了 考虑第 行的两次顺序写 *** 作 每一个线程把访问权释放给其他线程 然后撤销自己访问关键区域的意图 读线程应该从不期望在其他线程撤销访问意愿后观察到其他线程对turn变量的写 *** 作 这是个灾难
但是如果这些变量没有 volatile修饰符 这的确会发生!例如 没有volatile修饰符 第二个线程在第一个线程对turn执行写 *** 作(倒数第二行)之前可能会观察到 第一个线程对intentFirst(倒数第一行)的写 *** 作 关键词volatile避免了这种情况 因为它在对turn变量的写 *** 作和对 intentFirst变量的写 *** 作之间创建了一个先后关系 编译器无法重新排序这些写 *** 作 如果必要 它会利用一个内存屏障禁止处理器重排序 让我们来 看看一些实现细节
PrintAssembly HotSpot选项是JVM的一个诊断标志 允许我们获取JIT编译器生成的汇编指令 这需要最新的OpenJDK版本或者新HotSpot update 或者更高版本 通过需要一个反编译插件 Kenai项目提供了用于Solaris Linux和BSD的插件二进制文件 hsdis是另 一款可以在Windows通过源码构建的插件
两次顺序读 *** 作的第一次(第三行)的汇编指令如下 指令流基于Itanium 多处理硬件 JDK update 本文的所有指令流都在左手边以行号标记 相关的读 *** 作 写 *** 作和内存屏障指令都以粗体标记 建议读者不要沉迷于每一行指令
x de c: adds r = r ;; ;
x de a : ld acq r =[r ];; ; b a a
x de a : nop m x ; c
x de ac: sxt r r =r ;; ;
x de b : cmp eq p p = r ; c
x de b : nop i x ;
x de bc: nd dpnt many x de ;
简短的指令流其实内容丰富 第一次volatile位于第二行 Java内存模型确保了JVM会在第二次读 *** 作之前将第一次读 *** 作交给处理器 也就是按照 程序的顺序 但是这单单一行指令是不够的 因为处理器仍然可以自由乱序执行这些 *** 作 为了支持Java内存模型的一致性 JVM在第一次读 *** 作上添加了注解ld acq 也就是 载入获取 (load acquire) 通过使用ld acq 编译器确保第二行的读 *** 作在接下来的读 *** 作之前完成 问题就解决了
请注意这影响了读 *** 作 而不是写 内存屏障强制读或写 *** 作顺序限制不是单向的 强制读和写 *** 作顺序限制的内存屏障是双向的 类似于双向开的栅栏 使用ld acq就是单向内存屏障的例子
一致性具有两面性 如果一个读线程在两次读 *** 作之间插入了内存屏障而另外一个线程没有在两次写 *** 作之间添加内存屏障又有什么用呢?线程为了协调 必须同时 遵守这个协议 就像网络中的节点或者团队中的成员 如果某个线程破坏了这个约定 那么其他所有线程的努力都白费 Dekker算法的最后两行代码的汇编指令应该插入一个内存屏障 两次volatile写之间
$ java XX:+UnlockDiagnosticVMOptions XX:PrintAssemblyOptions=hsdis print bytes
XX:CompileCommand=print WriterReader write WriterReader
x de c : adds r = r ;; ; b
x de c : st rel [r ]=r ;
x de cc: adds r = r ;; ;
x de d : st rel [r ]=r ; a
x de d : mf ;
x de dc: nop i x ;; ;
x de e : mov r =r ;
x de e : mov ret b =r x de e
x de ec: mov i ar pfs=r ; aa
x de f : mov r =r ;
这里我们可以看到在第四行第二次写 *** 作被注解了一个显式内存屏障 通过使用st rel 即 存储释放 (store release) 编译器确保第一次写 *** 作在第二次写 *** 作之前完成 这就完成了两边的约定 因为第一次写 *** 作在第二次写 *** 作之前发生
st rel屏障是单向的 就像ld acq一样 但是在第五行编译器设置了一个双向内存屏障 mf指令 或者称为 内存栅栏 是Itanium 指令集中的完整栅栏 笔者认为是多余的
内存屏障是特定于硬件的
本文不想针对所有内存屏障做一综述 这将是一件不朽的功绩 但是 重要的是认识到这些指令在不同的硬件体系中迥异 下面的指令是连续写 *** 作在多处理 Intel Xeon硬件上编译的结果 本文后面的所有汇编指令除非特殊声明否则都出自于Intel Xeon
x f c: push %ebp ;
x f d: sub $ x %esp ; ec
x f : mov $ x c %edi ; bf c
x f : movb $ x x a f (%edi) ; c d a af
x f f: mfence ; faef
x f : mov $ x %ebp ; bd
x f : mov $ x d %edx ; ba d
x f c: mov l x a f (%edx) %ebx ; fbe a da af
x f : test %ebx %ebx ; db
x f : jne x f ;
x f : movl $ x x a f (%ebp) ; c d a af
x f : movb $ x x a f (%edi) ; c d a af
x f : mfence ; faef
x f b: add $ x %esp ; c
x f e: pop %ebp ; d
我们可以看到x Xeon在第 行执行两次volatile写 *** 作 第二次写 *** 作后面紧跟着mfence *** 作 显式的双向内存屏障 下面的连续写 *** 作基于SPARC
xfb ecc : ldub [ %l + x ] %l ; e c
xfb ecc : cmp %l ; a e
xfb ecc c: bne pn %icc xfb eccb ;
xfb ecc : nop ;
xfb ecc : st %l [ %l + x ] ; e
xfb ecc : clrb [ %l + x ] ; c c
xfb ecc c: membar #StoreLoad ; e
xfb ecca : sethi %hi( xff fc ) %l ; fcff
xfb ecca : ld [ %l ] %g ; c
xfb ecca : ret ; c e
xfb eccac: restore ; e
我们看到在第五 六行存在两次volatile写 *** 作 第二次写 *** 作后面是一个membar指令 显式的双向内存屏障 x 和SPARC的指令流与Itanium的指令流存在一个重要区别 JVM在x 和SPARC上通过内存屏障跟踪连续写 *** 作 但是在两次写 *** 作之间没有放置内存屏障
另一方面 Itanium的指令流在两次写 *** 作之间存在内存屏障 为何JVM在不同的硬件架构之间表现不一?因为硬件架构都有自己的内 存模型 每一个内存模型有一套一致性保障 某些内存模型 如x 和SPARC等 拥有强大的一致性保障 另一些内存模型 如Itanium PowerPC和Alpha 是一种弱保障
例如 x 和SPARC不会重新排序连续写 *** 作 也就没有必要放置内存屏障 Itanium PowerPC和Alpha将重新排序连续写 *** 作 因此JVM必须在两者之间放置内存屏障 JVM使用内存屏障减少Java内存模型和硬件内存模型之间的距离
隐式内存屏障
显式屏障指令不是序列化内存 *** 作的唯一方式 让我们再看一看Counter类这个例子
class Counter{
static int counter = ;
public static void main(String[] _){
for(int i = ; i < ; i++)
inc();
}
static synchronized void inc(){ counter += ; }
}
Counter类执行了一个典型的读 修改 写的 *** 作 静态counter字段不是volatile的 因为所有三个 *** 作必须要原子可见的 因此 inc 方法是synchronized修饰的 我们可以采用下面的命令编译Counter类并查看生成的汇编指令 Java内存模型确保了synchronized区域的退出和volatile内存 *** 作都是相同的可见性 因此我们应该预料到会有另一个内存屏障
$ java XX:+UnlockDiagnosticVMOptions XX:PrintAssemblyOptions=hsdis print bytes
XX: UseBiasedLocking XX:CompileCommand=print Counter inc Counter
x d eda : push %ebp ;
x d eda : mov %esp %ebp ; bec
x d edaa: sub $ x %esp ; ec
x d edad: mov $ x ba %esi ; be ba
x d edb : lea x (%esp) %edi ; d c
x d edb : mov %esi x (%edi) ;
x d edb : mov (%esi) %eax ; b
x d edbb: or $ x %eax ; c
x d edbe: mov %eax (%edi) ;
x d edc : lock cmpxchg %edi (%esi) ; f fb e
x d edc : je x d edda ; f
x d edca: sub %esp %eax ; bc
x d edcc: and $ xfffff %eax ; e f ffff
x d edd : mov %eax (%edi) ;
x d edd : jne x d ee ; f
x d edda: mov $ x ba b %eax ; b b ba
x d eddf: mov x (%eax) %esi ; bb
x d ede : inc %esi ;
x d ede : mov %esi x (%eax) ; b
x d edec: lea x (%esp) %eax ; d
x d edf : mov (%eax) %esi ; b
x d edf : test %esi %esi ; f
x d edf : je x d ee ; f d
x d edfa: mov x (%eax) %edi ; b
x d edfd: lock cmpxchg %esi (%edi) ; f fb
x d ee : jne x d ee f ; f
x d ee : mov %ebp %esp ; be
x d ee : pop %ebp ; d
不出意外 synchronized生成的指令数量比volatile多 第 行做了一次增 *** 作 但是JVM没有显式插入内存屏障 相反 JVM通过在 第 行和第 行cmpxchg的lock前缀一石二鸟 cmpxchg的语义超越了本文的范畴
lock cmpxchg不仅原子性执行写 *** 作 也会刷新等待的读写 *** 作 写 *** 作现在将在所有后续内存 *** 作之前完成 如果我们通过ncurrent atomic AtomicInteger 重构和运行Counter 将看到同样的手段
import ncurrent atomic AtomicInteger;
class Counter{
static AtomicInteger counter = new AtomicInteger( );
public static void main(String[] args){
for(int i = ; i < ; i++)
counter incrementAndGet();
}
}
$ java XX:+UnlockDiagnosticVMOptions XX:PrintAssemblyOptions=hsdis print bytes
XX:CompileCommand=print AtomicInteger incrementAndGet Counter
x f : push %ebp ;
x f : mov %esp %ebp ; bec
x fa: sub $ x %esp ; ec
x fd: jmp x a ; e
x : xchg %ax %ax ;
x : test %eax xb e ; e b
x a: mov x (%ecx) %eax ; b
x d: mov %eax %esi ; bf
x f: inc %esi ;
x : mov $ x a f d %edi ; bfd f a
x : mov x (%edi) %edi ; bbf
x b: mov %ecx %edi ; bf
x d: add $ x %edi ; c
x : lock cmpxchg %esi (%edi) ; f fb
x : mov $ x %eax ; b
x : je x ; f
x f: mov $ x %eax ; b
x : cmp $ x %eax ; f
x : je x ; cb
x : mov %esi %eax ; bc
x b: mov %ebp %esp ; be
x d: pop %ebp ; d
我们又一次在第 行看到了带有lock前缀的写 *** 作 这确保了变量的新值(写 *** 作)会在其他所有后续内存 *** 作之前完成
内存屏障能够避免
JVM非常擅于消除不必要的内存屏障 通常JVM很幸运 因为硬件内存模型的一致性保障强于或者等于Java内存模型 在这种情况下 JVM只是简单地插 入一个no op语句 而不是真实的内存屏障
例如 x 和SPARC内存模型的一致性保障足够强壮以消除读volatile变量时所需的内存屏障 还记得在 Itanium上两次读 *** 作之间的显式单向内存屏障吗?x 上的Dekker算法中连续volatile读 *** 作的汇编指令之间没有任何内存屏障 x 平台上共享内存的连续读 *** 作
x f : mov $ x %ebp ; bd
x f : mov $ x d %edx ; ba d
x f c: mov l x a f (%edx) %ebx ; fbe a da af
x f : test %ebx %ebx ; db
x f : jne x f ;
x f : movl $ x x a f (%ebp) ; c d a af
x f : movb $ x x a f (%edi) ; c d a af
x f : mfence ; faef
x f b: add $ x %esp ; c
x f e: pop %ebp ; d
x f f: test %eax xb ec ; c eb
x f : ret ; c
x f : nopw x (%eax %eax ) ; f f
x f : mov x a f (%ebp) %ebx ; b d d a af
x f : test %edi xb ec ; d c eb
第三行和第十四行存在volatile读 *** 作 而且都没有伴随内存屏障 也就是说 x 和SPARC上的volatile读 *** 作的性能下降对于代码的优 化影响很小 指令本身和常规读 *** 作一样
单向内存屏障本质上比双向屏障性能要好一些 JVM在确保单向屏障即可的情况下会避免使用双向屏障 本文的第一个例子展示了这点 Itanium平台上的 连续两次读 *** 作 入单向内存屏障 如果读 *** 作插入显式双向内存屏障 程序仍然正确 但是延迟比较长
动态编译
静态编译器在构建阶段决定的一切事情 在动态编译器那里都可以在运行时决定 甚至更多 更多信息意味着存在更多机会可以优化 例如 让我们看看JVM在单 处理器运行时如何对待内存屏障 以下指令流来自于通过Dekker算法实现两次连续volatile写 *** 作的运行时编译 程序运行于 x 硬件上的单处理器模式中的VMWare工作站镜像
x b c: push %ebp ;
x b d: sub $ x %esp ; ec
x b : mov $ x c %edi ; bf c
x b : movb $ x x f (%edi) ; c d aaf
x b f: mov $ x %ebp ; bd
x b : mov $ x d %edx ; ba d
x b : mov l x f (%edx) %ebx ; fbe a d aaf
x b : test %ebx %ebx ; db
x b : jne x b ; c
x b : movl $ x x f (%ebp) ; c d aaf
x b : add $ x %esp ; c
x b : pop %ebp ; d
在单处理器系统上 JVM为所有内存屏障插入了一个no op指令 因为内存 *** 作已经序列化了 每一个写 *** 作(第 行)后面都跟着一个屏障 JVM针对原子条件式做了类似的优化 下面的指令流来自于同一 个VMWare镜像的AtomicInteger incrementAndGet动态编译结果
x f : push %ebp ;
x f : mov %esp %ebp ; bec
x fa: sub $ x %esp ; ec
x fd: jmp x a ; e
x : xchg %ax %ax ;
x : test %eax xb b ; bb
x a: mov x (%ecx) %eax ; b
x d: mov %eax %esi ; bf
x f: inc %esi ;
x : mov $ x a f d %edi ; bfd f a
x : mov x (%edi) %edi ; bbf
x b: mov %ecx %edi ; bf
x d: add $ x %edi ; c
x : cmpxchg %esi (%edi) ; fb
x : mov $ x %eax ; b
x : je x ; f
x e: mov $ x %eax ; b
x : cmp $ x %eax ; f
x : je x ; cc
x : mov %esi %eax ; bc
x a: mov %ebp %esp ; be
x c: pop %ebp ; d
注意第 行的cmpxchg指令 之前我们看到编译器通过lock前缀把该指令提供给处理器 由于缺少SMP JVM决定避免这种成本 与静态编译有些不同
结束语
lishixinzhi/Article/program/Java/hx/201311/25723
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)