Java基础之问

基础类型

1、在Java当中有哪些基础类型?

byte:8位,最大存储数据量是255,存放的数据范围是-128~127之间。

short:16位,最大数据存储量是65536,数据范围是-32768~32767之间。

int:32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。

long:64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。

float:32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。

double:64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。

boolean:只有true和false两个取值。

char:16位,存储Unicode码,用单引号赋值。

2、对于boolean型变量,占用几个字节?
如果是一个基本的boolean型变量,如

 boolean a = true;

那么占用4个字节,如果是数组型,那么数组型每个元素占用1个字节。
原因是因为在JVM中普通boolean型变量的指令是int的指令,而boolean数组的指令是byte的指令。
3、什么是自动装箱?自动拆箱呢?
自动装箱指的是,当基本类型赋值给包装类型引用时,会触发自动装箱操作,调用包装类型的valueOf()方法。
当包装类型参与运算时会触发自动拆箱操作,调用包装类型对应的*** Value()方法,例如Integer类为intValue()。Why?因为运算是基本数据类型要做的事情。
4、包装类的缓存范围?

包装类范围
Integer-128~127
Booleanture、false
Short-128~127
Long-128~127
Char\u0000 ~ \u007F

5、为什么String是不可变的?

  1. 缓存的需要
    String是不可变的。因为String会被String pool缓存。因为缓存String字面量要在多个线程之间共享,一个客户端的行为会影响其他所有的客户端,所以会产生风险。如果其中一个客户端修改了内容"Test"为“TEST”, 其他客户端也会得到这个结果,但显然并想要这个结果。因为缓存字符串对性能来说至关重要,因此为了移除这种风险,String被设计成Immutable。
  2. HashMap的需要
    HashMap在Java里太重要了,而它的key通常是String类型的。如果String是mutable,那么修改属性后,其hashcode也将改变。这样导致在HashMap中找不到原来的value。
  3. 多线程需要
    如果String是可变的,即修改String的内容后,地址不变。那么当多个线程同时修改的时候,string的length是不确定的,造成不安全因素,无法得到正确的截取结果。而为了保证顺序正确,需要加synchronzied,但这会得到难以想象的性能问题。
  4. classloader中需要
    String会在加载class的时候需要,如果String可变,那么可能会修改加载中的类。
    总之,安全性和String字符串常量池缓存是String被设计成不可变的主要原因。
    6、Java中有哪些拼接字符串的方式

面向对象篇

1、请简述面向对象的六原则一法则
(1)单一职责原则:一个类只做它该做的事情。其核心就是我们常说的"高内聚",写代码最终极的原则只有六个字"高内聚、低耦合",一个对象如果承担太多的职责,那么注定它什么都做不好。
(2)开闭原则:软件实体应当对扩展开放,对修改关闭。(在理想的状态下,当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类就可以,不需要修改原来的任何一行代码。要做到开闭有两个要点:
①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;
②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱。)
(3)依赖倒转原则:面向接口编程。(该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代,请参考下面的里氏替换原则。)
(4)里氏替换原则:任何时候都可以用子类型替换掉父类型。简单的说就是能用父类型的地方就一定能使用子类型。里氏替换原则可以检查继承关系是否合理,如果一个继承关系违背了里氏替换原则,那么这个继承关系一定是错误的,需要对代码进行重构。
需要注意的是:子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。
(5)接口隔离原则:接口要小而专,绝不能大而全。
(6)聚合复用原则:优先使用聚合关系复用代码。(通过继承来复用代码是面向对象程序设计中被滥用得最多的东西,因为所有的教科书都无一例外的对继承进行了鼓吹从而误导了初学者,类与类之间简单的说有三种关系,Is-A关系、Has-A关系、Use-A关系,分别代表继承、关联和依赖。其中,关联关系根据其关联的强度又可以进一步划分为关联、聚合和合成,但说白了都是Has-A关系,合成聚合复用原则想表达的是优先考虑Has-A关系而不是Is-A关系复用代码。例如Properties类继承了Hashtable类,API中原话:因为 Properties继承于 Hashtable,所以可对Properties 对象应用put 和putAll 方法。但不建议使用这两个方法,因为它们允许调用者插入其键或值不是String 的项。相反,应该使用setProperty 方法。这个继承明显就是错误的,更好的做法是在Properties类中放置一个Hashtable类型的成员并且将其键和值都设置为字符串来存储数据。
(7)迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。(迪米特法则简单的说就是如何做到"低耦合",门面模式和调停者模式就是对迪米特法则的践行。
2、什么是面向对象?与面向过程有什么区别?
面向对象指的是把需求按照特点、功能划分,将这些存在共性的部分封装成类,创建的对象不是为了完成某一个步骤,而是描述某一个事物在解决问题的步骤中的行为,将繁琐的步骤,通过行为、功能,模块化,这就是面向对象。
面向过程指的是实现需求所需要的步骤,通过函数一步一步实现这些步骤。
面向过程在性能上来说,由于抽象程度较低,类无需实例化,所以性能较高,但不易维护,耦合性较高
面向过程在性能上来说,由于抽象程度较高,所以性能较低,但易于维护,耦合性低。
3、面向对象有哪三大特点?分别解释一下
封装:把客观事物封装成类,并且类可以让自己的数据和方法只让可信的类或对象操作,对不可信的进行隐藏,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。
继承:指可以让某个类型的对象获得另一个类型的对象的属性和方法
多态:是指一个类实例的相同方法在不同情形有不同表现形式。
4、什么是抽象类?什么是接口?有什么区别?
接口在java中是一个抽象类型,是抽象方法的集合。一个类通过继承接口的方式,从而继承接口的抽象方法。接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。对“接口为何是约束”的理解,我觉得配合泛型食用效果更佳。
使用abstract修饰符修饰的类。抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。
5、Java有哪几种内部类
成员内部类、局部内部类、匿名内部类和静态内部类
6、成员内部类是如何访问到外部类成员变量的?
在编译的时候,编译器会为成员内部类的构造方法添加一个指向外部成员类对象的引用,所以如果没有创建外步类对象,也就无法创建内部类。
7、.为什么局部内部类和匿名内部类只能访问局部final变量
会造成数据不一致性。内部类和外部类在编译的时候会被编译成两个class文件。内部类和外部类是处于同一个级别的,内部类不会因为定义在方法中就会随着方法的执行完毕就被销毁。
这里就会产生问题:当外部类的方法结束时,局部变量就会被销毁了,但是内部类对象可能还存在(只有没有人再引用它时,才会死亡)。这里就出现了一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的"copy"。这样就好像延长了局部变量的生命周期,将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,所以必须使用final修饰。
8、什么是泛型
把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型。
9、为什么需要泛型?
早期Java是使用Object来代表任意类型的,但是向下转型有强转的问题,这样程序就不太安全,可能会出现ClassCastException,而使用了泛型,只要在编译器没有抛出错误,那么在运行期就肯定不会抛出ClassCastException。
10、泛型有哪些优点?
1)、编译时的强类型检查
泛型要求在声明时指定实际数据类型,Java 编译器在编译时会对泛型代码做强类型检查,并在代码违反类型安全时发出告警。
2)、避免了类型转换。
3)、泛型编程可以实现通用算法
通过使用泛型,程序员可以实现通用算法,这些算法可以处理不同类型的集合,可以自定义,并且类型安全且易于阅读。
11、泛型可以用在哪些地方?
类,方法,接口
12、什么是泛型擦除?
Java 泛型是使用类型擦除来实现的,使用泛型时,任何具体的类型信息都被擦除了。

  • 把泛型中的所有类型参数替换为 Object,如果指定类型边界,则使用类型边界来替换。因此,生成的字节码仅包含普通的类,接口和方法。
  • 擦除出现的类型声明,即去掉 <> 的内容。比如 T get() 方法声明就变成了 Object get() ;List< String> 就变成了 List。如有必要,插入类型转换以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。
    类型擦除确保没有为参数化类型创建新的类; 因此,泛型不会导致运行时开销。

13、什么是桥接方法?
桥接方法是 JDK 1.5 引入泛型后,为了使Java的泛型方法生成的字节码和 1.5 版本前的字节码相兼容,由编译器自动生成的方法。
14、泛型可否向上转型?
不可以,因为泛型的信息被擦除了。
15、什么是类型边界?
对泛型的类型参数设置限制的条件。使用extends关键字或super
16、类型边界可以有多个么?
可以。但是extends关键字后面的第一个类型参数可以是类或接口,其他类型参数只能是接口。
17、泛型有哪些约束?
1)、不能使用基本数据类型
2)、不能创建泛型的实例
3)、不能创建属性为泛型的静态成员变量
4)、类型参数不能使用类型转换或 instanceof
5)、不能创建类型参数的数组
6)、不能创建、catch 或 throw 参数化类型对象
7)、仅仅是泛型类相同,而类型参数不同的方法不能重载
泛型
18、异常的分类?
在这里插入图片描述

异常分为Error和Exception,他们都继承于Throwable,Error是程序无法处理的问题,如堆栈溢出。Exception是程序可以处理的问题,又分为受检异常(例如:ClassNotFoundException)和非受检异常。
受检异常指的是在Java代码中必须用 try-catch 代码块,或在方法签名中用 throws 关键字声明该方法可能会抛出的受查异常,否则编译无法通过。
非受检异常也就运行时异常,指的是在运行时才可能发生的异常,例如NullPointerFoundException

19、NoClassDefFoundError 和 ClassNotFoundException 区别是什么?
ClassNotFoundException继承于Exception,发生在类加载阶段,当使用Class.forName()或者findSystemClass()方法或LoadClass()方法,就会产生此异常。
NoClassFoundError继承于Error发生在编译阶段存在,但在运行时却找不到此类时而引起的,是由JVM抛出的。
20、JVM 是如何处理异常的?
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,​​​​JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
21、哪些地方会发生OutOfMemoryError
除了程序计数器外的所有区域,即虚拟机栈、本地方法栈、方法区、堆。
22、ArrayList有什么特点?
- 底层数据结构是一个数组
- 允许存放不止一个null
- 允许存放重复元素,存储顺序按照元素的添加顺序
- ArrayList并不是一个线程安全的集合,如果要用线程安全的集合,考虑使用CopyOnWriteArrayList或者使用Collections.synchronized(List l)函数返回一个线程安全的ArrayList类

23、如果ArrayList不指定默认容量,那么在最开始的时候底层数组的容量大小是多少?如果是0,什么时候变成10呢?
是0。在第一次add元素时。
24、ArrayList扩容时,每次扩容大小是多少?

  • 原来大小的1.5倍(最大为Integer.MAX_VALUE)。
  • 扩容的过程其实是一个将原来元素拷贝到一个扩容后数组大小的长度新数组中。所以 ArrayList 的扩容其实是相对来说比较消耗性能的。

25、ArryayList删除元素时,可能会发生缩容么?
不会。
26、ArrayList(int initialCapacity)会不会初始化数组大小?
会。但是List的大小没有变,因为list的大小是返回size的。
27、为什么ArrayList指定index很慢?
因为他复制了一个新的数组,把原本index位置上的数组放到了index+1的后面,index的位置放上了新值。
同理,指定index删除也挺慢的。
28、LinkedList的底层数据结构?
LinkedList底层数据结构是双向链表。Node节点有三个信息
- item:表示数据
- prve:表示前一个节点
- next:表示后一个节点

29、LinkedList是如何查询的?
根据传入的index来判断是在左边还是右边,然后选择从头节点遍历或者从尾节点遍历。
30、ArrayList和LinkedList谁更占用空间
一般情况下,LinkedList的占用空间更大,因为每个节点要维护指向前后地址的两个节点,但也不是绝对,如果刚好数据量超过ArrayList默认的临时值时,ArrayList占用的空间也是不小的,因为扩容的原因会浪费将近原来数组一半的容量,不过,因为ArrayList的数组变量是用transient关键字修饰的,如果集合本身需要做序列化操作的话,ArrayList这部分多余的空间不会被序列化。
31、为什么hashcode的计算要使用31作为乘数?
hashcode

  • 31 有个很好的性能,即用移位和减法来代替乘法,可以得到更好的性能: 31 * i == (i << 5)- i, 现代的 VM 可以自动完成这种优化。
  • 乘数是31的时候,碰撞的概率已经很小了,基本稳定。
  • 199的碰撞概率更小,这就相当于一排奇数的茅坑量多,自然会减少碰撞。「但这个范围值已经远超过int的取值范围了,如果用此数作为乘数,又返回int值,就会丢失数据信息」。

32、为什么使用扰动函数?
理论上来说字符串的hashCode是一个int类型值,那可以直接作为数组下标了,且不会出现碰撞。但是这个hashCode的取值范围是[-2147483648, 2147483647],有将近40亿的长度,谁也不能把数组初始化的这么大,内存也是放不下的。我们默认初始化的Map大小是16个长度 DEFAULT_INITIAL_CAPACITY = 1 << 4,所以获取的Hash值并不能直接作为下标使用,需要与数组长度进行取模运算得到一个下标值。hashMap源码这里不只是直接获取哈希值,还进行了一次扰动计算,(h = key.hashCode()) ^ (h >>> 16)。把哈希值右移16位,也就正好是自己长度的一半,之后与原哈希值做异或运算,这样就混合了原哈希值中的高位和低位,增大了「随机性」。计算方式如下图;
扰动函数
使用扰动函数就是为了增加随机性,让数据元素更加均衡的散列,减少碰撞。
33、以下这段代码是做什么的?

	static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

用来填充二进制位,以找到大于该数的最小2的倍数-1,最后+1就是最小的2的倍数。
34、什么是负载因子?
负载因子,可以理解成一辆车可承重重量超过某个阀值时,把货放到新的车上。
那么在HashMap中,负载因子决定了数据量多少了以后进行扩容。要选择一个合理的大小下进行扩容,默认值0.75就是说当阀值容量占了3/4s时赶紧扩容,减少Hash碰撞。
同时0.75是一个默认构造值,在创建HashMap也可以调整,比如你希望用更多的空间换取时间,可以把负载因子调的更小一些,减少碰撞。
35、为什么HashMap的size要用2的幂?
因为获取 key 在数组中对应的下标是通过 key 的哈希值与数组长度 -1 进行与运算,如:tab[i = (n - 1) & hash]

  • n 为 2 的整数次幂,这样 n-1 后之前为 1 的位后面全是 1,这样就能保证 (n-1) & hash 后相应的位数既可能是 0 又可能是 1,这取决于 hash 的值,这样能保证散列的均匀,同时与运算效率高,如果不是2的幂,(n - 1) & hash 就不等于 n & hash
  • 如果 n 不是 2 的整数次幂,会造成更多的 hash 冲突

36、HashMap的put()方法
1)、首先根据hash函数计算出一个hash值
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
2)、判断tab是否为空或者长度为0
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
3)、根据哈希值与数组长度-1计算下标,如果下标没有元素,直接放入,如果有且hash相等,key相同,则覆盖
tab[i = (n - 1) & hash])
4)、判断是否为树节点,如果是,则插入树中
5)、如果不是树节点,插入链表,并判断阈值是否大于等于8,如果大于则需要树化,在这个过程会判断链表中是否有key相同和hash相等的节点,如果有则覆盖。
6)、处理完毕后,判断是否大于阈值,如果是,扩容。
7)、treeifyBin,是一个链表转树的方法,但不是所有的链表长度为8后都会转成树,还需要判断存放key值的数组桶长度是否小于64 MIN_TREEIFY_CAPACITY。如果小于则需要扩容,扩容后链表上的数据会被拆分散列的相应的桶节点上,也就把链表长度缩短了。
37、HashMap的扩容机制?
jdk1.8中有一个优化操作,即无需像jdk1.7中那样rehash,只需要原数组长度进行与操作,如果为1,则说明需要放到原位置+oldCap的地方,反之直接放到原位置。
1)、首先判断oldCap是否大于0,如果大于判断oldCap是否大于最大值,1 <<< 30 ,如果大于不扩容,反之容量扩容为两倍,阈值也变为2倍。
2)、反之,初始化时把threshold 的值赋值给 newCap,,HashMap 使用 threshold 变量暂时保存 initialCapacity 参数的值
3)、反之,为初始化,初始化容量和大小,默认为16,阈值为12
4)、如果newThr为0,则使用阀值公式计算容量
5)、初始化数组桶,用于存放key,容量为newCap
6)、如果旧数组桶,oldCap有值,则遍历将键值映射到新数组桶中
7)、映射过程如下,如果只有一个值,计算新的下标放到新的桶中
8)、如果是红黑树,拆分红黑树,放入新桶
9)、如果是链表,使用优化操作来进行放入新桶
38、HashMap查找操作?
扰动函数的使用,获取新的哈希值,这在上一章节已经讲过
下标的计算,同样也介绍过 tab[(n - 1) & hash])
确定了桶数组下标位置,接下来就是对红黑树和链表进行查找和遍历操作了
39、什么是fail-fast?
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制。
例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
解决办法:

  1. 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
  2. 使用CopyOnWriteArrayList来替换ArrayList

IO模型

1、Linux有几种IO模型?

  • 阻塞IO(Blocking IO)
  • 非阻塞IO(NonBlocking IO)
  • IO多路复用(IO multiplexing)
  • 信号驱动IO(signal driven IO)
  • 异步IO(Asynchronous IO)

2、什么是阻塞IO?
在这里插入图片描述
阻塞IO的执行过程是进程进行系统调用,等待内核将数据准备好并复制到用户态缓冲区后,进程放弃使用CPU并一直阻塞在此,直到数据准备好。
3、什么是非阻塞IO?
非阻塞IO
每次应用程序询问内核是否有数据准备好。如果就绪,就进行拷贝操作;如果未就绪,就不阻塞程序,内核直接返回未就绪的返回值,等待用户程序下一个轮询。
大致经历两个阶段:

  1. 等待数据阶段:未阻塞, 用户进程需要盲等,不停的去轮询内核。
  2. 数据复制阶段:阻塞,此时进行数据复制。

在这两个阶段中,用户进程只有在数据复制阶段被阻塞了,而等待数据阶 段没有阻塞,但是用户进程需要盲等,不停地轮询内核,看数据是否准备好。
4、什么是IO多路复用?
在这里插入图片描述
多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。
相比于阻塞IO模型,多路复用只是多了一个select/poll/epoll函数。select函数会不断地轮询自己所负责的文件描述符/套接字的到达状态,当某个套接字就绪时,就对这个套接字进行处理。select负责轮询等待,recvfrom负责拷贝。当用户进程调用该select,select会监听所有注册好的IO,如果所有IO都没注册好,调用进程就阻塞。
这里的IO复用模型,并没有向内核注册信号处理函数,所以,他并不是非阻塞的。进程在发出select后,要等到select监听的所有IO操作中至少有一个需要的数据准备好,才会有返回,并且也需要再次发送请求去进行文件的拷贝。
5、什么是信号驱动式IO
信号驱动IO
应用进程预先向内核注册一个信号处理函数,然后用户进程返回,并且不阻塞,当内核数据准备就绪时会发送一个信号给进程,用户进程便在信号处理函数中开始把数据拷贝的用户空间中。
该模型也分为两个阶段:

  • 数据准备阶段:未阻塞,当数据准备完成之后,会主动的通知用户进程数据已经准备完成,对用户进程做一个回调。
  • 数据拷贝阶段:阻塞用户进程,等待数据拷贝。

6、什么是异步IO?
在这里插入图片描述
用户进程发起aio_read操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,然后就立刻去做其他事情了。当内核收到aio_read后,会立刻返回,然后内核开始等待数据准备,数据准备好以后,直接把数据拷贝到用户控件,然后再通知进程本次IO已经完成。
特点:

  1. 异步I/O执行的两个阶段都不会阻塞读写操作,由内核完成。
  2. 完成后内核将数据放到指定的缓冲区,通知应用程序来取。

7、什么是select、poll、epoll
注意:图片有误,epoll不是哈希表是红黑树
在这里插入图片描述
select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
7、什么是文件描述符?
在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符
8、详解select?
select有三个特点

  • select()函数是阻塞的, 只有某些端口状态转换了或者达到timeout才会返回
  • 该函数可以允许进程指示等待多个事件中任何一个的发生
  • select(), poll() 都是水平触发

API如下

int select(
    int max_fd, //
    fd_set *readset, 
    fd_set *writeset, 
    fd_set *exceptset, 
    struct timeval *timeout
) 

fd_set: 描述符集合 这个结构体中有一个数组,作用是用于向数组中添加描述符,将描述符添加到集合中,实际上是将描述符这个数字对应的比特位置1;而这个位图中能够添加多少描述符取决于一个宏:_FD_SETSIZE=1024,因此select模型所能够监控的描述符是有最大数量限制的;
参数max_fd 是需要监视的最大的文件描述符值+1;
rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
参数timeout 为结构timeval,用来设置select()的等待时间

调用流程如下:
在这里插入图片描述

  1. 使用copy_from_user从用户空间拷贝fd_set到内核空间
  2. 注册回调函数__pollwait
  3. 遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
  4. 以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
  5. __pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
  6. poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
  7. 如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
  8. 把fd_set从内核空间拷贝到用户空间。

9、select有什么缺点?

  • 单个进程所打开的FD是有限制的,通过FD_SETSIZE设置,默认1024
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

10、poll详解

#include <poll.h>
// 数据结构
struct pollfd {
    int fd;                         // 需要监视的文件描述符
    short events;                   // 需要内核监视的事件
    short revents;                  // 实际发生的事件
};

// API
int poll(struct pollfd fds[], nfds_t nfds, int timeout);

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
11、poll的缺点?

  • 每次调用poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 对socket扫描时是线性扫描,采用轮询的方法,效率较低(高并发时)

12、epoll详解

#include <sys/epoll.h>

// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体
// 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
// epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
struct eventpoll {
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
};

// API

int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程

epfd表示epoll句柄;
op表示fd操作类型:EPOLL_CTL_ADD(注册新的fd到epfd中),EPOLL_CTL_MOD(修改已注册的fd的监听事件),EPOLL_CTL_DEL(从epfd中删除一个fd)
fd是要监听的描述符;
event表示要监听的事件

在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:
struct epitem{
    struct rb_node  rbn;//红黑树节点
    struct list_head    rdllink;//双向链表节点
    struct epoll_filefd  ffd;  //事件句柄信息
    struct eventpoll *ep;    //指向其所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型
}

每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为红黑树元素个数)。

而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

在这里插入图片描述

优点:

  • 没有最大并发连接的限制,能打开的fd上限远大于1024(1G的内存能监听约10万个端口)
  • 采用回调的方式,效率提升。只有活跃可用的fd才会调用callback函数,也就是说 epoll 只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。
  • 内存拷贝。使用mmap()文件映射内存来加速与内核空间的消息传递,减少复制开销。

epoll对文件描述符的操作有两种模式:LT(level trigger,水平触发)和ET(edge trigger)。

  • 水平触发:默认工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件。

  • 边缘触发:当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时通知一次)。

ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。

13、epoll是怎么解决poll和select的缺点的?

  • 对于每次需要把fd拷贝到内核空间,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
  • 对于需要线性遍历fd这个缺点,epoll使用红黑树+链表的方式来实现,当某个事件就绪后会调用回调方法,把这个事件从红黑树放到链表上面,这样当需要判断有无事件准备就绪时,只需要判断链表有无即诶但

14、epoll使用步骤?
第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。
第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件
15、Java中的IO分类

  • BIO:数据的读取写入必须阻塞在一个线程内等待其完成。
  • NIO:叫一个线程不断的轮询每个水壶的状态,看看是否有水壶的状态发生了改变,从而进行下一步的操作
  • AIO:异步非阻塞无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。对应到烧开水中就是,为每个水壶上面装了一个开关,水烧开之后,水壶会自动通知我水烧开了。

16、Java中BIO与NIO的区别?
NIO与BIO的区别
17、三种IO的适用场景

  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择。
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂。
  • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

18、NIO的核心概念?详细说说
NIO有三个核心概念,分别是Channel(通道),Buffer(缓冲区),Selector(选择器)

  1. 缓冲区Buffer

    Buffer是一个对象。它包含一些要写入或者读出的数据。在面向流的I/O中,可以将数据写入或者将数据直接读到Stream对象中。

    在NIO中,所有的数据都是用缓冲区处理。这也就本文上面谈到的IO是面向流的,NIO是面向缓冲区的。

    缓冲区实质是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit)等信息。

    最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能于操作byte数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean)都对应一种缓冲区,具体如下:

    ByteBuffer:字节缓冲区
    CharBuffer:字符缓冲区
    ShortBuffer:短整型缓冲区
    IntBuffer:整型缓冲区
    LongBuffer:长整型缓冲区
    FloatBuffer:浮点型缓冲区
    DoubleBuffer:双精度浮点型缓冲区

  2. 通道Channel

    Channel是一个通道,可以通过它读取和写入数据,他就像自来水管一样,网络数据通过Channel读取和写入。

    通道和流不同之处在于通道是双向的,流只是在一个方向移动,而且通道可以用于读,写或者同时用于读写。

    因为Channel是全双工的,所以它比流更好地映射底层操作系统的API,特别是在UNIX网络编程中,底层操作系统的通道都是全双工的,同时支持读和写。

    Channel有四种实现:

    FileChannel:是从文件中读取数据。
    DatagramChannel:从UDP网络中读取或者写入数据。
    SocketChannel:从TCP网络中读取或者写入数据。
    ServerSocketChannel:允许你监听来自TCP的连接,就像服务器一样。每一个连接都会有一个SocketChannel产生。

  3. 多路复用器Selector

    Selector选择器可以监听多个Channel通道感兴趣的事情(read、write、accept(服务端接收)、connect,实现一个线程管理多个Channel,节省线程切换上下文的资源消耗。Selector只能管理非阻塞的通道,FileChannel是阻塞的,无法管理。

19、NIO有哪些事件?
NIO的主要事件有几个:读就绪、写就绪、有新连接到来。
我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一般是connect失败需要重连或者直接异步调用connect的时候。
其次,用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6之前是select、poll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事件的到来。新事件到来的时候,会在selector上注册标记位,标示可读、可写或者有连接到来。
20、Java的Selector有什么缺点?
同一个channel的select不能被并发的调用。因此,如果有多个I/O线程,必须保证:一个socket只能属于一个IoThread,而一个IoThread可以管理多个socket。
21、NIO为什么性能这么好?
因为连接的处理和读写的处理通常可以选择分开,这样对于海量连接的注册和读写就可以分发。同时由于Selector的存在,NIO可以使用一个线程来管理多个连接,这样就省去了BIO多线程模式下的创建线程,销毁线程,线程占用内存的开销。
22、NIO有什么问题?
NIO毕竟是单线程,一旦某个连接占用过长,还是会造成阻塞。当连接数<1000,并发程度不高或者局域网环境下NIO并没有显著的性能优势。
NIO并没有完全屏蔽平台差异,它仍然是基于各个操作系统的I/O系统实现的,差异仍然存在。使用NIO做网络编程构建事件驱动模型并不容易,陷阱重重。
23、什么是Reactor?Proactor呢?
一般情况下,I/O 复用机制需要事件分发器(event dispatcher)。 事件分发器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊: 谁谁谁的快递到了, 快来拿吧!开发人员在开始的时候需要在分发器那里注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调函数。
涉及到事件分发器的两种模式称为:Reactor和Proactor。 Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
而在Proactor模式中,事件处理者(或者代由事件分发器发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发起时,需要提供的参数包括用于存放读到数据的缓存区、读的数据大小或用于存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分发器得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO操作(称为overlapped技术),事件分发器等IO Complete事件完成。这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系统代劳的。
举个例子,将有助于理解Reactor与Proactor二者的差异,以读操作为例(写操作类似)。

  • 在Reactor中实现读
    步骤1:等待事件到来(Reactor负责)。
    步骤2:将读就绪事件分发给用户定义的处理器(Reactor负责)。
    步骤3:读数据(用户处理器负责)。
    步骤4:处理数据(用户处理器负责)。
  • 在Proactor中实现读:
    步骤1:等待事件到来(Proactor负责)。
    步骤2:得到读就绪事件,发起异步请求,执行读数据(现在由Proactor负责)。
    步骤3:将读完成事件分发给用户处理器(Proactor负责)。
    步骤4:处理数据(用户处理器负责)。
    24、NIO没办法处理是否读完,怎么办?
    一次数据有没有读完nio和bio都是存在的,一般协议里都有包结束的标识符或者包长度标记,可以用mark和reset的方式重复来处理半包。
    25、什么是内存映射文件?
    直接操作缓存区,不需要内核空间到用户空间的复制
    在这里插入图片描述
    在这里插入图片描述
    26、什么是分散读取(scatter),聚集写入(gather)
    分散读取(scatter):将一个通道中的数据分散读取到多个缓冲区中
    聚集写入(gather):将多个缓冲区中的数据集中写入到一个通道中
    提示:
    第一步,应该弄明白,有哪些IO模型,它们的区别是什么。
    第二步,要搞清楚,这些IO模型的缺陷是什么,在高并发的情况下,为什么阻塞式接口 + 多线程会遇到瓶颈。
    第三步,解决方案就是IO多路复用,要搞清楚,Java的多路复用不过是操作系统相关调用的封装。比如 select / poll / epoll / kqueued 等等。
    第四步,掌握 selector 的用法,然后JDK源代码就变得很清楚了。通过查看JDK源代码,可以验证上一步的结论。
    第五步:外围与之相关联的东西看看就好,像FileChannel, DirectBuffer等比较特殊的地方,花点心思去看看就好了,这就不重要了。

反射

1、反射的底层原理?

  1. 反射如何获得想要反射的类?
    我们写的代码是以.java为结尾的文件,对于jvm来说,他执行的是编译后的.class文件。对于Java来讲,万物皆对象,因此.class文件也被理解为一个类,这个类的名字就叫做Class。因此我们需要通过这个Class文件来获得我们需要反射的类的一些属性和方法。对于Class对象最常见的就是Class.forName()方法,通过它我们可以拿到我们想要反射的类。
    在类加载机制中,有一个类加载阶段,这个阶段会在方法区生成一个java.lang.Class对象,以代表这个类在方法区中的入口。由于方法区含有被虚拟机加载的类型信息、常量、静态变量、即使编译后的代码缓存等数据,所有我们可以通过Class对象获得类的信息。
  2. 如何解剖类?
    对于一个类来说,一般有构造方法,方法,成员变量这三部分组成。那想要反射,第一步就是获得到类的字节码文件,所以简单说一下得到类的字节码的几种方式
    (1)、Class.forName(“com.cj.test.Person”); 这就是上边我们用的方式
    (2)、对象.getClass();
    (3)、类名.class;
    获得到类的字节码后,我们可以调用Class中提供的一些方法,例如 clazz.getDeclaredConstructor(int.class)来获得某个类。

2、如何通过反射获得普通方法?

  • getMethods()
  • getMethods(String name,Class<?> …parameterTypes)
  • getDeclaredMethods()
  • getDeclaredMethods(String name,Class<?>…parameterTypes)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值