学习目标:面试记录
- 高并发
- 单继承和多实现
- jvm类加载
学习内容:
- 高并发
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。
可见性问题产生的原因
在很多年前,那个嫁妆只需要一个手电筒的年代你或许还不会出现可见性这样的问题,因为大家都是单核处理器,不存在并发的情况。
而对于现在“视金钱如粪土”的年代。多核处理器已经是现代超级计算机的基础硬件。高速的CPU处理器和缓慢的内存之前数据的通信成了矛盾。
所以为了解决和缓和这样的情况,每个CPU和线程都有自己的本地缓存,所谓本地缓存即该缓存仅仅对它所在的处理器可见,CPU缓存与内存的数据不容易保证一致。
为了避免这种因为写数据速度不一致而导致 CPU 的性能浪费的情况,处理器通过使用写缓冲区来临时保存待写入主内存的数据。写缓冲区合并对同一内存地址的多次写,并以批处理的方式刷新,也就是说写缓冲区不会立即将数据刷新到主内存中。
缓存不能及时刷新到主内存就是导致可见性问题产生的根本原因。
有序性:程序执行的顺序按照代码的先后顺序执行。
这有啥的,程序老老实实按照程序员写的代码执行就完事了,这还会有什么问题吗?
有序性问题产生的原因
实际上编译器为了提高程序执行的性能。会改变我们代码的执行顺序的。即你写在前面的代码不一定是先被执行完的。
例如:int a = 1;int b =4;从表面和常规角度来看,程序的执行应该是先初始化 a ,然后初始化 b 。但是实际上非常有可能是先初始化 b,然后初始化 a。因为在编译器看了来,先初始化谁对这两个变量不会有任何影响。即这两个变量之间没有任何的数据依赖。
指令重排序有三种类型,分别为:
① 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
② 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。
③ 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上 去可能是在乱序执行。
原子性:
先来看下什么叫原子性
第一种理解:原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意 为“不可被中断的一个或一系列操作”
第二种理解:原子性,即一个操作或多个操作,要么全部执行并且在执行的过程中不被打断,要么全部不执行。(提供了互斥访问,在同一时刻只有一个线程进行访问)
原子,在物理学中定义是组成物体的不可分割的最小的单位。在 java 并发编程中我们可以将其理解为:一组要么成功要么失败的操作。
原子性问题的产生的原因
原子性问题产生的根本原因是什么?我们只要知道了症状才能准确的对症下药,本小节,我们就来一起探讨下原子性问题的由来。
我们都知道,程序在执行的时候,一定是以线程为单位在执行的,因为线程是 CPU 进行任务调度的基本单位。
电脑的 CPU 会根据不同的任务调度算法去执行线程的调度,将时间分片并派分给各个线程。
当某个线程获得CPU的时间片之后就获取了CPU的执行权,就可以执行任务,当时间片耗尽之后,就会失去CPU使用权。
进而本任务会暂时的停止执行。多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。
看完理论似乎并不能直观的理解原子性问题。下面我们就通过代码的方式来具体阐述下原子性问题的产生原因。 - java为什么不支持多继承,支持多实现。
单继承:
①若子类继承的父类中拥有相同的成员变量,子类在引用该变量时将无法判别使用哪个父类的成员变量。
②若一个子类继承的多个父类拥有相同方法,同时子类并未重写该方法(若重写,则直接使用子类中重写的方法),那么调用该方法时,将无法确定调用哪个父类的方法。
多实现:
可以多实现是因为,接口中的方法没有具体实现。实现多个接口时,就算两个接口中有相同的方法,但也不会出现矛盾。 - jvm类加载
一、类加载过程
1.加载
加载指的是将类的class文件读入到内存,并为之创建一个java.lang.Class对象,也就是说,当程序中使用任何类时,系统都会为之建立一个java.lang.Class对象。
类的加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此之外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据,通常有如下几种来源。
从本地文件系统加载class文件,这是前面绝大部分示例程序的类加载方式。
从JAR包加载class文件,这种方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
通过网络加载class文件。
把一个Java源文件动态编译,并执行加载。
类加载器通常无须等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。
2.链接
当类被加载之后,系统为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中。类连接又可分为如下3个阶段。
1)验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。Java是相对C++语言是安全的语言,例如它有C++不具有的数组越界的检查。这本身就是对自身安全的一种保护。验证阶段是Java非常重要的一个阶段,它会直接的保证应用是否会被恶意入侵的一道重要的防线,越是严谨的验证机制越安全。验证的目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。其主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
四种验证做进一步说明:
文件格式验证:主要验证字节流是否符合Class文件格式规范,并且能被当前的虚拟机加载处理。例如:主,次版本号是否在当前虚拟机处理的范围之内。常量池中是否有不被支持的常量类型。指向常量的中的索引值是否存在不存在的常量或不符合类型的常量。
元数据验证:对字节码描述的信息进行语义的分析,分析是否符合java的语言语法的规范。
字节码验证:最重要的验证环节,分析数据流和控制,确定语义是合法的,符合逻辑的。主要的针对元数据验证后对方法体的验证。保证类方法在运行时不会有危害出现。
符号引用验证:主要是针对符号引用转换为直接引用的时候,是会延伸到第三解析阶段,主要去确定访问类型等涉及到引用的情况,主要是要保证引用一定会被访问到,不会出现类等无法访问的问题。
2)准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。
3)解析:将类的二进制数据中的符号引用替换成直接引用。说明一下:符号引用:符号引用是以一组符号来描述所引用的目标,符号可以是任何的字面形式的字面量,只要不会出现冲突能够定位到就行。布局和内存无关。直接引用:是指向目标的指针,偏移量或者能够直接定位的句柄。该引用是和内存中的布局有关的,并且一定加载进来的。
3.初始化
初始化是为类的静态变量赋予正确的初始值,准备阶段和初始化阶段看似有点矛盾,其实是不矛盾的,如果类中有语句:private static int a = 10,它的执行过程是这样的,首先字节码文件被加载到内存后,先进行链接的验证这一步骤,验证通过后准备阶段,给a分配内存,因为变量a是static的,所以此时a等于int类型的默认初始值0,即a=0,然后到解析(后面在说),到初始化这一步骤时,才把a的真正的值10赋给a,此时a=10。
二、类加载机制:
1.JVM的类加载机制主要有如下3种。
全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。