您的位置:首页 > 其它

《深入理解JVM虚拟机》总结

2018-01-25 13:21 399 查看
1月15日:java
面向对象和JVM基础
三大特性。
jdk包括java工具和jre,jre包括jvm和其他java包

1月16日
jvm:Java内存结构
java:对象初始化和面向对象特性
对象初始化的六个步骤:先判断是否存在该对象引用,否则先加载该类,加载完后准备创建对象,需要分配内存,或者单线程cas分配,或者线程内用TLAB申请内存。然后进行实例数据的初始化,考虑该实例的对象头。
jvm:Java内存结构
类加载的过程:第七章

1虚拟机栈,线程私有,譬如运行某个方法时,将开启一个线程,栈内有局部变量表。调用一个方法将生成一个栈帧,用于存放该方法的局部变量以及调用其他方法的入口,栈帧压栈以后开始执行方法代码,若调用其他方法将生成新的栈帧进行压栈操作。
2程序计数器,线程私有,将注明每个线程运行到了第几行代码。
3java堆,分配实例,数组,1.6后常量池也移到堆中。
4java方法区,存放类的元数据,静态数据,1.6以前常量池也在方法区。永久代也在此。
后改为元数据区。
5本地方法栈,与虚拟机栈类似,只是调用的是本地方法,线程私有。

Java内存溢出实例
(1).java堆溢出:
Java堆用于存储实例对象,只要不断创建对象,并且保证GC Roots到对象之间有引用的可达,避免垃圾收集器回收实例对象,就会在对象数量达到堆最大容量时产生OutOfMemoryError异常。
内存泄漏:某些内存长时间未被回收。
(2)由于Java虚拟机栈会出现StackOverflowError和OutOfMemoryError两种异常,所以分别使用两个例子演示这两种情况:
a.java虚拟机栈深度溢出:
单线程的环境下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法再分配的时候,虚拟机总抛出StackOverflowError异常。
b.java虚拟机栈内存溢出:
多线程环境下,能够创建的线程最大内存=物理内存-最大堆内存-最大方法区内存,在java虚拟机栈内存一定的情况下,单个线程占用的内存越大,所能创建的线程数目越小,所以在多线程条件下很容易产生java虚拟机栈内存溢出的异常。
(3).运行时常量池溢出:
运行时常量池属于方法区的一部分,可以使用-XX:PermSize=10m和-XX:MaxPermSize=10m将永久代最大内存和最小内存设置为10MB大小,并且由于永久代最大内存和最小内存大小相同,因此无法扩展。
(4).方法区溢出:
运行时常量池是方法区的一部分,他们都属于HotSpot虚拟机中的永久代内存区域。方法区用于存放Class的相关信息,Java的反射和动态代理可以动态产生Class,另外第三方的CGLIB可以直接操作字节码,也可以动态产生Class,实验通过CGLIB来演示,
(5).本机直接内存溢出:
Java虚拟机可以通过参数-XX:MaxDirectMemorySize设定本机直接内存可用大小,如果不指定,则默认与java堆内存大小相同。JDK中可以通过反射获取Unsafe类(Unsafe的getUnsafe()方法只有启动类加载器Bootstrap才能返回实例)直接操作本机直接内存。

java内存泄漏原因。
1、静态集合类引起内存泄露: 
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。 
2、当集合里面的对象属性被修改后,再调用remove()方法时不起作用。
3、监听器 
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器,我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象的时候却没有记住去删除这些监听器,从而增加了内存泄漏的机会。
4、各种连接 
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。
5、内部类和外部模块等的引用 
内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如: 
public void registerMsg(Object b); 
这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。
6、单例模式 
不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,考虑下面的例子: 

1月17日jvm:垃圾收集算法和垃圾收集器,分代收集。
(1).引用计数算法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不再被使用的,垃圾收集器将回收该对象使用的内存。
(2).根搜索算法:
通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。
在java语言中,可作为GC Root的对象包括以下几种对象:
a. java虚拟机栈(栈帧中的本地变量表)中的引用的对象。
b.方法区中的类静态属性引用的对象。
c.方法区中的常量引用的对象。
d.本地方法栈中JNI本地方法的引用对象。
java方法区在Sun HotSpot虚拟机中被称为永久代,很多人认为该部分的内存是不用回收的,java虚拟机规范也没有对该部分内存的垃圾收集做规定,但是方法区中的废弃常量和无用的类还是需要回收以保证永久代不会发生内存溢出。
判断废弃常量的方法:如果常量池中的某个常量没有被任何引用所引用,则该常量是废弃常量。
判断无用的类:
(1).该类的所有实例都已经被回收,即java堆中不存在该类的实例对象。
(2).加载该类的类加载器已经被回收。
(3).该类所对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射机制访问该类的方法。

Java中常用的垃圾收集算法:
(1).标记-清除算法:
(2).复制算法:
(3).标记-整理算法:
(4).分代收集算法:
根据内存中对象的存活周期不同,将内存划分为几块,java的虚拟机中一般把内存划分为新生代和年老代,当新创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后仍然存活的对象会被移动到年老代内存中,当大对象在新生代中无法找到足够的连续内存时也直接在年老代中创建。
现在的Java虚拟机就联合使用了分代复制、标记-清除和标记-整理算法,java虚拟机垃圾收集器关注的内存结构如下:
年轻代停止复制,老年代标记清除,效率高。
eden survior(from to两部分)old 永久代

seria new年轻代单线程 停止复制
parallel new年轻代多线程 停止复制
(3).Parallel Scavenge收集器:
Parallel Scavenge收集器也是一个新生代垃圾收集器,同样使用复制算法
可控制吞吐量。用户线程耗时占总时间比例较高
(4).Serial Old收集器:
Serial Old是Serial垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法
(5).Parallel Old收集器:
Parallel Old收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在JDK1.6才开始提供。
(6).CMS收集器:并发执行,低停顿,但是吞吐量不高
Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。
最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验,CMS收集器是Sun HotSpot虚拟机中第一款真正意义上并发垃圾收集器,它第一次实现了让垃圾收集线程和用户线程同时工作。

(7).G1收集器:
Garbage first垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与CMS收集器,G1收集器两个最突出的改进是:
a.基于标记-整理算法,不产生内存碎片。
b.可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

1月18日jvm:java自带分析工具
visualVM.
Jps 线程
jmap 内存dump文件
jstat 前端展示
jstack 堆栈信息
jhat可分析dump文件等。

1月19日:java class的结构和信息。
8字节为基本单位。
16进制的两位就是一个字节。
魔术cafebabe一共八位占四个字节。
字节码class类文件是由一系列字节码命令组成,用于表示程序中各种常量、变量、关键字和运算符号的语义等等。
根据偏移和字节码来表示key和value;
所有字节码按顺序排列,不同的区域可能有不同的表示方式。比如有时候1个字节代表一个值,有时候2个字节代表一个值。有时候可能是更复杂的结构。
Java虚拟机规定,Class类文件格式采用类似C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表:
(1).无符号数:
属于基本类型的数据,以u1, u2, u4, u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码的字符串值。
(2).表:
由多个无符号数或其他表作为数据项构成的复合数据类型,所以表都习惯性地以“_info“结尾。表用于描述有层次关系的复合结构数据,整个Class文件本质就是一张表。
包含:
(1).magic:
(2).minor_version和major_version:
(3). constant_pool_count和constant_pool:
constant_pool_count代表Class文件中常量池的数目
constant_pool常量池是Class类文件中出现的第一个表类型数据,常量池主要存放两大类常量:
(4). access_flags:
用于表示Class或接口层次的访问标志,即类或接口层面的访问控制信息,通常存储的信息包括:Class类文件是类、接口、枚举或是注解;是否定义为public类型;是否定义为abstract类型;类是否被定义为final等等。
(5). this_class、super_class和interfaces:
this_class类索引用于确定类的全限定名
(6). field:
field_info字段表用于描述接口或者类中声明的变量,field字段包括了类级变量(静态变量)和实例级变量(成员变量),但不包括方法内部的局部变量。
(7).method:
method_info方法表用于描述类或者接口中声明的方法,methods_count用于表示Class文件中方法总数,method方法存储了方法的访问标识、是否静态、是否final、是否同步synchronized、是否本地方法native、是否抽象方法abstract、方法返回值类型、方法名称、方法参数列表等信息。
方法的代码指令并没有直接存放在方法表中,而是存放着属性表中的方法表Code中。
(8). attribute:
attribute_info属性表是Class文件格式中最具扩展性的一种数据项目,用于存放field_info字段表、method_info方法表以及Class文件的专有信息,属性表不要求各个属性有严格顺序,只要求不与已有的属性名字重复即可,属性表中存放的常用信息如下:

1月20日
类加载机制。
Java虚拟机类加载过程是把Class类文件加载到内存,并对Class文件中的数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程。
在加载阶段,java虚拟机需要完成以下3件事:
a.通过一个类的全限定名来获取定义此类的二进制字节流。
b.将定义类的二进制字节流所代表的静态存储结构转换为方法区的运行时数据结构。
c.在java堆中生成一个代表该类的java.lang.Class对象,作为方法区数据的访问入口。
类加载过程:
加载,验证,准备,解析,初始化。
(1).BootStrap ClassLoader:启动类加载器,负责加载存放在%JAVA_HOME%\lib目录中的,或者通被-Xbootclasspath参数所指定的路径中的,并且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即使放在指定路径中也不会被加载)类库到虚拟机的内存中,启动类加载器无法被java程序直接引用。
(2).Extension ClassLoader:扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
(3).Application ClassLoader:应用程序类加载器,由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径classpath上所指定的类库,是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者可以直接使用应用程序类加载器,如果程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器。
双亲委派模型
从JDK1.2开始,java虚拟机规范推荐开发者使用双亲委派模式(ParentsDelegation Model)进行类加载,其加载过程如下:
(1).如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器去完成。
(2).每一层的类加载器都把类加载请求委派给父类加载器,直到所有的类加载请求都应该传递给顶层的启动类加载器。
(3).如果顶层的启动类加载器无法完成加载请求,子类加载器尝试去加载,如果连最初发起类加载请求的类加载器也无法完成加载请求时,将会抛出ClassNotFoundException,而不再调用其子类加载器去进行类加载。

类加载器双亲委派模型是从JDK1.2以后引入的,并且只是一种推荐的模型,不是强制要求的,因此有一些没有遵循双亲委派模型的特例:
(1).在JDK1.2之前,自定义类加载器都要覆盖loadClass方法去实现加载类的功能,JDK1.2引入双亲委派模型之后,loadClass方法用于委派父类加载器进行类加载,只有父类加载器无法完成类加载请求时才调用自己的findClass方法进行类加载,因此在JDK1.2之前的类加载的loadClass方法没有遵循双亲委派模型,因此在JDK1.2之后,自定义类加载器不推荐覆盖loadClass方法,而只需要覆盖findClass方法即可。
(2).双亲委派模式很好地解决了各个类加载器的基础类统一问题,越基础的类由越上层的类加载器进行加载,但是这个基础类统一有一个不足,当基础类想要调用回下层的用户代码时无法委派子类加载器进行类加载。为了解决这个问题JDK引入了ThreadContext线程上下文,通过线程上下文的setContextClassLoader方法可以设置线程上下文类加载器。
JavaEE只是一个规范,sun公司只给出了接口规范,具体的实现由各个厂商进行实现,因此JNDI,JDBC,JAXB等这些第三方的实现库就可以被JDK的类库所调用。线程上下文类加载器也没有遵循双亲委派模型。
(3).近年来的热码替换,模块热部署等应用要求不用重启java虚拟机就可以实现代码模块的即插即用,催生了OSGi技术,在OSGi中类加载器体系被发展为网状结构。OSGi也没有完全遵循双亲委派模型。

Tomcat类加载实例
Tomcat 等主流Web服务器为了实现下面的基本功能,都实现了不止一个自定义的类加载器:
(1).部署在同一个服务器上的两个web应用程序所使用的java类库可以相互隔离。
(2).部署在同一个服务器上的两个web应用程序所使用的java类库可以相互共享。
(3).许多Web服务器本身使用java语言实现,因此服务器所使用的类库应与应用程序的类库相互独立。
(4).支持JSP应用的Web服务器,需要支持HotSwap功能,因为JSP文件最终是被编译为java的servlet来运行的,当修改JSP文件时,不需要重启服务器就可以实现热部署。
Tomcat作为JDK推荐的双亲委派模式正统类加载器实现的代表,Tomcat5和Tomcat6类加载体系结构有较大区别:
Tomcat5:
Tomcat5中可以存放java类库以及Web应用的目录如下:
(1)./common目录:类库可以被Tomcat服务器本身和所有的Web应用程序共同使用。
(2)./server目录:类库可以被Tomcat服务器本身使用,对应用程序不可见。
(3)./shared目录:类库可以被所有的应用程序使用,对Tomcat服务器本身不可见。
(4)./WebApp/WEB-INF目录:类库仅可以被应用程序使用,对其他的应用程序和Tomcat服务器不可见。
Tomcat5的类加载体系结构如下:

Tomcat6:
Tomcat默认把/common、/server和/shared三个目录合并成一个/lib目录,因此Tomcat6默认可以存放java类库以及Web应用的目录如下:
(1)./lib目录:类库可以被Tomcat服务器本身和所有的Web应用程序共同使用。
(2)./WebApp/WEB-INF目录:类库仅可以被应用程序使用,对其他的应用程序和Tomcat服务器不可见。
Tomcat6的默认类加载体系结构如下:

在Tomcat6中,可以通过指定<Tomcat安装目录>/conf/catalina.properties属性文件中的server.loader和share.loader建立和Tomcat5类似的ServerClassLoader和SharedClassLoader。

1月21日
java虚拟机字节码执行引擎。
1物理机的执行引擎基于架构,寄存器,指令集
2虚拟机基于自身的执行引擎
jvm执行的输入时字节码,可以理解为是解释执行。javac将程序编译为.class的字节码文件,虚拟机使用栈式执行引擎执行字节码。
3运行时栈帧结构包括:1局部变量表,2操作数栈,3动态链接(用于将字节码里的符号引用转换为对内存中的直接引用),4方法返回地址,5附加信息。
4方法调用
5动态语言支持,java属于静态语言,编译时就检查类型信息等,方便查错。但是代码比较臃肿,python等动态语言则是解释执行,执行时才检查类型,代码简单,效率不高。
jvm虚拟机支持使用methodhandler来支持动态语言,支持动态类型。
methodhandler和反射的区别是:
1反射在模拟Java层次使用,methodhandler在模拟字节码层次使用。
2反射重量级,后者轻量级。
3反射只用于java,后者支持jvm的所有语言,且在字节码上进行优化。
最后
jvm是基于栈的字节码指令执行引擎,速度没有基于寄存器的x86指令集执行引擎快。但是适合跨平台。

1月22日:
虚拟机编译期优化,
前期优化:java代码编译成class字节码阶段的优化有:
1注解优化
2语法糖优化:
自动装箱拆箱,本质是调用包装类的IntegerOf
泛型,编译期进行泛型擦除,变成object类型,并且使用强制转型。是伪泛型
foreach循环:实际是使用iterator迭代器进行迭代。
条件编译:如if语句中永远不成立的语句是不会被编译执行的。

后期优化:class字节码编译为本地平台支持的机器码阶段的优化。
又称JIT编译器。
1热点代码探测,jvm多次解释过的代码超过阈值后称为热点代码,需要用JIT编译成机器码。
2HotSpot JIT是主要的编译优化位置,
虚拟机团队使用了很多优化技术:
1公共子表达式消除,如计算a*b+c和a*b+d因为a*b已经算过,所以被视为表达式。
2数组范围检查消除,根据数据流提前判断数组下标是否越界
3内联方法,把内联方法的代码直接复制到调用处,而不是进行方法调用。
4逃逸分析:如果一个对象只会在一个线程存在,可以对其进行优化:

1栈上分配该对象,而不在堆上分配,该对象会跟随栈帧被清除。

2同步消除,如果不逃逸出线程,则不需要对该变量进行同步措施

3标量替换,如果不逃逸,且对象可拆为多个变量,则可以进行拆分,只保留基础类型,不产生对象。

java和c++编译期比较,java提高开发效率但是运行效率不如c++

1月23日

java内存模型与线程
随着多核CPU的高速发展,为了充分利用硬件的计算资源,操作系统的并发多任务功能正变得越来越重要,但是CPU在进行计算时,还需要从内存读取输出,并将计算结果存放到内存中,然而由于CPU的运算速度比内存高几个数量级,CPU内的寄存器数量和容量有限,为了不让CPU长时间处于等待内存的空闲状态,在CPU和内存之间引入了速度接近CPU的高速缓存Cache作为CPU和内存之间的缓冲。计算机硬件并发的原理如下:

Java虚拟机对并发的支持类似于计算机硬件,java虚拟机的并发支持是通过java虚拟机的内存模型来实现的。Java虚拟机的内存模型分为主内存和工作内存,程序中所有的变量都存储在主内存中,每个线程有自己的私有工作内存,工作内存中保存了被该线程使用到的变量的主内存拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量,不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。Java虚拟机并发原理如下:

一、Java虚拟机内存模型中定义了8种关于主内存和工作内存的交互协议操作:
(1).lock锁定:作用于主内存的变量,把一个变量标识为一条线程独占状态。
(2).unlock解锁:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量可以被其他线程锁定。
(3).read读取:作用于主内的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
(4).load加载:作用于工作内存的变量,把read读取操作从主内存中得到的变量值放入工作内存的变量拷贝中。
(5).use使用:作用于工作内存的变量,把工作内存中一个变量的值传递给java虚拟机执行引擎,每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行该操作。
(6).assign赋值:作用于工作内存变量,把一个从执行引擎接收到的变量的值赋值给工作变量,每当虚拟机遇到一个给变量赋值的字节码时将会执行该操作。
(7).store存储:作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
(8).write写入:作用于主内存的变量,把store操作从工作内存中得到的变量值放入主内存的变量中。
Java内存模型对上述8种操作有如下的约束:
(1).把一个变量从主内存复制到工作内存中必须顺序执行read读入操作和load载入操作。
把一个变量从工作内存同步回主内存中必须顺序执行store存储操作和write写入操作。
read和load操作之间、store和write操作之间可以插入其他指令,但是read和load操作、store和write操作必须要按顺序执行,即不允许read和load、store和write操作之一单独出现。
(2).不允许一个线程丢弃它的最近的assign赋值操作,即工作内存变量值改变之后必须同步回主内存。只有发生过assign赋值操作的变量才需要从工作内存同步回主内存。
(3).一个新变量只能在主内存中产生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,即一个变量在进行use和store操作之前,必须先执行过assgin和load操作。
(4).一个变量在同一时刻只允许一条线程对其进行lock锁定操作,但是lock锁定可以被一条线程重复执行多次,多次执行lock之后,只有执行相同次数的unlock操作变量才会被解锁。
(5).如果对一个变量执行lock锁定操作,将会清空工作内存中该变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
(6).如果一个变量事先没有被lock锁定,则不允许对这个变量进行unlock解锁操作,也不允许对一个被别的线程锁定的变量进行unlock解锁。
(7).一个变量进行unlock解锁操作之前,必须先把此变量同步回主内存中(执行store和write操作)。
上述这些规定保证了内存协议的使用。

二、Java中的关键字volatile是java虚拟机提供的最轻量级的线程同步机制,当一个变量被声明为volatile之后,该变量将具备以下两种特性:

(1).volatile保证变量对所有线程的可见性,即任何一个线程修改了该变量的值之后,新值对于所有其他线程都是可以立即得知的。(这是因为volatile的底层实现使该变量在主内存中保持最新,并且当线程读取该变量时需要立即从主内存中刷新获取最新值,因此保证了其在线程间的可见性,但是到字节码级别的程序,它可能就不是最新的值了。)
而普通变量需要先将工作内存中的变量同步回主内存,其他线程都需要从主内存重新读取变量的值才能使用最新修改后的值。
volatile变量也可以在各个工作内存中存在不一致的情况,但由于每次使用之前都需要先刷新(工作内存变量重新执行初始化),执行引擎看不到变量不一致的情况,因此可以任务volatile变量不存在不一致的情况。
但是java中的运算并非全部都是原子操作,因此volatile变量的运行在并发下一样是线程不安全的。
由于volatile变量只能保证可见性,只有在符合如下两条规则情况才是线程安全的。
a.运算结果不依赖变量的当前值,或者能够确保只有单一线程修改变量的值。
1比如多线程实现的计数器,increase方法的字节码会拆分成四行代码,第一行代码将volatile变量进行刷新并压栈,此时的值确实是最新的,但是在执行接下来的字节码时其他线程可能已经改变了该变量,于是接下来的操作就是针对过期数据的操作,写回内存的值也是不正确的。所以a规定下,计数器的运算结果依赖当前值,不能保证线程安全

2再如get和set方法,一个线程get一个线程set,可以对两个方法加synchronized或者对变量加volatile关键字来保证可见,因为该变量不依赖当前值。
b.变量不需要与其他其他变量共同参与不变约束。
不符合上述两条规则情况下,仍然需要通过synchronized同步关键字或者加锁机制来保证线程安全。
(2).volatile禁止指令重排序优化。
指令重排序是java根据as-if-serial语义对机器码顺序进行优化,并且不会改变单线程中的运行结果。
但是在多线程中指令重排序会造成共享变量的读写子问题。
原理:指令重排序优化是指机器码级别的优化,即机器码可能会更改变量赋值顺序,从而打破内存协议中对volatile的规定,所以volatile变量会在机器码所在的位置加入内存屏障,阻止屏障前面的代码移动到后面,这样就可以保证volatile内存操作的有序性,保证了其可见性。
普通变量仅能保证在方法执行过程中所有依赖赋值结果的地方都能获取正确的结果,而无法保证变量赋值操作顺序与程序代码执行顺序一致。
volatile禁止指令重排序,因此volatile变量的约束如下:
a.volatile变量的操作必须按read->load->use顺序,即每次在工作内存中使用变量前必须先从主内存中刷新最新的值,以保证能看到其他线程对变量的最新修改。
b. volatile变量的操作必须按assign->store->write顺序,即每次在工作内存为变量赋值之后必须将变量的值同步回主内存,以保证让其他线程能看到变量的最新修改。
c.若线程对volatile变量A的assign或者use操作先于对volatile变量B的assign或者use操作,则线程对volatile变量A的read/load或者store/write操作也必定先于对volatile变量B的read/load或者store/write操作。

三、Java的并发编程是依赖虚拟机内存模型的三个特性实现的:
(1).原子性(Atomicity):
原子性是指不可再分的最小操作指令,即单条机器指令,原子性操作任意时刻只能有一个线程,因此是线程安全的。(单条机器指令只有一个线程在操作,不会有其他线程的并发,所以不会受到其他线程操作的影响)
Java内存模型中通过read、load、assign、use、store和write这6个操作保证变量的原子性操作。
long和double这两个64位长度的数据类型java虚拟机并没有强制规定他们的read、load、store和write操作的原子性,即所谓的非原子性协定,但是目前的各种商业java虚拟机都把long和double数据类型的4中非原子性协定操作实现为原子性。所以java中基本数据类型的访问读写是原子性操作。
对于大范围的原子性保证需要通过lock和unlock操作以及synchronized同步块来保证。
(2).可见性(Visibility):
可见性是指当一个线程修改了共享变量的值,其他线程可以立即得知这个修改。
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
Java中通过volatile、final和synchronized这三个关键字保证可见性:
volatile:通过刷新变量值确保可见性。
synchronized:同步块通过变量lock锁定前必须清空工作内存中变量值,重新从主内存中读取变量值,unlock解锁前必须把变量值同步回主内存来确保可见性。
final:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this引用传递进去,那么在其他线程中就能看见final字段的值,无需同步就可以被其他线程正确访问。
(3).有序性(Ordering):
线程的有序性是指:在线程内部,所有的操作都是有序执行的,而在线程之间,因为工作内存和主内存同步的延迟,操作是乱序执行的。
Java通过volatile和synchronized关键字确保线程之间操作的有序性。
volatile禁止指令重排序优化实现有序性。
synchronized通过一个变量在同一时刻只允许一个线程对其进行lock锁定操作来确保有序性。

我们知道并发问题的一个原因是有序性,而java中volatile和synchronized可以保证有序性; 
但是在java中,并不是所有的操作都是由volatile和synchronized实现的,java中存在”先行发生“的原则。 
“先行发生”原则是判断数据是否存在竞争、线程是否安全的主要依据;

四、java内存模型中的”天然“先行关系

1、程序次序规则 

一个线程内,按照程序代码的顺序,书写在前面的操作先行发生于(逻辑上)书写在后面的操作。 

2、管程锁定规则 

一个unlock操作先行发生于后面对同一个锁的lock操作。后面指时间上的先后顺序。 

3、volatile变量规则 

对一个volatile变量的写操作先行发生于后面对这个变量的读操作。这里的后面指时间上的先后顺序。 

4、传递性 

如果操作A先行发生于操作B,操作B先行发生于操作C,那么,操作A也就先行发生于操作C。 

5、线程启动规则 

Thread对象的start方法先行发生于此线程的每个动作; 

6、线程终止规则 

线程中的所有操作都先行发生于对此线程的终止检测; 

7、线程中断规则 

对线程的interrupt()方法的调用先行发生于被中断线程的代码检测到中断时间的发生; 

8、对象终结规则 

一个对象的初始化完成先行发生于它的finalize方法的开始; 

。。。。。

java无须任何手段即可保证上面的先行发生规则成立;
保证了某个先行发生规则就可以保证有序性,消除并发问题。

五、JDK线程实现:
JDK线程的实现如下:

(1).Kernal thread:KLT,内核线程,运行在内核态,是直接有操作系统内核支持的线程,有操作系统内核完成内核线程切换,内核操作线程调度器Threadscheduler对内核线程进行调度,负责将内核线程任务映射到各个处理器上。
(2).Light weight process: LWP,轻量级用户进程,是编程中传统意义上的线程,每个轻量级进程都由一个内核线程支持。
(3).User thread:UT,用户线程,运行在用户态,完全由用户空间线程库实现,内核线程无法感知到用户线程的实现,用户线程的创建、同步、调度和销毁完全在用户态中完成,不需要内核态的支持。
JDK的线程是基于操作系统原生线程模型来实现的,因此JDK版本中线程模型取决于java虚拟机线程与操作系统线程的映射,在不同平台上是不同的。
线程调度有两种方式:
(1).协同式:线程的执行时间由线程本身来控制,线程任务执行完成之后主动通知系统切换到另一个线程去执行。
优点:实现简单,线程切换操作对线程本身是可知的,不存在线程同步问题。
缺点:线程执行时间不可控制,如果线程长时间执行不让出CPU执行时间可能导致系统崩溃。
(2).抢占式:每个线程的执行时间有操作系统来分配,操作系统给每个线程分配执行的时间片,抢到时间片的线程执行,时间片用完之后重新抢占执行时间,线程的切换不由线程本身来决定。
优点:线程执行时间可控制,不会因为一个线程阻塞问题导致系统崩溃。
 
当前JDK的多线程是抢占式的多线程系统,但是可以通过设置线程优先级和改变线程的执行时间分配概率。
注意:由于JDK的线程优先级和操作系统的线程优先级不是一一对应的,因此建议只使用1(最低优先级)、5(正常优先级)和10(最高优先级)这三个优先级。
另外,线程优先级只是操作系统给线程分配执行时间的概率大小,不是绝对的。
Java中线程的状态即调度关系如下:

Java线程安全与锁优化
线程安全
当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的
Java语言中的线程安全
Java语言中各种操作共享的数据分为以下5类:不可变、 绝对线程安全、 相对线程安全、 线程兼容和线程对立。
在Java API中符合不可变要求的类型,除了String之外,常用的还有枚举类型,以及java.lang.Number的部分子类,如Long和Double等数值包装类型,BigInteger和BigDecimal等大数据类型;但同为Number的子类型的原子类AtomicInteger和AtomicLong则并非不可变的(内部变量使用volatile)
1、互斥同步
同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用
而互斥是实现同步的一种手段,临界区(Critical Section)、 互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。 因此,在这4个字里面,互斥是因,同步是果;互斥是方法,同步是目的
在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。
如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。

根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。 如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorexit指令时会将锁计数器减1,当计数器为0时,锁就被释放。 如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
(每个对象有个锁计数器,存有线程id,对应id进行计数,获取锁加一,释放锁减一)
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。
synchronized是Java语言中一个重量级(Heavyweight)的操作
之所以是重量级操作,是因为线程的阻塞和唤醒都需要切换到内核态对对应的内核线程进行操作,其切换上下文的性能消耗很大。java中的线程与内核线程一一对应,除非是在Solaris操作系统。
可重入锁ReentrantLock的api表现为lock和unlock
除了synchronized之外,我们还可以使用java.util.concurrent(下文称J.U.C)包中的重入锁(ReentrantLock)来实现同步.
ReentrantLock表现为API层面的互斥锁(lock()和unlock()方法配合try/finally语句块来完成),synchronized表现为原生语法层面的互斥锁
不过,相比synchronized,ReentrantLock增加了一些高级功能,主要有以下3项:等待可中断、 可实现公平锁,以及锁可以绑定多个条件

等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;

锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象
2、非阻塞同步

先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。
硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成

测试并设置(Test-and-Set)。

获取并增加(Fetch-and-Increment)。

交换(Swap)。

比较并交换(Compare-and-Swap,下文称CAS)。

CAS指令需要有3个操作数,分别是内存位置(在Java中可以简单理解为变量的内存地址,用V表示)、 旧的预期值(用A表示)和新值(用B表示)。 CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新,但是无论是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作

CAS操作可以保证原子性,因为其操作是一条机器指令,只会在一个线程中执行。

虽然可能出现aba问题(在读取值之前该值已经被修改过并且修改回原值),但是基本上可以实现原子操作。
3.无同步方案
可重入代码(Reentrant Code):
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、 用到的状态量都由参数中传入、 不调用非可重入的方法等。
线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

Java可以可以通过java.lang.ThreadLocal类来实现线程本地存储的功能

由于互斥锁非常重量级,需要进行锁优化。
jvm默认不使用锁优化,需要自行加命令进行锁优化

锁优化
自旋锁与自适应自旋
挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。

自旋锁: 为了让线程等待(保持在用户态),我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁

自适应自旋:如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。
锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
锁粗化
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部
轻量级锁

此处使用cas操作是避免在修改对象头markword的时候被其他线程影响。

加锁:

“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁

它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗

在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock
Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word)

然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后2bit)将转变为“00”,即表示此对象处于轻量级锁定状态

如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。

如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标志的状态值变为“10”,Mark
Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

解锁:

如果对象的Mark Word仍然指向着线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了。
如果替换失败,说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程

偏向锁

它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。
如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连CAS操作都不做了。

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作

当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。 根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行

如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。

偏向锁只在单线程请求加锁时有用,当其他线程请求加锁时,就会失效。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: