小言_互联网的博客

Java面经整理

451人阅读  评论(0)

腾讯

1.java基础

  • 8种基本数据类型,int几个字节

    类型 存储需求 取值范围
    byte 1B -128~127
    short 2B -32768~32767
    int 4B -20亿~20亿
    long 8B  
    float 4B 小数点后6~7位
    double 8B 小数点后15位
    char 2B  
    boolean   true/false

     

  • static,final关键字

    • static
      • 被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载了,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。
      • 静态变量(类变量):静态变量被所有的对象所共享,也就是说我们创建了一个类的多个对象,多个对象共享着一个静态变量,如果我们修改了静态变量的值,那么其他对象的静态变量也会随之修改。

        非静态变量(实例变量):如果我们创建了一个类的多个对象,那么每个对象都有它自己该有的非静态变量。当你修改其中一个对象中的非静态变量时,不会引起其他对象非静态变量值得改变。

      • static修饰的成员方法称作静态方法,这样我们就可以通过“类名. 方法名”进行调用。由于静态方法在类加载的时候就存在了,所以它不依赖于任何对象的实例就可以进行调用,因此对于静态方法而言,是木有当前对象的概念,即没有this、super关键字的。因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。

      • 被static修饰的代码块也叫静态代码块,会随着JVM加载类的时候而加载这些静态代码块,并且会自动执行。它们可以有多个,可以存在于该类的任何地方。JVM会按照它们的先后顺序依次执行它们,而且每个静态代码块只会被初始化一次,不会进行多次初始化。

      • 普通类是不允许声明为静态的,只要内部类才可以,被static修饰的内部类可以直接作为一个普通类来使用,而不需先实例一个外部类

        • 静态内部类只能访问外部类的静态成员,否则编译会报错
        • 不管是静态方法还是非静态方法都可以在非静态内部类中访问。
        • 如果需要调用内部类的非静态方法,必须先new一个OuterClass的对象outerClass,然后通过outer。new生成内部类的对象,而static内部类则不需要。
    • final
      • 修饰类:当用final修饰一个类时,表明这个类不能被继承。
      • 修饰方法:方法不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是在子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)
      • 修饰变量:当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。
  • static和abstract能同时用吗

    不能,因为static方法独立于任何实例,因此static方法必须被实现,而不能是抽象的abstract。
  • 内部类可以调用外部的数据吗?如果是静态的呢?

    可以。静态内部类只能访问外部类的静态成员,否则编译会报错
  • 重写(override)和重载(overload)的区别

    重载: 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。   

    重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。

  • 返回值不同的重载,可以吗?为什么?

    不可以。在java语言中,要重载(overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此java语言里面无法仅仅依靠返回值不同来对一个已有的方法进行重载。
    • java代码层面的特征签名:方法名称+参数顺序+参数类型
    • 字节码文件的特征签名:以上+方法返回值+受查异常表
  • equals和==

    • == : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。
    • equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:
    • 情况1:类override没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
    • 情况2:类override了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
    • 举个例子:

      public class test1 {
          public static void main(String[] args) {
              String a = new String("ab"); // a 为一个引用
              String b = new String("ab"); // b为另一个引用,对象的内容一样
              String aa = "ab"; // 放在常量池中
              String bb = "ab"; // 从常量池中查找
              if (aa == bb) // true
                  System.out.println("aa==bb");
              if (a == b) // false,非同一对象
                  System.out.println("a==b");
              if (a.equals(b)) // true
                  System.out.println("aEQb");
              if (42 == 42.0) { // true
                  System.out.println("true");
              }
          }
      }

      说明:

    • String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
    • 当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。
  • String和StringBuffer、 StringBuilder

  1. 可变性:String 类中使用 final 关键字修饰字符数组来保存字符串private final char value[],所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
  2. 线程安全性:String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁(synchronized),所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
  3. 性能:每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder;多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer。
  • hashCode 与 equals (重要)

    • hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)。
    • 当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相同的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象如果equals方法返回为true,HashSet 就不会让其加入操作成功。如果返回false,就会重新散列到其他位置。
    • hashCode()与equals()的相关规定
    1. 如果不需要将该类的对象存放到哈希的集合中,比较对象相等时只需要重写equals()方法
    2. 如果不需要将该类的对象存放到哈希的集合中,则需要重写hashCode()方法
    • 例子
class Person {
	 int age;
	 String name;

	 public Person(String name, int age) {
		 this.name = name;
		 this.age = age;
	 }

	 public String toString() {
		 return name + " - " +age;
	 }

	 /** 
	  * @desc重写hashCode 
	  */  
	 @Override
	 public int hashCode(){  
		 int nameHash =  name.toUpperCase().hashCode();
		 return nameHash ^ age;
	 }

	 /** 
	  * @desc 覆盖equals方法 
	  */  
	 @Override
	 public boolean equals(Object obj){  
		 if(obj == null){  
			 return false;  
		 }  
		   
		 //如果是同一个对象返回true,反之返回false  
		 if(this == obj){  
			 return true;  
		 }  
		   
		 //判断是否类型相同  
		 if(this.getClass() != obj.getClass()){  
			 return false;  
		 }  
		   
		 Person person = (Person)obj;  
		 return name.equals(person.name) && age==person.age;  
	 } 
}
  • Object类有哪些方法

    • hashCode(),equals()
    • toString()
    • clone()
    • wait(),notify(),notifyAll()
    • finalize()
  • collection和collections

  • length,length()和size()

    • java 中的length 属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性.

    • java 中的length()方法是针对字 符串String说的,如果想看这个字符串的长度则用到 length()这个方法.

    • .java 中的size()方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!

  • ArrayList简介

  ArrayList 的底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。

它继承于 AbstractList,实现了 ListRandomAccessCloneablejava.io.Serializable 这些接口。

在我们学数据结构的时候就知道了线性表的顺序存储,插入删除元素的时间复杂度为O(n),求表长以及增加元素,取第 i 元素的时间复杂度为O(1)

  • ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
  • ArrayList 实现了RandomAccess 接口, RandomAccess 是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的。在 ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
  • ArrayList 实现了Cloneable 接口,即覆盖了函数 clone(),能被克隆
  • ArrayList 实现java.io.Serializable 接口,这意味着ArrayList支持序列化能通过序列化去传输
  • 和 Vector 不同,ArrayList 中的操作不是线程安全的!所以,建议在单线程中才使用 ArrayList,而在多线程中可以选择 Vector 或者 CopyOnWriteArrayList。

LinkedList简介

LinkedList是一个实现了List接口和Deque接口的双端链表。 LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性; LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以调用静态类Collections类中的synchronizedList方法:

List list=Collections.synchronizedList(new LinkedList(...));

看LinkedList类中的一个内部私有类Node就很好理解了:

private static class Node<E> {
        E item;//节点值
        Node<E> next;//后继节点
        Node<E> prev;//前驱节点

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

这个类就代表双端链表的节点Node。这个类有三个属性,分别是前驱节点,本节点的值,后继结点。

获取尾节点(index=-1)数据方法:getLast() 方法在链表为空时,会抛出NoSuchElementException,而peekLast() 则不会,只是会返回 null

删除方法:removeLast()在链表为空时将抛出NoSuchElementException,而pollLast()方法返回null

  1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

  2. 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构;

  3. 插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。

  4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index) 方法)。

  5. 内存空间占用: ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

ArrayList扩容机制

/**
     * 如有必要,增加此ArrayList实例的容量,以确保它至少能容纳元素的数量
     * @param   minCapacity   所需的最小容量
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }
   //得到最小扩容量
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
              // 获取默认的容量和传入参数的较大值
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }
  //判断是否需要扩容,上面两个方法都要调用
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 如果说minCapacity也就是所需的最小容量大于保存ArrayList数据的数组的长度的话,就需要调用grow(minCapacity)方法扩容。
        //这个minCapacity到底为多少呢?举个例子在添加元素(add)方法中这个minCapacity的大小就为现在数组的长度加1
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }

 

/**
 * 允许分配的数组最大容量
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

/**
 * ArrayList扩容的核心方法
 * @param minCapacity:最小扩容量
 */
private void grow(int minCapacity) {
        // oldCapacity为旧容量=数组当前长度
        int oldCapacity = elementData.length;

        // newCapacity为新容量=oldCapacity的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);

        // 然后检查新容量是否大于最小需要容量,若还是小于最小扩容量,那么就把最小扩容量当作数组的新容量
        if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

        // 再检查新容量是否超出了ArrayList所定义的最大容量,
        // 若超出了,则调用hugeCapacity()来比较minCapacity和 MAX_ARRAY_SIZE,
        // 如果minCapacity大于MAX_ARRAY_SIZE,则新容量则为Interger.MAX_VALUE,否则,新容量大小则为 MAX_ARRAY_SIZE。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
        }

private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
        }
  • ArrayList和Vector

    • Vector与ArrayList一样,也是通过数组实现的,Vector类的所有方法都是同步的。它也是线程安全的,而Arraylist是线程异步(ASynchronized)的,是不安全的。如果不考虑到线程的安全因素,一般用Arraylist效率比较高。
    • 使用ArrayList时,如果不指定大小,会生成一个空的数组;

      使用Vector时,如果不指定大小,会默认生成一个10个元素大小的数组

    • Vector 实现类中有一个变量 capacityIncrement 用来表示每次容量自增时应该增加多少,如果不指定,默认为0

      在扩容时,会判断,如果指定了capacityIncrement,会先把数组容量扩大到oldCapacity + capacityIncrement,如果没有指定capacityIncrement,会先把数组容量扩大到2倍的oldCapacity, 然后再进行判断扩充后的容量是否满足要求,如果不满足要求,直接将容量扩大到指定大小,源码如下:

      private void grow(int minCapacity) {
              // overflow-conscious code
              int oldCapacity = elementData.length;
              int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                               capacityIncrement : oldCapacity);
              if (newCapacity - minCapacity < 0)
                  newCapacity = minCapacity;
              if (newCapacity - MAX_ARRAY_SIZE > 0)
                  newCapacity = hugeCapacity(minCapacity);
              elementData = Arrays.copyOf(elementData, newCapacity);
          }
  • Set不能存放重复元素,其底层是如何实现的

    • HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone() writeObject()readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
    • 当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
  • HashMap原理

    • JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

    • JDK 1.8 HashMap 的 hash 方法源码:

      static final int hash(Object key) {
            int h;
            // key.hashCode():返回散列值也就是hashcode
            // ^ :按位异或
            // >>>:无符号右移,忽略符号位,空位都以0补齐
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//hashcode的高16位与低16位异或
        }
    • 所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可:

 

  • 为什么HashMap无序?为什么不安全?

    因为元素在底层数组中的索引位置是经过hash算法(key的哈希值+扰动)计算出来的,与原来的顺序没有关系。线程不安全,多线程访问的情况下,会出现问题。
  • Hash碰撞的解决方法

    • 数组+链表
    • 当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
  • HashMap满了之后怎么扩容(rehash)

    • loadFactor负载因子/装填因子

      loadFactor加载因子是控制数组存放数据的疏密程度,loadFactor越趋近于1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor越小,也就是趋近于0,数组中存放的数据(entry)也就越少,也就越稀疏。

      loadFactor太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor的默认值为0.75f是官方给出的一个比较好的临界值

      给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

    • threshold

      threshold = capacity * loadFactor当Size>=threshold的时候,那么就要考虑对数组的扩增了,也就是说,这个的意思就是 衡量数组是否需要扩增的一个标准

    • 进行扩容,会伴随着一次重新hash分配,并且会遍历hash表中所有的元素,是非常耗时的。在编写程序中,要尽量避免resize。
    • 下面我们讲解下JDK1.8做了哪些优化。经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

      元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

      因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:

      这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。

    • final Node<K,V>[] resize() {
              Node<K,V>[] oldTab = table;
              // oldCap旧容量:当前table的长度
              int oldCap = (oldTab == null) ? 0 : oldTab.length;
              int oldThr = threshold;
              int newCap, newThr = 0;
              //
              if (oldCap > 0) {
                  // 如果旧容量超过最大值就不再扩容了,就只好随你碰撞去吧
                  if (oldCap >= MAXIMUM_CAPACITY) {
                      threshold = Integer.MAX_VALUE;
                      return oldTab;
                  }
                  // 没超过最大值,newCap新容量为旧容量的2倍,newThr新阈值也扩容为旧阈值的2倍
                  else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
                      newThr = oldThr << 1; // double threshold
              }
              else if (oldThr > 0) // initial capacity was placed in threshold
                  newCap = oldThr;
              else {
                  // signifies using defaults
                  newCap = DEFAULT_INITIAL_CAPACITY;
                  newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
              }
              // 计算新的resize上限
              if (newThr == 0) {
                  float ft = (float)newCap * loadFactor;
                  newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
              }
              threshold = newThr;
              @SuppressWarnings({"rawtypes","unchecked"})
              Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
              table = newTab;
              if (oldTab != null) {
                  // 把oldTab中每个bucket都移动到新的buckets中
                  for (int j = 0; j < oldCap; ++j) {
                      Node<K,V> e;
                      // oldTable该索引位置有key
                      if ((e = oldTab[j]) != null) {
                          oldTab[j] = null;
      
                          // 该位置无冲突Node
                          if (e.next == null)
                              // 计算该key在新table中索引位置:e.hash&(newCap-1),存放在此
                              newTab[e.hash & (newCap - 1)] = e;
      
                          // 有冲突,是红黑树Node
                          else if (e instanceof TreeNode)
                              // 插入红黑树
                              ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
      
                          // 有冲突,是链表Node
                          else {
                              Node<K,V> loHead = null, loTail = null;
                              Node<K,V> hiHead = null, hiTail = null;
                              Node<K,V> next;
                              do {// 遍历插入
                                  next = e.next;
                                  // 原索引位置
                                  if ((e.hash & oldCap) == 0) {
                                      if (loTail == null)
                                          loHead = e;
                                      else
                                          loTail.next = e;
                                      loTail = e;
                                  }
                                  // 原索引位置+oldCap
                                  else {
                                      if (hiTail == null)
                                          hiHead = e;
                                      else
                                          hiTail.next = e;
                                      hiTail = e;
                                      }
                                  } while ((e = next) != null);
                                  
                                  // 原索引放到bucket里
                                  if (loTail != null) {
                                      loTail.next = null;
                                      newTab[j] = loHead;
                                  }
                                  
                                  // 原索引+oldCap放到bucket里
                                  if (hiTail != null) {
                                      hiTail.next = null;
                                      newTab[j + oldCap] = hiHead;
                                  }
                              }
                          }
                      }
                  }
          return newTab;
      }

       

  • HashMap 的长度为什么是2的幂次方

    散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
  • HashMap put操作的最差情况解决方案

    HashMap只提供了put用于添加元素,putVal方法只是给put方法调用的一个方法,并没有提供给用户使用。

    对putVal方法添加元素的分析如下:

    • ①如果定位到的数组位置没有元素 就直接插入。
    • ②如果定位到的数组位置有元素就和要插入的key比较,如果key相同就直接覆盖,如果key不相同,就判断p是否是一个树节点,如果是就调用e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value)将元素添加进入。如果不是就遍历链表插入(插入的是链表尾部)。
    • ①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

      ②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

      ③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

      ④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

      ⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

      ⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

    •  1 public V put(K key, V value) {
       2     // 对key的hashCode()做hash
       3     return putVal(hash(key), key, value, false, true);
       4 }
       5 
       6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
       7                boolean evict) {
       8     Node<K,V>[] tab; Node<K,V> p; int n, i;
       9     // 步骤①:tab为空则创建
      10     if ((tab = table) == null || (n = tab.length) == 0)
      11         n = (tab = resize()).length;
      12     // 步骤②:计算index,并对null做处理 
      13     if ((p = tab[i = (n - 1) & hash]) == null) 
      14         tab[i] = newNode(hash, key, value, null);
      15     else {
      16         Node<K,V> e; K k;
      17         // 步骤③:节点key存在,直接覆盖value
      18         if (p.hash == hash &&
      19             ((k = p.key) == key || (key != null && key.equals(k))))
      20             e = p;
      21         // 步骤④:判断该链为红黑树
      22         else if (p instanceof TreeNode)
      23             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
      24         // 步骤⑤:该链为链表
      25         else {
      26             for (int binCount = 0; ; ++binCount) {
      27                 if ((e = p.next) == null) {
      28                     p.next = newNode(hash, key,value,null);
                              //链表长度大于8转换为红黑树进行处理
      29                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  
      30                         treeifyBin(tab, hash);
      31                     break;
      32                 }
                          // key已经存在直接覆盖value
      33                 if (e.hash == hash &&
      34                     ((k = e.key) == key || (key != null && key.equals(k))))                                          break;
      36                 p = e;
      37             }
      38         }
      39         // 表示在桶中找到key值、hash值与插入元素相等的结点
      40         if (e != null) { 
      41             V oldValue = e.value;// 记录e的value
      42             if (!onlyIfAbsent || oldValue == null)
      43                 e.value = value;// 用新值替换旧值
      44             afterNodeAccess(e);// 访问后回调
      45             return oldValue;// 返回旧值
      46         }
      47     }
      
      48     ++modCount;
      49     // 步骤⑥:超过最大容量 就扩容
      50     if (++size > threshold)
      51         resize();
      52     afterNodeInsertion(evict);
      53     return null;
      54 }

  • 海量数据存进HashMap性能会变差吗?

    当一个链表太长的时候,HashMap会动态的将它替换成一个红黑树,这话的话会将时间复杂度从O(n)降为O(logn)。
  • HashMap和HashTable的区别,并说明其底层实现数据结构

    • 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
    • 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
    • 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛出 NullPointerException。
    • 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
    • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
  • 说说TreeMap的实现原理

    • TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。
  • 海量数据怎么查找敏感词

  • Java中异常的分类

  • Jvm是怎样进行内存分配的

  • String a=new String("Hello");说说栈、堆、常量池的存储情况

  • 讲一下垃圾回收机制

  • 为什么Java语言具有平台无关性,jvm内存回收方式有哪些,讲一下类加载机制,有没有在项目中考虑及体现内存回收。

  • 老年代还有内存,但是程序显示OOM什么原因?

  • JVM内存模型

  • JVM参数,线程数调整

  • 性能调优看哪些命令?

  • BIO、NIO、AIO各自的含义、特点,差异体现在哪里

  • Rest ful API的规范

2.java高并发

  • StringBuilder和StringBuffer使用单线程执行,有区别吗?

    StringBuffer是线程安全的,而StringBuilder是非线程安全的,在单线程的情况下,StringBuffer由于同步机制性能降低,建议使用StringBuilder。
  • java并发集合有哪些

  • CurrentHashMap原理

    • 一个ConcurrentHashMap维护多个Segment数组,一个Segment维护多个HashEntry数组。

    • 对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。

  • ConcurrentHashMap 和 Hashtable 的区别

    • HashTable和HashMap的实现原理几乎一样,差别无非是1.HashTable不允许key和value为null;2.HashTable是线程安全的。但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
    • HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想。
  • ConcurrentHashMap在不扩容时,get和put的操作过程,在扩容时get和put怎么操作

    • get方法:先定位Segment,再定位HashEntry,无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。
    • put方法:
      • ​​​​​​​1.计算得到的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
      • 2.调用segment的put方法
        • tryLock:不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),若找不到,则创建HashEntry。tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。定位HashEntry
        • 定位到HashEntry,put
        • 若size超过阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。这样可以最大程度避免之前散列好的entry重新散列
    •  到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))。synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
       
  • synchronized静态方法和实例所属方法的区别

  • 类中的两个方法,一个public synchronized修饰,一个public static synchronized修饰,两个线程分别调用,会出现正常还是死锁

  • volatile了解哪些?如何实现你说的可见性

  • 用volatile修饰的a,多线程调用会不会出现问题?为什么?

  • 说说Lock

  • 说说ReentrantLock是基于哪个类的?说说队列同步器

  • 线程的几种状态,waiting和blocked的转换?

  • 讲讲线程,线程池

  • 常见的几种线程池

  • Executors工具类有哪几种构造线程池的方法,说说线程池创建时的参数

  • 线程池处理task的流程

  • happen before原则

  • CAS原理,AQS

  • 阻塞队列及其具体的实现有哪些

3.数据结构与算法

  • 排序算法

  • 堆和栈的数据结构

  • 红黑树与AVL二叉平衡树的区别

  1. 二叉查找树(BST):
    1. 左子树所有节点值均小于或等于根节点的值+右子树所有节点值均大于或等于根节点的值+左右子树也是BST
    2. 查找,插入,删除的时间复杂度等于树的高度O(logN)
    3. 缺点:会退化成为倾斜的二叉查找树,复杂度为O(N)
  2. 平衡二叉树(AVL):平衡二叉树就是为了解决二叉查找树退化成一颗链表而诞生了
    1. 具有二叉查找树的全部特性。
    2. 每个节点的左子树和右子树的高度差至多等于1。
    3. 对于有 n 个节点的平衡树,最坏的查找时间复杂度也为 O(logn)。
    4. 缺点:平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋右旋来进行调整,使之再次成为一颗符合要求的平衡树。
  3. 红黑树:在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树
    1. 节点是红色或黑色。
    2. 根节点是黑色
    3. 每个叶子节点都是黑色的空节点(NIL节点)
    4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
    5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点

              当插入或删除节点使得红黑树的规则被打破时,需要对其进行调整:

              1.变色:为了重新符合红黑树的规则,尝试把红色节点变为黑色,或者把黑色节点变为红色(会发生连锁效应)

              2.左旋:逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。

                            

              3.右旋:顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子。

                            

               与平衡树不同的是,红黑树在插入、删除等操作,不会像平衡树那样,频繁着破坏红黑树的规则,所以不需要频繁着调整,这也是我们为什么大多数情况下使用红黑树的原因。

  • 最长子序列问题,说说思路

  • 7升桶和3升桶,怎么打到2升水?

  • n*m的矩阵,每行每列都有序,查找某个数

  • 有序整数数组,给定一个数,从数组中找出2个数相加等于它。要求O(n)时间复杂度。

4.操作系统

  • 说说用户态和内核态的特点?不同之处?

    • 内核态:运行操作系统的内核程序(与硬件关联紧密的程序:时钟管理,中断处理,运行频率较高的程序:进程管理,内存管理)
    • 用户态:运行用户自编写的应用程序
    • 内核程序是用户程序的管理者,作为管理程序,内核程序能够执行一些用户程序不能执行的特权指令,如,IO指令,置中断指令
  • 进程与线程的区别

    • 进程是系统资源分配的基本单位,线程是CPU调度的基本单位
    • 一个进程的所有线程共享该进程的资源
    • 进程切换需要保存和设置进程的CPU环境;而线程切换只需要保存和设置少量寄存器内容
  • 进程之间的通信方式

    • 共享存储区:在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行读/写操作实现进程之间的信息交换
    • 消息传递:利用操作系统提供的消息传递方法实现进程通信
      • 直接通信方式:发送进程直接把消息发送给接收进程,把它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中区的消息
      • 间接通信方式:发送进程把消息发送到某个中间实体(一般称为信箱)中,接收进程从中间实体中取得消息
    • 管道通信:管道是一个固定大小的缓冲区,半双工通信,某一时刻只能单向传输
      • 写进程:以字符流形式将大量的数据送入写管道,写满则阻塞
      • 读进程:从管道中读取数据,数据一旦被读取,就从管道中被抛弃
  • 说说协程,协程的优势

  1. 协程既不是进程,也不是线程,协程仅仅是一个特殊的函数,协程跟他们就不是一个维度。
  2. 一个进程可以包含多个线程,一个线程可以包含多个协程。
  3. 一个线程内的多个协程虽然可以切换,但是这多个协程是串行执行的,只能在这一个线程内运行,没法利用CPU多核能力。
  4. 协程与进程一样,它们的切换都存在上下文切换问题。

表面上,进程、线程、协程都存在上下文切换的问题,但是三者上下文切换又有明显不同,见下表:

协程适合I/O 阻塞型。
I/O本身就是阻塞型的(相较于CPU的时间世界而言)。就目前而言,无论I/O的速度多快,也比不上CPU的速度,所以一个I/O相关的程序,当其在进行I/O操作时候,CPU实际上是空闲的。
我们假设这样的场景,如下图:1个线程有5个I/O的事情(子程序)要处理。如果我们绝对的串行化,那么当其中一个I/O阻塞时,其他4个I/O并不能得到执行,因为程序是绝对串行的,5个I/O必须一个一个排队等待处理,当一个I/O阻塞时,其它4个也得等着。

而协程能比较好地处理这个问题,当一个协程(特殊子进程)阻塞时,它可以切换到其他没有阻塞的协程上去继续执行,这样就能得到比较高的效率,如下图所示:

  • CPU的调度策略

    • 方式
      • 非抢占式调度:CPU上当前运行的进程不会因为有更为重要或紧迫的进程进入就绪队列而暂停,还会继续执行直到进程完成或进入阻塞状态
      • 抢占式调度:CPU上当前运行的进程可以因为有更为重要或紧迫的进程进入就绪队列而暂停,将CPU分配给这个更为重要或紧迫的进程
    • 调度算法
    1. 先来先服务(FCFS):属于不可抢占算法,对长作业比较有利,对短作业不利
    2. 短作业优先(SJF):
      • 每次从就绪队列中选择估计运行时间最短的进程,将CPU分配给它,使之立即执行
      • 对长作业不利,容易产生饥饿现象(长作业长期不被调度)
    3. 优先级调度:每次从就绪队列中选择优先级最高的进程,将CPU分配给它,使之立即执行
      • 非抢占式优先级调度和抢占式优先级调度
      • 静态优先级(优先级在进程创建时确定)和动态优先级(优先级在进程运行期间动态调整)
    4. 高响应比优先调度:每次进行作业调度时,先计算后备队列中每个作业的响应比,从中选出响应比最高的作业投入运行
      • 响应比Rp=(等待时间+要求服务时间)/要求服务时间
      • 对于长作业,作业的响应比可以随着等待时间的增加而提高,当其等待时间足够长时,响应比便可以升到很高,从而也可获得CPU。克服了饥饿状态,兼顾了长作业。
    5. 时间片轮转调度:系统将所有的进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中的第一个进程执行,即先来先服务,但仅能运行一个时间片。在使用完一个时间片后,即使进程并未完成其运行,它也必须释放出(被抢占)CPU给下一个就绪的进程,而被抢占的进程返回到就绪队列的末尾重新排队,等候再次运行。
    6. 多级反馈队列调度:综合了时间片轮转算法和优先级调度算法
      • 设置多个就绪队列,优先级从高到低
      • 优先级越高的就绪队列分配的时间片越小
  • 会不会存在有些进程一直得不会得到CPU的使用权

    在短进程优先的调度算法下,长进程可能会长时间得不到CPU的调度,即产生饥饿现象
  • 某个进程进入死循环,cpu还上下文切换吗?

    不会影响上下文切换
  • 什么是死锁,形成死锁须具备哪些条件,怎么预防死锁,如果死锁已经产生怎么解决,讲一下银行家算法。

    • 死锁产生的必要条件
      • 互斥条件:在一段时间内某资源仅为一个进程所占有
      • 不可剥夺条件:进程所获得的资源在使用完毕之前,不能被其他进程强行夺走
      • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求
      • 循环等待条件:进程的资源等待关系形成了闭合的环
    • 死锁预防:破坏必要条件
      • 破坏互斥,不太可行
      • 强行剥夺进程占有的资源
      • 资源静态分配
      • 资源按序分配:只要进程提出申请分配资源Ri,则该进程在以后的资源申请中,只能申请编号大于Ri的资源
    • 死锁避免:银行家算法
      • 允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性。若此次分配不会导致系统进入不安全状态(如果系统无法找到一个安全序列,则称系统处于不安全状态),以避免发生死锁。
      • 可利用资源向量(Available):Available[j]=K--表示系统中现有Rj类资源的数目为K
      • 最大需求矩阵(Max):Max[i][j]=K--表示进程i需要Rj类资源的最大数目为K
      • 分配矩阵(Allocation):Allocation[i][j]=K--表示进程i当前已分到的Rj类资源的数目为K
      • 需求矩阵(Need):Need[i][j]=K--表示进程i还需要Rj类资源的数目为K
    • 死锁解除
      • 资源剥夺
      • 撤销进程
      • 进程回退:回退到足以回避死锁的地步
  • 页面置换算法

    • 最佳置换算法(OPT):淘汰以后不再被访问的页面
    • 先进先出置换算法(FIFO):淘汰最早进入内存的页面,会出现Belady异常
    • 最近最久未使用算法(LRU):淘汰最近最长时间未访问过的页面
    • 时钟置换算法(CLOCK):使用位u+修改位m
  • LRU算法知道吗?自己设计怎么设计?

    为每个页面设置一个访问字段,来记录页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰

5.计算机网络

  • TCP和UDP的区别

    • UDP:在IP服务的基础上,仅增加复用、分用,差错检验,无连接不可靠;面向报文(依次交付一个完整报文);首部长度:8B
    • TCP:面向连接,点对点,全双工通信;可靠(差错检验,保证有序到达,无丢失);面向字节流;首部长度:20B
  • TCP三次握手和四次挥手过程

    • 协议层有哪些?说说五层协议

    1. OSI参考模型:物理层,数据链路层,网络层,传输层,会话层,表示层,应用层
    2. TCP/IP模型(Internet上使用的协议):网络接口层,网际层,传输层,应用层
    3. 网络五层模型:物理层,数据链路层,网络层,传输层,应用层
    • 三次握手,建立连接

  1. 客户端向服务器发送同步请求报文段(不携带数据):SYN=1,seq=x(选择1个初始序列号);客户端进入SYN-SENT状态
  2. 服务器接收到请求报文段之后,向客户端发送同步确认报文段(不携带数据):SYN=1,ACK=1,seq=y(选择1个初始序列号),ack=x+1(确认收到x序列号);服务器端进入SYN-RCVD状态
  3. 客户端收到同步确认报文段之后,向服务器端发送确认报文段(可以携带数据):ACK=1,seq=x+1,ack=y+1(确认收到y序列号);客户端和服务器进入ESTABLISHED状态,完成3次握手。
  • 四次挥手,释放连接

  1. 客户端向服务器发送请求终止报文段(停止发送数据):FIN=1,seq=u(之前最后传输字节序号+1);客户端进入FIN-WAIT-1状态
  2. 服务器接收到连接释放报文段之后,向客户端发送确认报文段(不携带数据):ACK=1,seq=v,ack=u+1(确认收到u序列号);服务器端进入CLOSE-WAIT状态(半关闭),通知高层的应用进程:客户端要释放与服务器端的TCP连接了。客户端收到确认报文段之后,进入FIN-WAIT-2状态:等待服务器发送确认终止报文。
  3. 服务器将数据发送完毕后,向客户端发送确认终止报文:FIN=1,ACK=1,seq=w(半关闭状态,服务器可能发送了一些数据,假定此时序号为w),ack=u+1;服务器端进入LAST-ACK状态,等待客户端的最终确认。
  4. 客户端接收到确认终止报文之后,向服务器发送确认报文段:ACK=1,seq=u+1,ack=w+1;客户端进入TIME-WAIT状态(此时客户端的TCP连接还没有释放),等待2MSL(最长报文段寿命)时间之后,连接才真正的释放CLOSED。
  5. 服务器端只要收到客户端的确认,立即CLOSED状态。
  • TCP为什么是三次握手,不是两次

    防止两次握手的情况下,客户端发出的已失效的连接请求报文段突然又传送到了服务器端,产生了错误,因为三次握手时,客户端收到服务器端对失效连接报文段的确认时,可以不予理睬,则连接不会建立。
  • 为什么四次挥手不是三次

  1. 保证客户端发送的最后一个确认报文段能够到达服务器端,不等待2MSL,则可能服务器端没能进入正常关闭状态,而客户端已经关闭,无法重传。
  2. 防止出现“已失效的连接请求报文段”,等待2MSL可保证本连接持续的时间内所产生的所有报文段从网络中消失。
  • TCP最后一次ACK包没有送到就开始传输数据包,会发生什么?

    题目的意思应该是说,客户端只经过两次握手就开始发送数据会发生什么,见上上题。
  • TCP的滑动窗口

  1. 接受窗口(rwnd):接收方根据接受目前缓存大小所允许的最新窗口值;反映了接收方的容量。
  2. 拥塞窗口(cwnd):发送方根据自己估算的网络拥塞程度而设置的窗口值,反映了网络当前的容量。

     发送窗口的上限值=Min(rwnd,cwnd)

  • ping命令,Traceroute是哪一层

  1. ping:
    1. 测试两个主机之间的连通性
    2. 工作在应用层,直接使用网络的ICMP协议的回送请求和回答报文,没有使用传输层的TCP或UDP协议
  2. traceroute:
    1. 跟踪分组经过的路由
    2. 工作在网络层,使用ICMP协议的时间超时报文
  • 说说HTTP协议的特点

  1. 客户/服务器模式:
  2. 无连接:HTTP协议本身是无连接的,使用TCP连接
  3. 无状态:协议对于事务处理没有记忆能力,缺少状态意味着如果后续处理需要前面的信息,则必须重传;通常在用户主机上存储Cookie文本文件,用于Web服务识别用户。
  • 说说HTTP的过程

  1. 浏览器分析链接指向的页面的URL
  2. 浏览器向DNS请求解析URL的IP地址
  3. 域名系统DNS解析出IP地址
  4. 浏览器与服务器建立TCP连接(默认端口号:80)
  5. 浏览器向服务器发出HTTP请求报文
  6. 服务器向浏览器发送HTTP响应报文
  7. TCP连接终止
  8. 浏览器将响应报文进行解释,并将Web页面显示给用户
  • HTTP常见的状态码

  • GET请求和Post请求的区别

  1. 报文层面:
    1. get将请求信息(账号,密码)放在URL中,不安全;
    2. post将请求信息放在报文体中,安全
  2. 数据库层面:
    1. get是查询数据的,符合幂等性(多次操作对数据库的结果是相同的)和安全性(对数据库中的操作没有改变数据库中的数据)
    2. post是向数据库中提交数据,因此会改变数据库中的数据,post是作用在上一级的URL上的,每一次请求都会添加一份新资源,不符合幂等性
  3. 其他层面:
    1. get请求可以被缓存,可以保存在浏览器的浏览记录中,能够保存为书签
    2. post则不行
  • Cookie和Session的区别

    HTTP协议无状态的解决方案
    • 客户端解决方案:Cookie是服务器发送给客户端的特殊信息(例如:个人信息),以文本的形式存储在客户端,客户端每次向服务器发送请求时,都会带上这些特殊信息,服务器收到后,会解析Cookie生成与客户端相对应的内容
    • 服务器端解决方案:Session是服务器使用的类似于散列表的结构保存信息,当程序需要为某个客户端的请求创建session的时候,服务器首先检查这个请求是否已包含了一个session id,如果已经包含,说明之前已经为该客户端创建过session,服务器就按session id将这个session检索出来使用,不包含session id则为此客户端创建session,并且生成与此session相关的session id,响应时会发给客户端进行保存
    • 区别
    1. Cookie数据存放在本地的浏览器上,Session数据存放在服务器上
    2. Cookie不是很安全,别人可以分析存放在本地的Cookie,并进行Cookie欺骗
    3. Session会在一定时间内保存在服务器上,当访问增多会影响服务器的性能
  • HTTP1.0, HTTP1.1和 HTTP2.0的区别

    1.1相较1.0最明显的区别是引入了长连接技术
  • SSL(安全套接层):

    • 为网络通信提供安全及数据完整性的一种安全协议

    • 位于TCP与各个应用层协议之间,是操作系统对外的API

    • 采用身份认证数据加密保证网络通信的安全及数据完整性

  • HTTP安全吗?说说HTTPS的改变

  1. HTTP:客户端和服务器没有任何身份确认的过程,数据全部明文传输,容易被黑客截获,黑客可冒充服务器,返回任意信息给客户端,而不被客户端察觉

  2. HTTPS(超文本传输安全协议):以计算机网络安全通信为目的的传输协议,在HTTP下加入了SSL层,从而具有了保护数据隐私、完整性和提供对网站服务器身份认证的功能。

    数据加密(给数据裹上一层外套)
    1. 数字签名:在信息的后面加上一段内容(经过哈希后的值),可以证明信息没有被修改过

    2. 哈希算法:将任意长度的信息转化为固定长度的值,算法不可逆,常见:MD5算法

    3. 非对称加密:加密时用的秘钥(公钥)和解密使用的秘钥(私钥)是不相同的,私钥是保密的

    4. 对称加密:加密和解密使用同一个秘钥

        HTTPS数据传输流程(数据传输之前,web浏览器会与服务器进行一次握手,已确定双方的加密密码信息)

  1. web浏览器将生成的随机密码和支持的加密算法信息发松给服务器
  2. 服务器选择一套浏览器支持的加密算法,以证书的形式发送给web浏览器
  3. web浏览器验证证书的合法性,并使用证书中的公钥将信息加密后发送给服务器
  4. 服务器使用网站本身的私钥将web浏览器发送的信息解密确定密码,验证哈希是否一致,然后服务器会使用密码加密握手信息发送给浏览器
  5. web浏览器解密响应信息,并计算经过哈希算法加密的握手消息,如果与服务器发送过来的哈希值一致,则此过程结束后,浏览器会使用之前生成的随机密码和对称加密算法进行加密,交互数据。
  • HTTP和HTTPS之间的区别

  1. HTTPS需要到CA申请证书,HTTP不需要
  2. HTTPS是密文传输的,HTTP是明文传输的
  3. 连接方式不同,HTTPS默认使用443端口,HTTP使用80端口
  4. HTTPS=HTTP+加密+认证+完整性保护,较HTTP安全
  • DNS域名解析原理

    • DNS系统:将主机名转换为对应的IP地址,运行在UDP协议之上
    • 层次:根域名服务器(知道所有顶级域名服务器的IP地址)==>顶级域名服务器==>权限域名服务器(总是能够将其管辖的主机名转换为IP地址)==>本地域名服务器(主机发出DNS查询请求时,请求报文首先发给本地域名服务器)
    • 查询顺序:本地域名服务器==>根域名服务器==>顶级域名服务器==>权限域名服务器
      • 递归查询:我帮你去下一级域名服务器上查找
      • 迭代查询:我告诉你下一级域名服务器在哪,你自己去找
  • 每次DNS域名解析都要请求DNS服务器,是不是很耗时?怎么解决

    • 在域名服务器中广泛使用高速缓存:DNS服务器可以将自己查询到的结果信息缓存到高速缓存中。当下次有另一个星通的域名查询到达该服务器时,该服务器能够直接提供所需要的IP地址,而不需要去其他的DNS服务器上查询了。

    • 浏览器缓存==>系统缓存==>路由器缓存==>本地域名服务器缓存==>根域名服务器缓存==>顶级域名服务器缓存

  • ARP协议原理

    • 无论网络层使用什么协议,在输数据链路层传输数据帧都需要使用硬件地址(MAC地址),所以需要一种方法完成IP地址到MAC地址的映射,即地址解析协议ARP
    • ARP工作在网络层,因为他能够看到IP地址
    • 每个主机都设有一个ARP高速缓存,存放本局域网上各主机和路由器的IP地址到MAC地址的映射表,称为ARP表
    • 原理:当主机A欲向本局域网上的某个主机B发送IP数据报时,就先在其ARP高速缓存中查看有无主机B的IP地址。如果有,就可查出其对应的MAC地址,再将此MAC地址写入MAC帧,然后通过局域网将该MAC帧发送到此MAC地址;如果没有,就通过使用目的地址为FF-FF-FF-FF-FF-FF的帧来封装并广播ARP请求分组,可以使得同一个局域网内的所有主机都收到ARP请求。当主机B收到该ARP请求后,就会向主机A发出响应ARP分组,分组中包含主机B的IP地址与MAC地址的映射关系,主机A在收到后就将此映射写入ARP缓存中,然后按查询到的硬件地址发送MAC帧。
  • 使用Socket实现一个WebServer

  • 用过网络编程吧?用过,说说select和epoll的原理和区别

6.数据库

  • mysql查询语句怎么做性能分析

  • 为什么用B+树不用B树

  • 数据库索引有哪些优点和缺点,mysql数据库索引是通过什么实现的,相比B+树索引数据结构,有没有更快的索引数据结构,为什么不用更快的数据结构,在哪些情况下不适合创建索引,红黑树的时间复杂度是多少。

  • mysql的事务管理,如何设计一个事务系统

  • 数据库引擎有哪些?说说MYISAM和InnoDB的区别

  • Linux ps命令,以及看内存当前使用状态的命令

  • 数据库设计的三范式,可以违反三范式吗

  • Redis(分布式缓存,多个实例共用一份缓存数据)

简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。

事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

  • Redis使用哪些数据结构?

  • string(字符串)
    • string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。
    • string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。
    • string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。
  • hash(哈希)--map
    • Redis hash 是一个键值(key=>value)对集合。
    • Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
    • 每个 hash 可以存储 232 -1 键值对(40多亿)
  • list(双向列表)
    • Redis list是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
    • 支持范围查询:lrange
    • 列表最多可存储 232 - 1 元素 (4294967295, 每个列表可存储40多亿)
  • set(集合)
    • Redis 的 Set 是 string 类型的无序集合。
    • 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
    • 集合中最大的成员数为 232 - 1(4294967295, 每个集合可存储40多亿个成员)。
  • zset(sorted set:有序集合)
    • Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。
    • 不同的是每个元素都会关联一个double类型的score。redis正是通过分数来为集合中的成员进行从小到大的排序。
    • zset的成员是唯一的,但分数(score)却可以重复。
  • Redis是多线程还是单线程的

    • 单线程
  • Redis常用命令

  • redis 和 memcached 的区别

    • redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
    • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
    • 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
    • Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
  • redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)

    • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
    • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
    • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
    • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
    • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
    • no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
    • volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
    • allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key
  • redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)

    • 快照(snapshotting)持久化(RDB):将内存中某一时刻的数据状态进行备份
    • AOF(append-only file)持久化:每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件

7.Linux相关

  • Linux中远程传输文件有什么方式

  • linux查看负载的命令:ps,top

  • Linux ps命令,以及看内存当前使用状态的命令

    1.ps:探查进程,只能显示某个特定时间点的信息
    • 默认,ps命令只会显示运行在当前控制台下的属于当前用户的进程,PID,TTY(启动进程的终端),TIME(进程已用CPU时间)

  •           可选参数:
    • -e:显示所有进程
    • -f:显示完整格式的输出
    • -l:显示长列表

C列:进程生命周期中的CPU利用率 

2.top:实时监测进程,实时显示进程信息

 

 第1行:当前时间,系统运行时间,登录用户数量,系统的平均负载(最近1分钟,5分钟,15分钟)

第2行:进程总数,running状态的进程数,sleeping状态下的进程数,stopped状态下的进程数,zombie(僵化:完成了,但父进程没有响应)

第3行:CPU的利用率(详细:计算linux服务器CPU利用率性能分析Linux服务器CPU利用率

CPU时间包含User time、System time、Nice time、Idle time、Waiting time、Hardirq time、Softirq time、Steal time

空闲态时间==idle time

用户态时间==user time+ Nice time。

内核态时间==system time+ Hardirq time+ Softirq time。

user time。指CPU在用户态执行进程的时间。

system time。指CPU在内核运行的时间。

nice time。指系统花费在调整进程优先级上的时间。

idle time。系统处于空闲期,等待进程运行。

waiting time。指CPU花费在等待I/O操作上的总时间,与ed相似。

steal time。指当前CPU被强制(involuntary wait )等待另外虚拟的CPU处理完毕时花费的时间,此时 hypervisor 在为另一个虚拟处理器服务。 

Softirq time 、Hardirq time。分别对应系统在处理软硬中断时候所花费的CPU时间。

第4行:系统的物理内存:内存总容量,已使用大小,空闲大小,缓冲区大小

第5行:系统的交换空间:总容量,已使用大小,空闲大小,缓存大小

 

 PR列:进程的优先级

VIRT列:进程占用的虚拟内存总量

RES列:进程占用的物理内存总量

%CPU:进程是用的CPU时间比例

%MEM:进程是用的内存占可用内存的比例

键入f:选择对输出进行排序的字段

键入d:修改轮训间隔

减去q:退出

  • swap了解吗?真正内存包括缓存和cache吗

Swap分区在系统的物理内存(这里应该是运行内存)不够用的时候,把物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间被临时保存到Swap分区中,等到那些程序要运行时,再从Swap分区中恢复保存的数据到内存中。

  • top命令你注意哪个指标?(见上上一题)

8.框架

  • MyBatis的底层实现

  • Spring底层实现

  • Spring中@autoware和@resource的区别

  • SpringMVC底层实现

9.分布式

  • 分布式一致性协议

  • paxos算法

  • 分布式事务

  • 说下3pc是什么,和2pc相比有什么不一样


转载:https://blog.csdn.net/gefeng1209/article/details/101556489
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场