java面试总结

Java基础

一、Hashmap是怎么实现的,底层原理?

HashMap的底层使用数组+链表/红黑树实现。html

transient Node<K,V>[] table;这表示HashMap是Node数组构成,其中Node类的实现以下,能够看出这其实就是个链表,链表的每一个结点是一个<K,V>映射。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

HashMap的每一个下标都存放了一条链表。java

常量/变量定义node

 1 /* 常量定义 */
 2 
 3 // 初始容量为16
 4 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 5 // 最大容量
 6 static final int MAXIMUM_CAPACITY = 1 << 30;
 7 // 负载因子,当键值对个数达到DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR会触发resize扩容 
 8 static final float DEFAULT_LOAD_FACTOR = 0.75f;
 9 // 当链表长度大于8,且数组长度大于MIN_TREEIFY_CAPACITY,就会转为红黑树
10 static final int TREEIFY_THRESHOLD = 8;
11 // 当resize时候发现链表长度小于6时,从红黑树退化为链表
12 static final int UNTREEIFY_THRESHOLD = 6;
13 // 在要将链表转为红黑树以前,再进行一次判断,若数组容量小于该值,则用resize扩容,放弃转为红黑树
14 // 主要是为了在创建Map的初期,放置过多键值对进入同一个数组下标中,而致使没必要要的链表->红黑树的转化,此时扩容便可,可有效减小冲突
15 static final int MIN_TREEIFY_CAPACITY = 64;
16 
17 /* 变量定义 */
18 
19 // 键值对的个数
20 transient int size;
21 // 键值对的个数大于该值时候,会触发扩容
22 int threshold;
23 // 非线程安全的集合类中几乎都有这个变量的影子,每次结构被修改都会更新该值,表示被修改的次数
24 transient int modCount;

关于modCount的做用见这篇blogmysql

在一个迭代器初始的时候会赋予它调用这个迭代器的对象的modCount,若是在迭代器遍历的过程当中,一旦发现这个对象的modCount和迭代器中存储的modCount不同那就抛异常。
Fail-Fast机制:java.util.HashMap不是线程安全的,所以若是在使用迭代器的过程当中有其余线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。这一策略在源码中的实现是经过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增长这个值,那么在迭代器初始化过程当中会将这个值赋给迭代器的expectedModCount。在迭代过程当中,判断modCount跟expectedModCount是否相等,若是不相等就表示已经有其余线程修改了Map。程序员

注意初始容量和扩容后的容量都必须是2的次幂,为何呢?算法

hash方法spring

先看散列方法sql

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap的散列方法如上,其实就是将hash值的高16位和低16位异或,咱们将立刻看到hash在与n - 1相与的时候,高位的信息也被考虑了,能使碰撞的几率减少,散列得更均匀。数据库

在JDK 8中,HashMap的putVal方法中有这么一句编程

 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); 

关键就是这句(n - 1) & hash,这行代码是把待插入的结点散列到数组中某个下标中,其中hash就是经过上面的方法的获得的,为待插入Node的key的hash值,n是table的容量即table.length,2的次幂用二进制表示的话,只有最高位为1,其他为都是0。减去1,恰好就反了过来。好比16的二进制表示为10000,减去1后的二进制表示为01111,除了最高位其他各位都是1,保证了在相与时,能够使得散列值分布得更均匀(由于若是某位为0好比1011,那么结点永远不会被散列到1111这个位置),且当n为2的次幂时候有(n - 1) & hash == hash % n, 举个例子,好比hash等于6时候,01111和00110相与就是00110,hash等于16时,相与就等于0了,多举几个例子即可以验证这一结论。最后来回答为何HashMap的容量要始终保持2的次幂

  • 使散列值分布均匀
  • 位运算的效率比取余的效率高

注意table.length是数组的容量,而transient int size表示存入Map中的键值对数。

int threshold表示临界值,当键值对的个数大于临界值,就会扩容。threshold的更新是由下面的方法完成的。

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;
}

该方法返回大于等于cap的最小的二次幂数值。好比cap为16,就返回16,cap为17就返回32。

put方法

put方法主要由putVal方法实现:

  • 若是没有产生hash冲突,直接在数组tab[i = (n - 1) & hash]处新建一个结点;
  • 不然,发生了hash冲突,此时key若是和头结点的key相同,找到要更新的结点,直接跳到最后去更新值
  • 不然,若是数组下标中的类型是TreeNode,就插入到红黑树中
  • 若是只是普通的链表,就在链表中查找,找到key相同的结点就跳出,到最后去更新值;到链表尾也没有找到就在尾部插入一个新结点。接着判断此时链表长度若大于8的话,还须要将链表转为红黑树(注意在要将链表转为红黑树以前,再进行一次判断,若数组容量小于64,则用resize扩容,放弃转为红黑树)

get方法

get方法由getNode方法实现:

  • 若是在数组下标的链表头就找到key相同的,那么返回链表头的值
  • 不然若是数组下标处的类型是TreeNode,就在红黑树中查找
  • 不然就是在普通链表中查找了
  • 都找不到就返回null

remove方法的流程大体和get方法相似。

HashMap的扩容,resize()过程?

newCap = oldCap << 1

resize方法中有这么一句,说明是扩容后数组大小是原数组的两倍。

Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    // 若是数组中只有一个元素,即只有一个头结点,从新哈希到新数组的某个下标
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        // 数组下标处的链表长度大于1,非红黑树的状况
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            // oldCap是2的次幂,最高位是1,其他为是0,哈希值和其相与,根据哈希值的最高位是1仍是0,链表被拆分红两条,哈希值最高位是0分到loHead。
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            // 哈希值最高位是1分到hiHead
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            // loHead挂到新数组[原下标]处;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            // hiHead挂到新数组中[原下标+oldCap]处
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;

举个例子,好比oldCap是16,二进制表示是10000,hash值的后五位和oldCap相与,由于oldCap的最高位(从右往左数的第5位)是1其他位是0,所以hash值的该位是0的全部元素被分到一条链表,挂到新数组中原下标处,hash值该位为1的被分到另一条链表,挂到新数组中原下标+oldCap处。举个例子:桶0中的元素其hash值后五位是0XXXX的就被分到桶0种,其hash值后五位是1XXXX就被分到桶4中。

二、Java中的错误和异常?

Java中的全部异常都是Throwable的子类对象,Error类和Exception类是Throwable类的两个直接子类。

Error:包括一些严重的、程序不能处理的系统错误类。这些错误通常不是程序形成的,好比StackOverflowError和OutOfMemoryError。

Exception:异常分为运行时异常和检查型异常。

  • 检查型异常要求必须对异常进行处理,要么往上抛,要么try-catch捕获,否则不能经过编译。这类异常比较常见的是IOException。
  • 运行时异常,可处理可不处理,在编译时能够经过,异常在运行时才暴露。好比数组下标越界,除0异常等。

三、Java的集合类框架介绍一下?

首先接口Collection和Map是平级的,Map没有实现Collection。

Map的实现类常见有HashMap、TreeMap、LinkedHashMap和HashTable等。其中HashMap使用散列法实现,低层是数组,采用链地址法解决哈希冲突,每一个数组的下标都是一条链表,当长度超过8时,转换成红黑树。TreeMap使用红黑树实现,能够按照键进行排序。LinkedHashMap的实现综合了HashMap和双向链表,可保证以插入时的顺序(或访问顺序,LRU的实现)进行迭代。HashTable和HashMap比,前者是线程安全的,后者不是线程安全的。HashTable的键或者值不容许null,HashMap容许。

Collection的实现类常见的有List、Set和Queue。List的实现类有ArrayList和LinkedList以及Vector等,ArrayList就是一个可扩容的对象数组,LinkedList是一个双向链表。Vector是线程安全的(ArrayList不是线程安全的)。Set的里的元素不可重复,实现类常见的有HashSet、TreeSet、LinkedHashSet等,HashSet的实现基于HashMap,实际上就是HashMap中的Key,一样TreeSet低层由TreeMap实现,LinkedHashSet低层由LinkedHashMap实现。Queue的实现类有LinkedList,能够用做栈、队列和双向队列,另外还有PriorityQueue是基于堆的优先队列。

四、Java反射是什么?为何要用反射,有什么好处,哪些地方用到了反射?

反射:容许任意一个类在运行时获取自身的类信息,而且能够操做这个类的方法和属性。这种动态获取类信息和动态调用对象方法的功能称为Java的反射机制。

反射的核心是JVM在运行时才动态加载类或调用方法/访问属性。它不须要事先(写代码的时候或编译期)知道运行对象是谁,如Class.ForName()根本就没有指定某个特定的类,彻底由你传入的类全限定名决定,而经过new的方式你是知道运行时对象是哪一个类的。 反射避免了将程序“写死”。

反射能够下降程序耦合性,提升程序的灵活性。new是形成紧耦合的一大缘由。好比下面的工厂方法中,根据水果类型决定返回哪个类。

public class FruitFactory {
    public Fruit getFruit(String type) {
        Fruit fruit = null;
        if ("Apple".equals(type)) {
            fruit = new Apple();
        } else if ("Banana".equals(type)) {
            fruit = new Banana();
        } else if ("Orange".equals(type)) {
            fruit = new Orange();
        }
        return fruit;
    }
}

class Fruit {}
class Banana extends Fruit {}
class Orange extends Fruit {}
class Apple extends Fruit {}

可是咱们事先并不知道以后会有哪些类,好比新增了Mango,就须要在if-else中新增;若是之后不须要Banana了就须要从if-else中删除。这就是说只要子类变更了,咱们必须在工厂类进行修改,而后再编译。若是用反射呢?

public class FruitFactory {
    public Fruit getFruit(String type) {
        Fruit fruit = null;
        try {
            fruit = (Fruit) Class.forName(type).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return fruit;
    }
}

class Fruit {}
class Banana extends Fruit {}
class Orange extends Fruit {}
class Apple extends Fruit {}

若是再将子类的全限定名存放在配置文件中。

class-type=com.fruit.Apple

那么无论新增多少子类,根据不一样的场景只需修改文件就行了,上面的代码无需修改代码、从新编译,就能正确运行。

哪些地方用到了反射?举几个例子

  • 加载数据库驱动时
  • Spring的IOC容器,根据XML配置文件中的类全限定名动态加载类
  • 工厂方法模式中(如上)

五、说说你对面向对象、封装、继承、多态的理解?

  • 封装:隐藏实现细节,明确标识出容许外部使用的全部成员函数和数据项。 防止代码或数据被破坏。
  • 继承:子类继承父类,拥有父类的全部功能,而且能够在父类基础上进行扩展。实现了代码重用。子类和父类是兼容的,外部调用者无需关注二者的区别。
  • 多态:一个接口有多个子类或实现类,在运行期间(而非编译期间)才决定所引用的对象的实际类型,再根据其实际的类型调用其对应的方法,也就是“动态绑定”。

Java实现多态有三个必要条件:继承、重写、向上转型。

  • 继承:子类继承或者实行父类
  • 重写:在子类里面重写从父类继承下来的方法
  • 向上转型:父类引用指向子类对象
    public class OOP {
        public static void main(String[] args) {
            /*
             * 1. Cat继承了Animal
             * 2. Cat重写了Animal的eat方法
             * 3. 父类Animal的引用指向了子类Cat。
             * 在编译期间其静态类型为Animal;在运行期间其实际类型为Cat,所以animal.eat()将选择Cat的eat方法而不是其余子类的eat方法
             */
            Animal animal = new Cat();
            printEating(animal);
        }
    
        public static void printEating(Animal animal) {
            animal.eat();
        }
    }
    
    abstract class Animal {
        abstract void eat();
    }
    class Cat extends Animal {
        @Override
        void eat() {
            System.out.println("Cat eating...");
        }
    }
    class Dog extends Animal {
        @Override
        void eat() {
            System.out.println("Dog eating...");
        }
    }

    六、实现不可变对象的策略?好比JDK中的String类。

    • 不提供setter方法(包括修改字段、字段引用到的的对象等方法)
    • 将全部字段设置为final、private
    • 将类修饰为final,不容许子类继承、重写方法。能够将构造函数设为private,经过工厂方法建立。
    • 若是类的字段是对可变对象的引用,不容许修改被引用对象。 1)不提供修改可变对象的方法;2)不共享对可变对象的引用。对于外部传入的可变对象,不保存该引用。如要保存能够保存其复制后的副本;对于内部可变对象,不要返回对象自己,而是返回其复制后的副本。

    七、Java序列话中若是有些字段不想进行序列化,怎么办?

    对于不想进行序列化的变量,使用transient关键字修饰。功能是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。

    八、==和equals的区别?

    == 对于基本类型,比较值是否相等,对于对象,比较的是两个对象的地址是否相同,便是否是指相同一个对象。

    equals的默认实现实际上使用了==来比较两个对象是否相等,可是像Integer、String这些类对equals方法进行了重写,比较的是两个对象的内容是否相等。

    对于Integer,若是依然坚持使用==来比较,有一些要注意的地方。对于[-128,127]区间里的数,有一个缓存。所以

  • Integer a = 127;
    Integer b = 127;
    System.out.println(a == b); // true
    
    Integer a = 128;
    Integer b = 128;
    System.out.println(a == b); // false
    
    // 不过采用new的方式,a在堆中,这里打印false
    Integer a = new Integer(127);
    Integer b = 127;
    System.out.println(a == b);

    对于String,由于它有一个常量池。因此

  • String a = "gg" + "rr";
    String b = "ggrr";
    System.out.println(a == b); // true
    
    // 固然牵涉到new的话,该对象就在堆上建立了,因此这里打印false
    String a = "gg" + "rr";
    String b = new String("ggrr");
    System.out.println(a == b);

    九、接口和抽象类的区别?

    • Java不能多继承,一个类只能继承一个抽象类;可是能够实现多个接口。
    • 继承抽象类是一种IS-A的关系,实现接口是一种LIKE-A的关系。
    • 继承抽象类能够实现对父类代码的复用,也能够重写抽象方法实现子类特有的功能。实现接口能够为类新增额外的功能。
    • 抽象类定义基本的共性内容,接口是定义额外的功能。
    • 调用者使用动机不一样, 实现接口是为了使用他规范的某一个行为;继承抽象类是为了使用这个类属性和行为.

    十、给你一个Person对象p,如何将该对象变成JSON表示?

    本质是考察Java反射,由于要实现一个通用的程序。实现可能根本不知道该类有哪些字段,因此不能经过get和set等方法来获取键-值。使用反射的getDeclaredFields()能够得到其声明的字段。若是字段是private的,须要调用该字段的f.setAccessible(true);,才能读取和修改该字段。

  • import java.lang.reflect.Field;
    import java.util.HashMap;
    
    public class Object2Json {
        public static class Person {
            private int age;
            private String name;
    
            public Person(int age, String name) {
                this.age = age;
                this.name = name;
            }
        }
    
        public static void main(String[] args) throws IllegalAccessException {
            Person p = new Person(18, "Bob");
            Class<?> classPerson = p.getClass();
            Field[] fields = classPerson.getDeclaredFields();
            HashMap<String, String> map = new HashMap<>();
            for (Field f: fields) {
                // 对于private字段要先设置accessible为true
                f.setAccessible(true);
                map.put(String.valueOf(f.getName()), String.valueOf(f.get(p)));
            }
            System.out.println(map);
        }
    }

    获得了map,再弄成JSON标准格式就行了。

    十一、JDBC中sql查询的完整过程?操做事务呢?

  • @Test
    public void fun2() throws SQLException, ClassNotFoundException {
        // 1. 注册驱动
        Class.forName("com.mysql.jdbc.Driver");
        String url = "jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf-8";
        // 2.创建链接
        Connection connection = DriverManager.getConnection(url, "root", "admin");
        // 3. 执行sql语句使用的Statement或者PreparedStatment
        Statement statement = connection.createStatement();
        String sql = "select * from stu;";
        ResultSet resultSet = statement.executeQuery(sql);
    
        while (resultSet.next()) {
            // 第一列是id,因此从第二行开始
            String name = resultSet.getString(2); // 能够传入列的索引,1表明第一行,索引不是从0开始
            int age = resultSet.getInt(3);
            String gender = resultSet.getString(4);
            System.out.println("学生姓名:" + name + " | 年龄:" + age + " | 性别:" + gender);
        }
        // 关闭结果集
        resultSet.close();
        // 关闭statemenet
        statement.close();
        // 关闭链接
        connection.close();
    }

    ResultSet维持一个指向当前行记录的cursor(游标)指针

    • 注册驱动
    • 创建链接
    • 准备sql语句
    • 执行sql语句获得结果集
    • 对结果集进行遍历
    • 关闭结果集(ResultSet)
    • 关闭statement
    • 关闭链接(connection)

    因为JDBC默认自动提交事务,每执行一个update ,delete或者insert的时候都会自动提交到数据库,没法回滚事务。因此若须要实现事务的回滚,要指定setAutoCommit(false)

    • true:sql命令的提交(commit)由驱动程序负责
    • false:sql命令的提交由应用程序负责,程序必须调用commit或者rollback方法

    JDBC操做事务的格式以下,在捕获异常中进行事务的回滚。

  • 十二、实现单例,有哪些要注意的地方?

    就普通的实现方法来看。

    • 不容许在其余类中直接new出对象,故构造方法私有化
    • 在本类中建立惟一一个static实例对象
    • 定义一个public static方法,返回该实例
    • public class SingletonImp {
          // 饿汉模式
          private static SingletonImp singletonImp = new SingletonImp();
          // 私有化(private)该类的构造函数
          private SingletonImp() {
          }
      
          public static SingletonImp getInstance() {
              return singletonImp;
          }
      }

      饿汉模式:线程安全,不能延迟加载

    • public class SingletonImp4 {
          private static volatile SingletonImp4 singletonImp4;
      
          private SingletonImp4() {}
      
          public static SingletonImp4 getInstance() {
              if (singletonImp4 == null) {
                  synchronized (SingletonImp4.class) {
                      if (singletonImp4 == null) {
                          singletonImp4 = new SingletonImp4();
                      }
                  }
              }
      
              return singletonImp4;
          }
      }

      双重检测锁+volatile禁止语义重排。由于singletonImp4 = new SingletonImp4();不是原子操做。

    • public class SingletonImp6 {
          private SingletonImp6() {}
      
          // 专门用于建立Singleton的静态类
          private static class Nested {
              private static SingletonImp6 singletonImp6 = new SingletonImp6();
          }
      
          public static SingletonImp6 getInstance() {
              return Nested.singletonImp6;
          }
      }

      静态内部类,能够实现延迟加载。

      最推荐的是单一元素枚举实现单例。

      • 写法简单
      • 枚举实例的建立默认就是线程安全的
      • 提供了自由的序列化机制。面对复杂的序列或反射攻击,也能保证是单例
        public enum Singleton {
            INSTANCE;
            public void anyOtherMethod() {}
        }

        数据结构与算法

        一、二叉树的遍历方式?它们属于深搜仍是广搜?

        • 先序遍历。父结点 -> 左子结点 -> 右子结点
        • 中序遍历。左子结点 -> 父结点 -> 右子结点
        • 后序遍历。左子结点 -> 右子结点 -> 父结点
        • 层序遍历。一层一层自上而下,从左往右访问。

        其中,先序、中序、后序遍历属于深度优先搜索(DFS),层序遍历属于广度优先搜索(BFS)

        二、什么是平衡二叉树,它的好处是什么?被应用在哪些场景中?

        平衡二叉树首先是一棵二叉查找树,其次它须要知足其左右两棵子树的高度之差不超过1,且子树也必须是平衡二叉树,换句话说对于平衡二叉树的每一个结点,要求其左右子树高度之差都不超过1。

        二叉查找树在最坏状况下,退化成链表,查找时间从平均O(lg n)降到O(n),平衡二叉树使树的结构更加平衡,提升了查找的效率;可是因为插入和删除后须要从新恢复树的平衡,因此插入和删除会慢一些。

        应用场景好比在HashMap中用到了红黑树(平衡二叉树的特例),数据库索引中的B+树等。

        三、数组和链表的区别?

        • 数组是一段连续的内存空间,因此支持随机访问,能够经过下标以O(1)的时间进行查找。链表中的元素在内存中不是连续的,能够分散在内存中各个地方。所以它不支持随机访问,查找效率是O(n)
        • 数组的插入和删除元素时间是O(n),插入和删除后要移动元素;而链表只需断开结点连接再从新连上,因此链表的删除和插入时间是O(1)
        • 数组必须指定初始大小,达到最大容量若是不扩容就不能再存入新的元素;而链表没有容量的限制

        应用场景:数组适合读多写少、事先知道元素大概个数的状况;链表适合写多读少的状况。

        四、冒泡和快排的区别,最差和平均的时间复杂度?

        • 冒泡:相邻元素进行两两比较,将最大值推进到最右边。重复以上过程。时间复杂度平均O(N^2)最差O(N^2),空间复杂度O(1)
        • 快排:选择数组中第一个元素做为基准,从左边向右找到第一个大于等于基准的元素,从右边向左找到第一个小于等于基准的元素,交换这两个元素,最后基准左边的元素都小于等于基准,基准右边的元素都大于等于基准。而后固定基准元素,对其左边和右边采起一样的作法。典型的分治思想。时间复杂度平均O(N lgN)最差O(N^2),基于递归的实现因为用到了系统栈,因此平均状况下空间复杂度为O(lgN)
        • 冒泡排序是稳定的排序算法,快速排序不是稳定的排序算法。

        排序中所说的稳定是指,对于两个相同的元素,在排序后其相对位置没有发生变化。

        常见的稳定排序有,冒泡、插入、归并、基数排序。选择、希尔、快排、堆排序都不是稳定的。

        五、说说经常使用的散列方法?解决哈希冲突的方法有哪些?

        • 最经常使用的除留余数法(取余),大小为M的数组,key的哈希值为k,那么k % M的值必定是落在0-M-1之间的。
        • 直接定址法。用一个函数对key做映射获得哈希值。如线性函数:hash(key) = a * key + b
        • 其余(略)

        解决哈希冲突的方法:

        • 开放定址法:采用M大小的数组存放N个键值对,其中M > N。开放定址中最简单的是线性探测法,当发生碰撞时,直接检查散列表中的下一个位置。若是到了数组末尾,折回到索引0处继续查找。
        • 链地址法:采用链表数组实现,当发生哈希冲突时,将冲突键值以头插或者尾插的方式插入数组下标所在的链表,HashMap中正是使用了这种方法。
        • 再哈希法:当发生哈希冲突时,换一个散列函数从新计算哈希值。
        • 公共溢出区法:创建一个基本表和溢出表,全部冲突的键值都存放到溢出表中。在查找时,先在基本表中查,相等,查找成功,如不相等则去溢出表中进行顺序查找。

        六、堆排序的过程说一下?

        堆排序使用了最大堆/最小堆,拿数组升序排序来讲,须要创建一个最大堆,基于数组实现的二叉堆能够看做一棵彻底二叉树,其知足堆中每一个父结点它左右两个结点值都大,且堆顶的元素最大。

        • 将堆顶元素和数组最后一个元素交换,最大元素被交换数组最后一个位置,同时从堆中删除原来处于堆顶的最大元素
        • 被交换到堆顶的元素通常会打破堆结构的定义,所以须要进行堆的调整(下沉)。将堆顶的元素和其左右两个结点比较,将三者中的最大的交换到堆顶,而后继续跟踪此结点,循环上述过程,直到他比左右两个结点都大时中止调整,此时堆调整完毕,再次知足堆结构的定义
        • 重复以上两个过程。直到堆中只剩一个元素,此时排序完成

        每次调整堆的平均时间为O(lg N),所以对大小为N的数组排序,时间复杂度最差和平均都 O(N lg N).

        七、堆排序和快排应用场景的?时间复杂度和空间复杂度各是多少?

        快排序在平均状况下,比绝大多数排序算法都快些。不少编程语言的sort默认使用快排,像Java的Array.sort()就采用了双轴快速排序 。堆排序使用堆实现,空间复杂度只有O(1)。堆排序使用堆的结构,能以O(1)的时间得到最大/最小值,在处理TOP K问题时很方便,另外堆还能够实现优先队列。

        时间复杂度:

        • 快排,平均O(N lg N) ,最差O(N^2),这种状况发生在数组自己就有序,这样每次切分元素都是数组中的最小值,切分得就极为不平衡。
        • 堆排序,平均和最差都是O(N lgN)。由于每次调整堆结构都是O(lg N),所以对大小为N的数组排序,时间复杂度最差和平均都 O(N lg N).

        空间复杂度:

        • 快排,通常基于递归实现,须要使用系统栈O(lg N)
        • 堆排序,额外空间复杂度O(1)
        • 放一张神图

          975503-20170214211234550-1109833343

          八、双向链表,给你Node a,在其后插入Node c?

        • c.next = a.next;
          a.next = c;
          c.prev = a;
          // 若是a不是最后一个结点,就有下面一句
          c.next.prev = c;

          计算机网络

          一、HTTP有哪些请求方法?它们的做用或者说应用场景?

          • GET: 请求指定的页面信息,并返回实体主体。
          • HEAD: 和GET相似,只不过不返回报文主体,只返回响应首部。可用于确认URI的有效性及资源更新的日期时间;
          • POST: 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会致使新的资源的创建和/或已有资源的修改。
          • PUT: 用来传输文件,要求请求报文的主体中包含文件内容,而后保存到请求URI指定的位置。
          • DELETE: 和PUT相反,按请求URI删除指定的资源。
          • OPTIONS: 用来查询针对请求URI指定的资源支持的方法。若是请求成功,会有一个Allow的头包含相似“GET,POST”这样的信息
          • TRACE: 让服务端将以前的请求通讯返回给客户端的方法(所以客户端能够得知请求是怎么一步步到服务端的)。主要用于测试或诊断。
          • CONNECT: 使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通讯内容加密后经网络隧道传输。

          二、GET和POST的对比,或者说区别?

          • GET用于获取数据,POST用于提交数据;
          • GET的参数长度有限制(不一样的浏览器和服务器限制不一样),POST没有限制
          • GET把参数包含在URL中,POST经过封装参数到请求体中发送;
          • GET请求只能进行url编码,而POST支持多种编码方式。
          • GET能够发送的参数只能是ASCII类型,POST没有限制,甚至能够传输二进制。
          • GET比POST更不安全,由于参数直接暴露在URL上,因此不能用来传递敏感信息;
          • GET刷新无害,而POST会再次提交数据
          • 还有GET请求会被保存浏览器历史记录,能够被收藏为书签,而POST请求不能等等

          GET和POST本质都是TCP链接。不过GET产生一个TCP数据包;POST产生两个TCP数据包。

          对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200 OK(返回数据);

          而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 OK(返回数据)。

          三、TCP三次握手?四次挥手?

          三次握手

          • 请求先由客户端发起。客户端发送SYN = 1和客户端序号c给服务端,同时进入SYN-SENT状态
          • 服务端收到SYN后须要作出确认,因而发送ACK = 1,同时本身也发送SYN = 一、服务端序号s,还有确认号c + 1,表示想收到的下一个序号。此时服务端进入SYN-RCVD状态
          • 客户端收到服务端的SYN和ACK,作出确认,发送ACK = 1,以及序号c +1,同时发送确认号s + 1,表示客户端想收到下一个序号。此时客户端和服务端进入ESTABLISHED状态,链接已创建!

          f8ef1381-713f-4fbe-83d7-b13a1c4b6bc9

          四次挥手

          • 关闭链接也是先有客户端发起。客户端发送FIN = 1和序号c向服务端请求断开链接。此时客户端进入FIN-WAIT-1状态
          • 服务端收到FIN后作出确认,发送ACK = 1和服务端序号,还有确认号c + 1表示想要收到的下一个序号。服务端此时还能够向客户端发送数据。此时服务端进入CLOSE-WAIT状态,客户端进入FIN-WAIT-2状态
          • 服务端没有数据发送时,它向客户端发送FIN= 一、ACK = 1请求断开链接,同时发送服务端序号s以及确认号c + 1。此时服务端进入LAST-ACK状态
          • 客户端收到后进行确认,发送ACK = 1,以及须要c + 1和确认号s + 1。此时客户端进入TIME-WAIT状态。客户端须要等待2MSL,确保服务端收到了ACK,若这期间客户端没有收到服务端的消息,即可认为服务端收到了确认,此时能够断开链接。客户端和服务端进入CLOSED状态。

          8e9432d9-e6ca-4227-a8b6-fd958df00b6b

          四、TCP为何须要三次握手?两次不行吗?

          两次握手的话,只要服务端发出确认就创建链接了。有一种状况是客户端发出了两次链接请求,但因为某种缘由,使得第一次请求被滞留了。第二次请求先到达后创建链接成功,此后第一次请求终于到达,这是一个失效的请求了,服务端觉得这是一个新的请求因而赞成创建链接,可是此时客户端不搭理服务端,服务端一直处于等待状态,这样就浪费了资源。假设采用三次握手,因为服务端还须要等待客户端的确认,若客户端没有确认,服务端就能够认为客户端没有想要创建链接的意思,因而此次链接不会生效。

          五、四次握手,为何客户端发送确认后还须要等待2MSL?

          由于第四次握手客户端发送ACK确认后,有可能丢包了,致使服务端没有收到,服务端就会再次发送FIN = 1,若是客户端不等待当即CLOSED,客户端就不能对服务端的FIN = 1进行确认。等待的目的就是为了能在服务端再次发送FIN = 1时候能进行确认。若是在2MSL内客户端都没有收到服务端的任何消息,便认为服务端收到了确认。此时能够结束TCP链接。

          六、cookie和session区别和联系?

          • Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据能够保存在集群、数据库、文件中;
          • Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
          • session的运行依赖session id,而session id是存在cookie中的,也就是说,若是浏览器禁用了cookie ,同时session也会失效(可是能够经过url重写,即在url中传递session_id)

          七、为什么使用session?session的原理?

          好比网上购物,每一个用户有本身的购物车,当点击下单时,因为HTTP协议无状态,并不知道是哪一个用户操做的,因此服务端要为特定的用户建立特定的Session,用于标识这个用户,而且跟踪用户。

          Session原理:浏览器第一次访问服务器时,服务器会响应一个cookie给浏览器。这个cookie记录的就是sessionId,以后每次访问携带着这个sessionId,服务器里查询该sessionId,即可以识别并跟踪特定的用户了。

          Cookie原理:第一次访问服务器,服务器响应时,要求浏览器记住一个信息。以后浏览器每次访问服务器时候,携带第一次记住的信息访问。至关于服务器识别客户端的一个通行证。Cookie不可跨域,浏览览器判断一个网站是否能操做另外一个网站Cookie的依据是域名。Google与Baidu的域名不同,所以Google不能操做Baidu的Cookie,换句话说Google只能操做Google的Cookie。

          八、网络的7层模型了解吗?

          即OSI参考模型。

          • 应用层。针对特定应用的协议,为应用程序提供服务。如电子邮件、远程登陆、文件传输等协议。
          • 表示层。主要负责数据格式的转换,把不一样表现形式的信息转换成适合网络传输的格式。
          • 会话层。通讯管理,负责创建和断开通讯链接。即什么时候创建链接、什么时候断开链接以及保持多久的链接。
          • 传输层。在两个通讯结点之间负责数据的传输,起着可靠传输的做用。
          • 网络层。路由选择。在多个网络之间转发数据包,负责将数据包传送到目标地址。
          • 数据链路层。负责物理层面上互联设备之间的通讯传输。例如与一个以太网相连的两个节点之间的通讯。是数据帧与一、0比特流之间的转换。
          • 物理层。主要是一、0比特流与电子信号的高低电平之间的转换。

          还有一种TCP/IP五层模型,就是把应用层、表示层、会话层统一归到应用层。借用一张图。

          20170905102825355

          九、有了传输层为何还须要网络层?或者说网络层和传输层是如何协做的?

          网络层是针对主机与主机之间的服务。而传输层针对的是不一样主机进程之间的通讯。传输层协议将应用进程的消息传送到网络层,可是它并不涉及消息是怎么在网络层之间传送(这部分是由网络层的路由选择完成的)。网络层真正负责将数据包从源IP地址转发到目标IP地址,而传输层负责将数据包再递交给主机中对应端口的进程。

          e24d3846-c2d5-4714-afed-106c7c8096bf

          打个比方。房子A中的人要向房子B中的人写信。房子中都有专门负责将主人写好的信投递到邮箱,以及从邮箱接收信件后交到主人手中的管家。那么:

          • 房子 = 主机
          • 信的内容 = 应用程序消息
          • 信封 = 数据包,带有源端口、目的端口、源IP地址、目的IP地址。
          • 邮递员 = 网络层协议,知道信从哪一个房子开始发的,以及最后要送到哪一个具体的房子。
          • 管家 = 传输层协议,负责将信投入到信箱中、以及从信箱中接收信件。知道这封信是谁写的以及要送到谁手上(具体端口号)

          以上只是我的理解,若有误请联系更正。

          十、TCP和UDP的区别?

          • TCP面向链接,传输数据以前要须要创建会话。UDP是无链接的。
          • TCP提供可靠传输,保证数据不丢包、不重复且按顺序到达;UDP只尽努力交付,不保证可靠交付
          • TCP提供了拥塞控制;UDP不提供
          • TCP是面向字节流的;UDP面向报文。
          • TCP只支持点到点通讯;UDP支持一对1、一对多、多对多的交互通讯。
          • TCP首部开销大20字节,UDP首部开销小8字节。

          十一、传输层的可靠传输指的是什么?是如何实现的?

          可靠传输是指

          • 传输的信道不产生差错
          • 保证传输数据的正确性,无差错、不丢失、不重复且按顺序到达。

          TCP如何实现可靠传输:

          • 应用数据被分割成TCP认为最适合发送的块进行传输
          • 超时重传,TCP发出一个分组后,它启动一个定时器,等接收方确认收到这个分组。若是发送方不能及时收到一个确认,将重传给接收方。
          • 序号,用于检测丢失的分组和冗余的分组。
          • 确认,告知对方已经正确收到的分组以及指望的下一个分组
          • 校验和,校验数据在传输过程当中是否发生改变,如校验有错则丢弃分组;
          • 流量控制,使用滑动窗口,发送窗口的大小由接收窗口和拥塞窗口的的大小决定(取二者中小的那个),当接收方来不及处理发送方的数据,能提示发送方下降发送的速率,防止包丢失。
          • 拥塞控制:当网络拥塞时,减小数据的发送。

          十二、主机A向主机B发送数据,在这个过程当中,传输层和网络层作了什么?

          当TCP链接创建以后,应用程序就可以使用该链接进行数据收发。应用程序将数据提交给TCP,TCP将数据放入本身的缓存,数据会被当作字节流并进行分段,而后加上TCP头部并提交给网络层。再加上IP头后被网络层提交给到目的主机,目的主机的IP层会将分组提交给TCP,TCP根据报文段的头部信息找到相应的socket,并将报文段提交给该socket,socket是和应用关联的,因而数据就提交给了应用。

          对于UDP会简单些,UDP面向报文段。传输层加上UDP头部递交给网络层,再加上IP头部经路由转发到目的主机,目的主机将分组提交给UDP,UDP根据头部信息找到相应的socket,并将报文段提交给该socket,socket是和应用关联的,因而数据就提交给了应用。

          1三、TCP序号的做用?怎么保证可靠传输的?

          序号和确认号是实现可靠传输的关键。

          • 序号:当前数据包的首个字节的顺序号。
          • 确认号:表示下一个想要接收的字节序号,同时确认号表示对发送方的一个确认回应,表示已经正确收到确认号以前的字节了。

          通讯双方经过序号和确认号,来判断数据是否丢失、是否按顺序到达、是否冗余等,以此决定要不要进行重传丢失的分组或丢弃冗余的分组。换句话说,由于有了序号、确认号和重传机制,保证了数据不丢失、不重复、有序到达。

          1四、浏览器发起HTTP请求后发生了什么?越详细越好。

          当在浏览器输入网址www.baidu.com并敲下回车后:

          • DNS域名解析,将域名www.baidu.com解析成IP地址
          • 发起TCP三次握手,创建TCP链接。浏览器以一个随机端口(1024~65535)向服务器的80端口发起TCP链接。
          • TCP链接创建后,发起HTTP请求。
          • 服务端响应HTTP请求,将html代码返回给浏览器。
          • 浏览器解析html代码,请求html中的资源
          • 浏览器对页面进行渲染呈现给用户

          964016-20160830113547246-672458721

          1五、DNS域名解析的请求过程?

          • 先在浏览器自身的DNS缓存中搜索
          • 如上述步骤未找到,浏览器搜索操做系统自己的DNS缓存
          • 若是在系统DNS缓存中未找到,则尝试读取hosts文件,寻找有没有该域名对应的IP
          • 若是hosts文件中没找到,浏览器会向本地配置的首选DNS服务器发起域名解析请求 。运营商的DNS服务器首先查找自身的缓存,若找到对应的条目且没有过时,则解析成功。若是没有找到,运营商的DNS代咱们的浏览器,以根域名->顶级域名->二级域名->三级域名这样的顺序发起迭代DNS解析请求。

          964016-20160830113557949-272537363

          1六、HTTP是基于TCP仍是UDP的?

          HTTP协议是基于TCP协议的,客户端向服务端发送一个HTTP请求时,须要先与服务端创建TCP链接(三次握手),握手成功之后才能进行数据交互。

          1七、HTTP请求和响应的报文结构(格式)?

          HTTP请求的报文格式:

          • 请求行:包括请求方法、URL、HTTP协议版本号
          • 请求头:若干键值对组成
          • 请求空行:告诉服务器请求头的键值对已经发送完毕
          • 请求主体

          20170330192653242

          HTTP响应的报文格式:

          • 响应行:HTTP协议版本号、状态码、状态码描述
          • 响应头:若干键值对表示
          • 响应空行:标识响应头的结束
          • 响应主体

          20170330192754102

          1八、HTTP常见的状态码?

          • 1XX:信息性状态码,表示接收的请求正在处理
          • 2XX:成功状态码,表示请求正常处理完毕
          • 3XX:重定向状态码,表示须要进行附加操做以完成请求
          • 4XX:客户端错误状态码,表示服务器没法处理请求
          • 5XX:服务端错误状态码,表示服务器处理请求出错

          常见的状态码有:

          • 200 OK,请求被正常处理
          • 301 Move Permanently,永久性重定向
          • 302 Found,临时性重定向
          • 400 Bad Request,请求报文中存在语法错误
          • 403 Forbidden,对请求资源的访问被服务器拒绝
          • 404 Not Found,在服务器上不能找到请求的资源
          • 500 Internal Server Error,服务器内部错误
          • synchronized做了哪些优化?

            synchronized对内置锁引入了偏向锁、轻量级锁、自旋锁、锁消除等优化。使得性能和重入锁差很少了。

            • 偏向锁:偏向锁会偏向第一个得到它的线程,若是在接下来的执行过程当中,该锁没有被其余线程获取,则持有偏向锁的线程永远也不须要再进行同步。偏向锁是在无竞争的状况下把整个同步都消除掉,CAS操做也没有了。适合于同一个线程请求同一个锁,不适用于不一样线程请求同一个锁,此时会形成偏向锁失效。
            • 轻量级锁:若是偏向锁失效,虚拟机不会当即挂起线程,会使用一种称为轻量级锁的优化手段,轻量级锁的加锁和解锁都是经过CAS操做完成的。若是线程得到轻量级锁成功,则能够顺利进入临界区。若是轻量级锁加锁失败,表示其余线程抢先获得了锁,轻量级锁将膨胀为重量级锁。
            • 自旋锁:锁膨胀后,虚拟机为了不线程真实地在操做系统层面挂起,虚拟机还会作最后的努力--自旋锁。若是共享数据的锁定状态只有很短的一段时间,为了这段时间去挂起和恢复线程(都须要转入内核态)并不值得,因此此时让后面请求锁的那个线程稍微等待如下,但不放弃处理器的执行时间。这里的等待其实就是执行了一个忙循环,这就是所谓的自旋。虚拟机会让当前线程作几个循环,若干次循环后若是获得了锁,就顺利进入临界区;若是仍是没获得,这才将线程在操做系统层面挂起。
            • 锁消除:虚拟机即时编译时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。锁消除的依据来源于“逃逸分析”技术。堆上的全部数据都不会逃逸出去被其余线程访问到,就能够把它们当栈上的数据对待,认为它们是线程私有的,同步加锁就是没有必要的。

            五、Java中线程的建立方式有哪些?

            • 继承Thread并重写run方法
            • 实现Runnable并重写run方法,而后做为参数传入Thread
            • 实现Callable,并重写call(),call方法有返回值。使用FutureTask包装Callable实现类,其中FutureTask实现了Runnable和Future接口,最后将FutureTask做为参数传入Thread中
            • 由线程池建立并管理线程。
            • 七、BIO、NIO、AIO的区别?

              首先要搞明白在I/O中的同步、异步、阻塞、非阻塞是什么意思。

              • 同步I/O。由用户进程本身处理I/O的读写,处理过程当中不能作其余事。须要主动去询问I/O状态。

              • 异步I/O。由系统内核完成I/O操做,完成后系统会通知用户进程。

              • 阻塞。I/O请求操做须要的条件不知足,请求操做一直等待,直到条件知足。

              • 非阻塞。 I/O请求操做须要的条件不知足,会当即返回一个标志,而不会一直等待。

              如今来看BIO、NIO、AIO的区别。

              BIO:同步并阻塞。用户进程在发起一个I/O请求后,必须等待I/O准备就绪,I/O操做也由本身来处理,在IO操做未完成以前,用户进程必须等待。

              NIO:同步非阻塞。用户进程发起一个I/O请求后可当即返回去作其余任务,当I/O准备就绪时它会收到通知。接着由这个线程自行进行I/O操做,I/O操做自己仍是同步的。

              AIO:异步非阻塞。用户进程发起一个I/O操做之后可当即返回去作其余任务,真正的I/O操做由内核完成后通知用户进程。

              NIO和AIO的不一样:NIO是操做系统通知用户进程I/O已经准备就绪,由用户进程自行完成I/O操做;AIO是操做系统完成I/O后通知用户进程。

              BIO是为每个客户端链接开启一个线程,简单说就是一个链接一个线程。

              NIO主要组件有Seletor、Channel、Buffer,数据须要经过BUffer包装后才能使用Channel进行读取和写入。一个Selector能够由一个线程管理,每个Channel可看做一个客户端链接。一个Selector能够监听多个Channel,即便用一个或极少数的线程来管理大量的客户端链接。当与客户端链接的数据没有准备好时,Selector处于等待状态,一旦某个Channel的准备好了数据,Selector就能当即获得通知。

              八、两个线程交替打印奇数和偶数?

              先使用synchronized实现。PrintOdd用于打印奇数;PrintEven用于打印偶数。核心就是判断当前count若是是奇数,就让PrintEven阻塞,PrintOdd打印后唤醒在lock对象上等待的PrintEven而且释放锁。此时PrintEven得到锁打印偶数再唤醒PrintOdd,两个线程如此交替唤醒对方就实现了交替打印奇偶数。

            • public class PrintOddEven {
                  private static final Object lock = new Object();
                  private static int count = 1;
              
                  static class PrintOdd implements Runnable {
                      @Override
                      public void run() {
                          for (int i = 0; i < 10; i++) {
                              synchronized (lock) {
                                  try {
                                      while ((count & 1) != 1) {
                                          lock.wait();
                                      }
                                      System.out.println(Thread.currentThread().getName() + " " +count);
                                      count++;
                                      lock.notify();
                                  } catch (InterruptedException e) {
                                      e.printStackTrace();
                                  }
                              }
                          }
                      }
                  }
              
                  static class PrintEven implements Runnable {
                      @Override
                      public void run() {
                          for (int i = 0; i < 10; i++) {
                              synchronized (lock) {
                                  try {
                                      while ((count & 1) != 0) {
                                          lock.wait();
                                      }
                                      System.out.println(Thread.currentThread().getName() + " " +count);
                                      count++;
                                      lock.notify();
                                  } catch (InterruptedException e) {
                                      e.printStackTrace();
                                  }
                              }
                          }
                      }
                  }
              
                  public static void main(String[] args) {
                      new Thread(new PrintOdd()).start();
                      new Thread(new PrintEven()).start();
                  }
              }

              1六、进程和线程的区别?

              进程是资源分配的最小单位,线程是程序执行的最小单位。 进程是线程的容器,即进程里面能够容纳多个线程,多个线程之间能够共享数据。

              1七、线程的死锁指什么?如何检测死锁?如何解决死锁?

              是指两个或两个以上的线程在执行过程当中,互相占用着对方想要的资源但都不释放,形成了互相等待,结果线程都没法向前推动。

              死锁的检测:能够采用等待图(wait-for gragh)。采用深度优先搜索的算法实现,若是图中有环路就说明存在死锁。

              解决死锁:

              • 破环锁的四个必要条件之一,能够预防死锁。
              • 加锁顺序保持一致。不一样的加锁顺序极可能致使死锁,好比哲学家问题:A先申请筷子1在申请筷子2,而B先申请筷子2在申请筷子1,最后谁也得不到一双筷子(同时拥有筷子1和筷子2)
              • 撤消或挂起进程,剥夺资源。终止参与死锁的进程,收回它们占有的资源,从而解除死锁。

              1八、CPU线程调度?

              • 协同式线程调度:线程的执行时间以及线程的切换都是由线程自己来控制,线程把本身的任务执行完后,主动通知系统切换到另外一个线程。优势是没有线程安全的问题,缺点是线程执行的时间不可控,可能由于某一个线程不让出CPU,而致使整个程序被阻塞。

              • 抢占式调度模式:线程的执行时间和切换都是由系统来分配和控制的。不过能够经过设置线程优先级,让优先级高的线程优先占用CPU。

                Java虚拟机默认采用抢占式调度模型。

              1九、HashMap在多线程下有可能出现什么问题?

              • JDK8以前,并发put下可能形成死循环。缘由是多线程下单链表的数据结构被破环,指向混乱,形成了链表成环。JDK 8中对HashMap作了大量优化,已经不存在这个问题。
              • 并发put,有可能形成键值对的丢失,若是两个线程同时读取到当前node,在链表尾部插入,先插入的线程是无效的,会被后面的线程覆盖掉。

              20、ConcurrentHashMap是如何保证线程安全的?

              JDK 7中使用的是分段锁,内部分红了16个Segment即分段,每一个分段能够看做是一个小型的HashMap,每次put只会锁定一个分段,下降了锁的粒度:

              • 首先根据key计算出一个hash值,找到对应的Segment
              • 调用Segment的lock方法(Segment继承了重入锁),锁住该段内的数据,因此并无锁住ConcurrentHashMap的所有数据
              • 根据key计算出hash值,找到Segment中数组中对应下标的链表,并将该数据放置到该链表中
              • 判断当前Segment包含元素的数量大于阈值,则Segment进行扩容(Segment的个数是不能扩容的,可是单个Segment里面的数组是能够扩容的)

              多线程put的时候,只要被加入的键值不属于 同一个分段,就能够作到真正的并行put。对不一样的Segment则无需考虑线程同步,对于同一个Segment的操做才需考虑。

              JDK 8中使用了CAS+synchronized保证线程安全,也采起了数组+链表/红黑树的结构。

              put时使用synchronized锁住了桶中链表的头结点。

              数组的扩容,被问到了我在看吧.....我只知道多个线程能够协助数据的迁移。

              有这么一个问题,ConcurrentHashMap,有三个线程,A先put触发了扩容,扩容时间很长,此时B也put会怎么样?此时C调用get方法会怎么样?C读取到的元素是旧桶中的元素仍是新桶中的

              A先触发扩容,ConcurrentHashMap迁移是在锁定旧桶的前提下进行迁移的,并无去锁定新桶。

              • 在某个桶的迁移过程当中,别的线程想要对该桶进行put操做怎么办?一旦某个桶在迁移过程当中了,必然要获取该桶的锁,因此其余线程的put操做要被阻塞。所以B被阻塞
              • 某个桶已经迁移完成(其余桶还未完成),别的线程想要对该桶进行put操做怎么办?该线程会首先检查是否还有未分配的迁移任务,若是有则先去执行迁移任务,若是没有即所有任务已经分发出去了,那么此时该线程能够直接对新的桶进行插入操做(映射到的新桶必然已经完成了迁移,因此能够放心执行操做)
              • 2一、ThreadLocal的做用和实现原理?

                对于共享变量,通常采起同步的方式保证线程安全。而ThreadLocal是为每个线程都提供了一个线程内的局部变量,每一个线程只能访问到属于它的副本。

                实现原理,下面是set和get的实现

              • // set方法
                public void set(T value) {
                   Thread t = Thread.currentThread();
                   ThreadLocalMap map = getMap(t);
                   if (map != null)
                       map.set(this, value);
                   else
                       createMap(t, value);
                }
                
                // 上面的getMap方法
                ThreadLocalMap getMap(Thread t) {
                   return t.threadLocals;
                }
                
                // get方法
                public T get() {
                   Thread t = Thread.currentThread();
                   ThreadLocalMap map = getMap(t);
                   if (map != null) {
                       ThreadLocalMap.Entry e = map.getEntry(this);
                       if (e != null) {
                           @SuppressWarnings("unchecked")
                           T result = (T)e.value;
                           return result;
                       }
                   }
                   return setInitialValue();
                }

                sleep、wait、yield的区别和联系?

                sleep() 容许指定以毫秒为单位的一段时间做为参数,它使得线程在指定的时间内进入阻塞状态,不能获得CPU 时间,指定的时间一过,线程从新进入可执行状态。调用sleep后不会释放锁。

                yield() 使得线程放弃CPU执行时间,可是不使线程阻塞,线程从运行状态进入就绪状态,随时可能再次分得 CPU 时间。有可能当某个线程调用了yield()方法暂停以后进入就绪状态,它又立刻抢占了CPU的执行权,继续执行。

                wait()是Object的方法,会使线程进入阻塞状态,和sleep不一样,wait会同时释放锁。wait/notify在调用以前必须先得到对象的锁。

                30、Thread类中的start和run方法区别?

                run方法只是一个普通方法调用,仍是在调用它的线程里执行。

                start才是开启线程的方法,run方法里面的逻辑会在新开的线程中执行。

                JVM

                一、Java内存区域(注意不是Java内存模型JMM)的划分?

                • 程序计数器。
                • 虚拟机栈。
                • 本地方法栈。
                • Java堆。
                • 方法区。

                前三个是线程私有的,后两个是线程共享的。

                字节码解释器经过改变程序计数器的值来决定下一条要执行的指令,为了在线程切换后每条线程都能正确回到上次执行的位置,由于每条线程都有本身的程序计数器。

                虚拟机栈是存放Java方法内存模型,每一个方法在执行时都会建立一个栈帧,用于存储局部变量表、操做数栈、动态连接、方法返回地址等信息。方法的开始调用对应着栈帧的进栈,方法执行完成对应这栈帧的出栈。位于栈顶被称为“当前方法”。

                本地方法栈和虚拟机栈相似,不过虚拟机栈针对Java方法,而本地方法栈针对Native方法。

                Java堆。对象实例被分配内存的地方,也是垃圾回收的主要区域。

                方法区。存放被虚拟机加载的类信息、常量(final)、静态变量(static)、即时编译期编译后的代码。方法区是用永久代实现的,这个区域的内存回收目标主要是针对常量池的回收和类型的卸载。运行时常量池是方法区的一部分,运行时常量池是Class文件中的一项信息,存放编译期生成的各类字面量和符号引用。

                二、新生代和老年代。对象如何进入老年代,新生代怎么变成老年代?

                Java堆分为新生代和老年代。在新生代又被划分为Eden区,From Sruvivor和To Survivor区,比例是8:1:1,因此新生代可用空间其实只有其容量的90%。对象优先被分配在Eden区。

                • 不过大对象好比长字符串、数组因为须要大量连续的内存空间,因此直接进入老年代。这是对象进入老年代的一种方式,
                • 还有就是长期存活的对象会进入老年代。在Eden区出生的对象通过一次Minor GC会若存活,且Survivor区容纳得下,就会进入Survivor区且对象年龄加1,当对象年龄达到必定的值,就会进入老年代。
                • 在上述状况中,若Survivor区不能容纳存活的对象,则会经过分配担保机制转移到老年代。
                • 同年龄的对象达到suivivor空间的一半,大于等于该年龄的对象会直接进入老年代。

                三、新生代的GC和老年代的GC?

                发生在新生代的GC称为Minor GC,当Eden区被占满了而又须要分配内存时,会发生一次Minor GC,通常使用复制算法,将Eden和From Survivor区中还存活的对象一块儿复制到To Survivor区中,而后一次性清理掉Eden和From Survivor中的内存,使用复制算法不会产生碎片。

                老年代的GC称为Full GC或者Major GC:

                • 当老年代的内存占满而又须要分配内存时,会发起Full GC
                • 调用System.gc()时,可能会发生Full GC,并不保证必定会执行。
                • 在Minor GC后survivor区放不下,经过担保机制进入老年代的对象比老年代的内存空间还大,会发生Full GC;
                • 在发生Minor GC以前,会先比较历次晋升到老年代的对象平均年龄,若是大于老年代的内存,也会触发Full GC。若是不容许担保失败,直接Full GC。

                四、对象在何时能够被回收,调用finalize方法后必定会被回收吗?

                在通过可达性分析后,到GC Roots不可达的对象能够被回收(但并非必定会被回收,至少要通过两次标记),此时对象被第一次标记,并进行一次判断:

                • 若是该对象没有调用过或者没有重写finalize()方法,那么在第二次标记后能够被回收了;
                • 不然,该对象会进入一个FQueue中,稍后由JVM创建的一个Finalizer线程中去执行回收,此时若对象中finalize中“自救”,即和引用链上的任意一个对象创建引用关系,到GC Roots又可达了,在第二次标记时它会被移除“即将回收”的集合;若是finalize中没有逃脱,那就面临被回收。

                所以finalize方法被调用后,对象不必定会被回收。

                五、哪些对象能够做为GC Roots?

                • 虚拟机栈中引用的对象
                • 方法区中类静态属性引用的对象(static)
                • 方法区中常量引用的对象(final)
                • 本地方法栈中引用的对象

                六、讲一讲垃圾回收算法?

                • 复制算法,通常用于新生代的垃圾回收
                • 标记清除, 通常用于老年代的垃圾回收
                • 标记整理,通常用于老年代的垃圾回收
                • 分代收集:根据对象存活周期的不一样把Java堆分为新生代和老年代。新生代中又分为Eden区、from survivor区和to survivor区,默认8:1:1,对象默认建立在Eden区,每次垃圾收集时新生代都会有大量对象死亡。此时利用复制算法将Eden区和from survivor区还存活的对象一并复制到tosurvivor区。老年代的对象存活率高,没有额外空间进行分配担保,所以采用标记-清除或者标记-整理的算法进行回收。前者会产生空间碎片,然后者不会。

                七、介绍下类加载器和类加载过程?

                先说类加载器

                在Java中,系统提供了三种类加载器。

                • 启动类加载器(Bootstrap ClassLoader),启动类加载器没法被Java程序直接引用,用户在编写自定义类加载器时,若是须要委派给启动类加载器,直接使用null。
                • 扩展类加载器(Extension ClassLoader)
                • 应用程序类加载器(Application ClassLoader),负责加载用户类路径(ClassPath)上锁指定的类库。是程序中默认的类加载器。

                固然用户也能够自定义类加载器。

                再说类加载的过程

                主要是如下几个过程:

                加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

                加载

                • 经过一个类的全限定名获取定义该类的二进制字节流
                • 将字节流表示的静态存储结构转化为方法区的运行时数据结构
                • 在内存中生成这个类的Class对象,做为方法区这个类的各类数据的访问入口

                验证

                • 文件格式验证:好比检查是否以魔数0xCAFEBABE开头
                • 元数据验证:对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。好比检查该类是否继承了被final修饰的类。
                • 字节码验证,经过数据流和控制流的分析,验证程序语义是合法的、符合逻辑的。

                准备
                为类变量(static)分配内存并设置默认值。好比static int a = 123在准备阶段的默认值是0,可是若是有final修饰,在准备阶段就会被赋值为123了。

                解析

                将常量池中的符号引用替换成直接引用的过程。包括类或接口、字段、类方法、接口方法的解析。

                初始化

                按照程序员的计划初始化类变量。如static int a = 123,在准备阶段a的值被设置为默认的0,而到了初始化阶段其值被设置为123。

                八、什么是双亲委派模型,有什么好处?如何打破双亲委派模型?

                类加载器之间知足双亲委派模型,即:除了顶层的启动类加载器外,其余全部类加载器都必需要本身的父类加载器。当一个类加载器收到类加载请求时,本身首先不会去加载这个类,而是不断把这个请求委派给父类加载器完成,所以全部的加载请求最终都传递给了顶层的启动类加载器。只有当父类没法完成这个加载请求时,子类加载器才会尝试本身去加载。

                双亲委派模型的好处?使得Java的类随着它的类加载器一块儿具有了一种带有优先级的层次关系。Java的Object类是全部类的父类,所以不管哪一个类加载器都会加载这个类,由于双亲委派模型,全部的加载请求都委派给了顶层的启动类加载器进行加载。因此Object类在任何类加载器环境中都是同一个类。

                如何打破双亲委派模型?使用OSGi能够打破。OSGI(Open Services Gateway Initiative),或者通俗点说JAVA动态模块系统。能够实现代码热替换、模块热部署。在OSGi环境下,类加载器再也不是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。

                九、说一说CMS和G1垃圾收集器?各有什么特色。

                CMS(Concurrent Mark Sweep) 从名字能够看出是能够进行并发标记-清除的垃圾收集器。针对老年代的垃圾收集器,目的是尽量地减小用户线程的停顿时间。

                收集过程有以下几个步骤:

                • 初始标记:标记从GC Roots能直接关联到的对象,会暂停用户线程
                • 并发标记:即在堆中堆对象进行可达性分析,从GC Roots开始找出存活的对象,能够和用户线程一块儿进行
                • 从新标记:修正并发标记期间因用户程序继续运做致使标记产生变更的对象的标记记录
                • 并发清除:并发清除标记阶段中肯定为不可达的对象

                CMS的缺点:

                • 因为是基于标记-清除算法,因此会产生空间碎片
                • 没法处理浮动垃圾,即在清理期间因为用户线程还在运行,还会持续产生垃圾,而这部分垃圾尚未被标记,在本次没法进行回收。
                • 对CPU资源敏感

                CMS比较相似适合用户交互的场景,能够得到较小的响应时间。

                G1(Garbage First),有以下特色:

                • 并行与并发
                • 分代收集
                • 空间整合 :总体上看是“标记-整理”算法,局部(两个Region之间 )看是复制算法。确保其不会产生空间碎片。(这是和CMS的区别之一)
                • 可预测的停顿:G1除了追求低停顿外,还能创建可预测的时间模型,主要缘由是它能够有计划地避免在整个Java堆中进行全区域的垃圾收集。

                在使用G1收集器时,Java堆的内存划分为多个大小相等的独立区域,新生代和老年代再也不是物理隔离。G1跟踪各个区域的垃圾堆积的价值大小,在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大的区域。

                G1的收集过程和CMS有些相似:

                • 初始标记:标记与GC Roots直接关联的对象,会暂停用户线程(Stop the World)
                • 并发标记:并发从GC Roots开始找出存活的对象,能够和用户线程一块儿进行
                • 最终标记:修正并发标记期间因用户程序继续运做致使标记产生变更的对象的标记记录
                • 筛选回收:清除标记阶段中肯定为不可达的对象,具体来讲对各个区域的回收价值和成本进行排序,根据用户所指望的GC停顿时间来制定回收计划。

                G1的优点:可预测的停顿;实时性较强,大幅减小了长时间的gc;必定程度的高吞吐量。

                十、CMS和G1的区别?

                由上一个问题可总结出CMS和G1的区别:

                • G1堆的内存布局和其余垃圾收集器不一样,它将整个Java堆划分红多个大小相等的独立区域(Region)。G1依然保留了分代收集,可是新生代和老年代再也不是物理隔离的,它们都属于一部分Region的集合,所以仅使用G1就能够管理整个堆。
                • CMS基于标记-清除,会产生空间碎片;G1从总体看是标记-整理,从局部(两个Region之间)看是复制算法,不会产生空间碎片。
                • G1能实现可预测的停顿。

                十一、GC必定会致使停顿吗,为何必定要停顿?任意时候均可以GC吗仍是在特定的时候?

                GC进行时必须暂停全部Java执行线程,这被称为Stop The World。为何要停顿呢?由于可达性分析过程当中不容许对象的引用关系还在变化,不然可达性分析的准确性就没法获得保证。因此须要STW以保证可达性分析的正确性。

                程序执行时并不是在全部地方都能停顿下来开始GC,只有在“安全点”才能暂停。安全点指的是:HotSpot没有为每一条指令都生成OopMap(Ordinary Object Pointer),而是在一些特定的位置记录了这些信息。这些位置就叫安全点。

                数据库

                一、数据库设计的三大范式?

                • 第一范式1NF: 数据表中的每一列(字段),必须是不可拆分的最小单元,也就是确保每一列的原子性。如订单信息列为orderInfo = "DD1024 2018.5.18",必须拆分为orderId和orderTime。
                • 第二范式2NF: 在知足第一范式的基础上,表中的全部列都必需依赖于主键(和主键有关系),其余和主键没有关系的列能够拆分出去。通俗点说就是:一个表只描述一件事情。好比order表中有orderId、orderTime、userId和userName,只有前两列依赖于订单表,后两列须要拆分到user表中。
                • 第三范式3NF: 在知足第二范式的基础上,要求数据不能有传递关系。表中的每一列都要与主键直接相关,而不是间接相关(表中的每一列只能依赖于主键)。好比order表中有orderId、orderTime、userId和userName,根据orderId能够查出userId,根据userId又能够查出userName,这就是数据的传递性,彻底能够只留下userId这一列。

                二、MySql的事务隔离级别?推荐使用哪一种?

                • 读未提交
                • 读已提交
                • 可重复读
                • 串行化

                在具体解释上面的四个隔离级别前。有必要了解事务的四大特性(ACID)

                推荐阅读这篇博客

                • 原子性(Atomicity):事务一旦开始,其后全部的操做要么都作完,要么都不作,不能被中断。如在执行过程当中出错,会回滚到事务开始前的状态。
                • 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破环。好比A向B转了钱,A扣了钱,B必须收到对应数目的钱。
                • 隔离性(Isolation):同一时间只容许一个事务请求同一个数据,其余事务不能影响当前事务,即该事务提交前对其余事务都不可见。
                • 持久性(Durability):事务完成后,事务对数据库的更新被保存到数据库,其结果是永久的。

                事务并发可能产生的问题:
                脏数据:事务对缓冲池中的行记录进行修改,可是尚未被提交。

                • 脏读:事务A读取到了事务B修改但未提交的数据。若是此时B回滚到修改以前的状态,A就读到了脏数据。
                • 不可重复读:事务A屡次读取同一个数据,此时事务B在A读取过程当中对数据修改并提交了,致使事务A在同一个事务中屡次读取同一数据而结果不一样。
                • 幻读:事务A对表进行修改,这个修改涉及到表中全部的行,但此时事务B新插入了一条数据,事务A就会发现竟然还有数据没有被修改,就好像发生幻觉同样。

                脏读是读取到事务未提交的数据,不可重复度读读取到的是提交提交后的数据,只不过在一次事务中读取结果不同。

                不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住知足条件的行,解决幻读须要锁表。

                821187-20160811171241606-133220585

                通常来讲,数据库隔离级别不同,可能出现的并发问题也不一样。级别最高的是串行化,全部问题都不会出现。可是在并发下性能极低,可重复读会只会致使幻读。

                因此通常使用MySQL默认的可重复读便可。MVCC(多版本并发控制)使用undo_log使得事务能够读取到数据的快照(某个历史版本),从而实现了可重复读。MySQL采用Next-Key Lock算法,对于索引的扫描不只是锁住扫描到的索引,还锁住了这些索引覆盖的范围,避免了不可重复读和幻读的产生。

                三、MySql数据库在什么状况下出现死锁?产生死锁的四个必要条件?如何解决死锁?

                死锁是指两个或两个以上的事务在执行过程当中,因争夺锁资源而形成的一种互相等待的现象,若无外力做用两个事务都没法推动,这样就产生了死锁。下去 死锁的四个必要条件:

                • 互斥条件:即任什么时候刻,一个资源只能被一个进程使用。其余进程必须等待。
                • 请求和保持条件:即当资源请求者在请求其余的资源的同时保持对原有资源的占有且不释放。
                • 不剥夺条件:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
                • 环路等待条件:好比A占有B在等待的资源(B等待A释放),B占有A在等待的资源(A等待B释放)。多个进程循环等待着相邻进程占用着的资源。

                避免死锁能够经过破环四个必要条件之一。

                解决死锁的方法:

                • 加锁顺序保持一致。不一样的加锁顺序极可能致使死锁,好比哲学家问题:A先申请筷子1在申请筷子2,而B先申请筷子2在申请筷子1,最后谁也得不到一双筷子(同时拥有筷子1和筷子2)
                • 超时,为其中一个事务设置等待时间,若超过这个阈值事务就回滚,另外一个等待的事务就能得以继续执行。
                • 及时检测出死锁,回滚undo量最小的事务。通常是采用等待图(wait-for gragh)。采用深度优先搜索的算法实现,若是图中有环路就说明存在死锁。

                四、如今发现sql查询很慢,如何分析哪里出了问题,应该如何优化?

                开启慢查询,查找哪些sql语句执行得慢。使用explain查看语句的执行计划,好比有没有使用到索引,是否启用了全表扫描等。查询慢,很大多是由于没有使用索引或者索引没有被命中。还有其余的缘由,好比发生了死锁,硬件、网速等缘由。

                优化手段:为相关列添加索引,而且确保索引能够被命中。优化sql语句的编写。

                五、索引的好处?

                索引是对数据库表中一个或多个列的值进行排序的结构。MySql中索引是B+树,在查找时能够利用二分查找等高效率的查找方式,以O(lg n)的时间找到。所以索引能够加快查询速度。

                六、哪些状况须要创建索引?

                • 在常常要搜索的列上
                • 常常出如今where后面的列上
                • 在做为主键的列上
                • 做为外键的列上
                • 常常须要排序、分组和联合操做的字段创建索引

                哪些状况不适合创建索引?

                • 查询中不多使用的字段
                • 数值太少的字段
                • 惟一性不太差的字段
                • 更新频繁的字段
                • 不会出如今where后的字段
                • 索引适合创建在小字段上,text和blob等大字段不适合创建索引

                七、索引的最左匹配原则了解吗?

                建了一个(a,b,c)的联合索引,那么实际等于建了(a),(a,b),(a,b,c)三个索引,可是有时在条件查询时只会匹配到a或者(a, b)而不会匹配到(a, b, c)。下面的例子

                SELECT * FROM table WHERE a = 1 AND c = 3; // 使用了索引a,c不走索引 SELECT * FROM table WHERE a = 1 AND b < 2 AND c = 3; // 使用到了索引(a,b),c不走索引

                创建联合索引(a, b ,c),因此索引是按照a -> b -> c的顺序进行排序的。a-b-c这样的索引是先找a,而后在范围里面找b,再在范围内找c。 因此上面的语句里的c 会分散在不少个b里面且不是排序的,因此没办法走索引。

                举个例子好比(a, b)联合索引,先按a排序再按b排序,获得

                (1,1)->(1, 2)->(2, 1)  (2, 4)->(3, 1)->(3, 2)

                若是执行select a from table where b=2,就没有使用到(a, b)这个联合索引,由于b的值1,2,1,4,1,2显然不是排序的。

                具体来讲:MySQL会从左开始一直向右匹配直到遇到范围查询(>,<,BETWEEN,LIKE)就中止匹配,好比: a = 1 AND b = 2 AND c > 3 AND d = 4,若是创建 (a,b,c,d)顺序的索引,使用了索引(a, b, c),可是d是没有走索引的,若是创建(a,b,d,c)的索引,则能够命中索引(a, b, c, d),其中a,b,d的顺序能够任意调整。

                等于(=)和in 能够乱序。好比,a = 1 AND b = 2 AND c = 3 创建(a,b,c)索引能够任意顺序。

                八、如何创建复合索引,能够使sql语句能尽量匹配到索引?

                • 等于条件的索引放在前面(最左),范围查询放在后面。a = 1 AND b = 2 AND c > 3 AND d = 4,创建(a, b, d, c)就是不错的选择;
                • 先过滤后排序(ORDER BY)如SELECT * FROM t WHERE c = 100 and d = 'xyz' ORDER BY b创建(c, d, b)联合索引就是不错的选择
                • 对于索引列的查询,通常不建议使用LIKE操做,像LIKE '%abc'这样的不能命中索引;不过LIKE 'abc%'能够命中索引。

                九、创建了索引,索引就必定会被命中吗?或者说索引何时失效

                • 使用了not in, <>,!=则不会命中索引。注:<>是不等号
                • innoDB引擎下,若使用OR,只有先后两个列都有索引才能命中(执行查询计划,type是index_merge),不然不会使用索引。
                • 模糊查询中,通配符在最前面时,即LIKE '%abc'这样不能命中索引
                • 对列进行函数运算的状况(如 where md5(password) = "xxxx")
                • 联合索引中,遇到范围查询时,其后的索引不会被命中
                • 存了数字的char或varchar类型,常见的如用字符串表示的手机号,在查询时不加引号,则不会命中(如where phone=‘13340456789’能命中,where phone=13340456789不能命中)
                • 当数据量小时,MySQL发现全表扫描反而比使用索引查询更快时不会使用索引。

                十、为何要使用联合索引?

                MySQL5.0以前,一个表一次只能使用一个索引,没法同时使用多个索引分别进行条件扫描。可是从5.1开始,引入了 index merge 优化技术,对同一个表能够使用多个索引分别进行条件扫描。

                推荐阅读这篇博客

                • 减小开销。建了一个(a,b,c)的联合索引,至关于建了(a),(a,b),(a,b,c)三个索引
                • 覆盖索引。减小了随机IO操做。一样的有复合索引(a,b,c),若是有以下的sql: select a,b,c from table where a=1 and b = 1。那么MySQL能够直接经过遍历索引取得数据,而无需回表,这减小了不少的随机io操做
                • 效率高。索引列越多,经过索引筛选出的数据越少。好比有1000W条数据的表,有以下sql:select * from table where a = 1 and b =2 and c = 3,假设假设每一个条件能够筛选出10%的数据,若是只有单值索引,那么经过该索引能筛选出1000W10%=100w 条数据,而后再回表从100w条数据中找到符合b=2 and c= 3的数据,而后再排序,再分页;若是是复合索引,经过索引筛选出1000w 10% 10% 10%=1w,而后再排序、分页。

                十一、既然索引能够加快查询速度,索引越多越好是吗?

                推荐阅读这篇优博客

                大多数状况下索引能大幅度提升查询效率,但数据的变动(增删改)都须要维护索引,所以更多的索引意味着更多的维护成本和更多的空间 (一本100页的书,却有50页目录?)并且太小的表,创建索引可能会更慢(读个2页的宣传手册,你还先去找目录?)

                十二、主键和惟一索引的区别?

                • 主键是一种约束,惟一索引是索引,一种数据结构。
                • 主键必定是惟一索引,惟一索引不必定是主键。
                • 一个表中能够有多个惟一索引,但只能有一个主键。
                • 主键不容许空值,惟一索引容许。
                • 主键能够作为外键,惟一索引不行;

                1三、B+树和B-树的区别?

                B-树是一种平衡的多路查找树。2-3树和2-3-4树都是B-树的特例。一棵M阶的B-树,除了根结点外的其余非叶子结点,最多含有M-1对键和连接,最少含有M/2对键和连接。根结点能够少于M/2,可是也不能少于2对。

                • 关键字集合分布在整颗树中
                • 每一个元素在该树中只出现一次,可能在叶子结点上,也可能在非叶子结点上。
                • 搜索有可能在非叶子结点结束。
                • 全部叶子结点位于同一层

                B+树是B-树的变体,也是一种多路查找树。

                • 非叶子结点值能够看做索引,仅含有其子树中的最大(或)最小关键字。
                • 叶子结点保存了全部关键字,且叶子结点按照从小到大的顺序排列,是一个双向链表结构。
                • 只能在叶子节点命中搜索

                B+ 树更适合用于数据库和操做系统的文件系统中。

                假设一个结点就是一个页面,B树遍历全部记录,经过中序遍历的方式,要屡次返回到父结点,同一个结点屡次访问了,增长了磁盘I/O操做的次数。B+由于在叶子结点存放了全部的记录,并且是双向链表的结构,只需在叶子节点这一层就能遍历全部记录,大大减小了磁盘I/O操做,因此数据库索引用B+树结构更好。

                1四、汇集索引与非汇集索引的区别?

                • 对于汇集索引,表记录的排列顺序和与索引的排列顺序是一致的;非汇集索引不是
                • 汇集索引就是按每张表的主键构造一棵B+树,每张表只能拥有一个汇集索引;一张表能够有多个非汇集索引
                • 汇集索引的叶子结点存放的是整张表的行记录数据;非汇集索引的叶子结点并不包含行记录的所有数据,除了包含键值还包含一个书签——即相应行数据的汇集索引键。所以经过非汇集索引查找时,先根据叶子结点的指针得到指向主键索引的主键,而后再经过主键索引来找到一个完整的行记录。

                1五、InnoDB和MyISAM引擎的区别?

                • InnoDB支持事务,MyISAM不支持
                • InnoDB是行锁设计,MyISAM是表锁设计
                • InnoDB支持外键,MyISAM不支持
                • InnoDB采用汇集的方式,每张表按照主键的顺序进行存放。若是没有主键,InnoDB会为每一行生成一个6字节的ROWID并以此为主键;MyISAM能够不指定主键和索引
                • InnoDB没有保存表的总行数,所以查询行数时会遍历整表;而MyISAM有一个变量存储可表的总行数,查询时能够直接取出该值
                • InnoDB适合联机事务处理(OLTP),MyISAM适合联机分析处理(OLAP)

                1六、COUNT(*)COUNT(1)的区别?COUNT(列名)COUNT(*)的区别?

                COUNT(*)COUNT(1)没区别。COUNT(列名)COUNT(*)区别在于前者不会统计列为NULL的数据,后者会统计。

                1七、数据库中悲观锁和乐观锁讲一讲?

                悲观锁:老是假设在并发下会出现问题,即假设多个事务对同一个数据的访问会产生冲突。当其余事务想要访问数据时,会在临界区提早加锁,须要将其阻塞挂起。好比MySQL中的排他锁(X锁)、和共享锁(S锁)

                乐观锁: 老是假设任务在并发下是安全的,即假设多个事务对同一个数据的访问不会发生冲突,所以不会加锁,就对数据进行修改。当遇到冲突时,采用CAS或者版本号、时间戳的方式来解决冲突。数据库中使用的乐观锁是版本号或时间戳。乐观并发控制(OCC)是一种用来解决写-写冲突的无锁并发控制,不用加锁就尝试对数据进行修改,在修改以前先检查一下版本号,真正提交事务时,再检查版本号有,若是不相同说明已经被其余事务修改了,能够选择回滚当前事务或者重试;若是版本号相同,则能够修改。

                提一下乐观锁和MVCC的区别,其实MVCC也利用了版本号,和乐观锁仍是能扯上些关系。

                MVCC主要解决了读-写的阻塞,由于读只能读到数据的历史版本(快照);OCC主要解决了写-写的阻塞,多个事务对数据进行修改而不加锁,更新失败的事务能够选择回滚或者重试。

                当多个用户/进程/线程同时对数据库进行操做时,会出现3种冲突情形:读-读,不存在任何问题;读-写,有隔离性问题,可能遇到脏读、不可重复读 、幻读等。写-写,可能丢失更新。多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,读操做只读该事务开始前的数据库的快照,实现了一致性非锁定读。 这样在读操做不用阻塞写操做,写操做不用阻塞读操做的同时,避免了脏读和不可重复读。乐观并发控制(OCC)是一种用来解决写-写冲突的无锁并发控制,不用加锁就尝试对数据进行修改,在修改以前先检查一下版本号,真正提交事务时,再检查版本号有,若是不相同说明已经被其余事务修改了,能够选择回滚当前事务或者重试;若是版本号相同,则能够修改。

                1八、MySQL的可重复读是如何实现的?

                MVCC(多版本并发控制)使用undo_log使得事务能够读取到数据的快照(某个历史版本),从而实现了可重复读。MySQL采用Next-Key Lock算法,对于索引的扫描不只是锁住扫描到的索引,还锁住了这些索引覆盖的范围,避免了不可重复读和幻读的产生。

                具体来讲:

                在可重复读下: select....from会采用MVCC实现的一致性非锁定读,读取的是事务开始的快照,避免了不可重复读。select .....from .... for update会采用 Next-Key Locking来保证可重复读和幻读。

                在读已提交下: select....from 会采用快照,读取的是最新一份的快照数据,不可以保证不可重复读和幻读;select .....from .... for update会采用Record Lock,不可以保证不可重复读/幻读。

                1九、覆盖索引是什么?

                若是一个索引包含(或覆盖)全部须要查询的字段的值,即只需扫描索引而无须回表,这称为“覆盖索引”。InnoDB的辅助索引在叶子节点中保存了部分键值信息以及指向汇集索引键的指针,若是辅助索引叶子结点中的键值信息已经覆盖了要查询的字段,就没有必要利用指向主键索引的主键,而后再经过主键索引来找到一个完整的行记录了。

                20、MySQL中JOIN和UNION什么区别?

                UNION 操做符用于合并两个或多个 SELECT 语句的结果集。UNION 内部的 SELECT 语句必须拥有相同数量的列。列也必须拥有相同的数据类型。同时,每条 SELECT 语句中的列的顺序必须相同。默认状况下,UNION会过滤掉重复的值。使用 UNION ALL则会包含重复的值。

                JOIN用于链接两个有关联的表,筛选两个表中知足条件(ON后的条件)的行记录获得一个结果集。从结果集中SELECT的字段能够是表A或者表B中的任意列。

                JOIN经常使用的有LEFT JOIN、RIGHT JOIN、INNER JOIN。

                • LEFT JOIN会以左表为基础,包含左表的全部记录,以及右表中匹配ON条件的记录,对于未匹配的列,会以NULL表示。
                • LEFT JOIN会以右表为基础,包含左表的全部记录,以及右表匹配ON条件的记录,对于未匹配的列,会以NULL表示。
                • INNER JOIN,产生两个表的交集(只包含知足ON条件的记录)

                231634008872011

                INNER JOIN

                231634016379381

                FULL OUTER JOIN

                231634027315181

                231634045743650

                LEFT JOIN

                231634072774691

                RIGHT JOIN和LEFT JOIN相似。

                241947220904425

                2一、WHERE和HAVING的区别?

                • WHERE过滤的是行,HAVING过滤分组。
                • WHERE能完成的,均可以用HAVING(只是有时候不必)
                • WHERE在分组前对数据进行过滤,HAVING在分组后对数据进行过滤
                • WHERE后不能接聚合函数,HAVING后面一般都有聚合函数

                2二、SQL注入是什么,如何防止?

                所谓SQL注入式攻击,就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的SQL命令。

                好比在登陆界面,若是用户名填入'xxx' OR 1=1 --就能构造下面的SQL语句,由于OR 1=1,password被注释掉,所以不管name和password填入什么都能登陆成功。

                SELECT * FROM USER WHERE NAME='xxx' OR 1=1 -- and password='xxx';

                使用PrepareStatement,能够防止sql注入攻击,sql的执行须要编译,注入问题之因此出现,是由于用户填写 sql语句参与了编译。使用PrepareStatement对象在执行sql语句时,会分为两步,第一步将sql语句 "运送" 到mysql上预编译,再回到java端拿到参数运送到mysql端。预先编译好,也就是SQL引擎会预先进行语法分析,产生语法树,生成执行计划,也就是说,后面你输入的参数,不管你输入的是什么,都不会影响该sql语句的语法结构了。用户填写的 sql语句,就不会参与编译,只会当作参数来看。从而避免了sql注入问题。

                SSM

                一、Spring有什么好处(特性),怎么管理对象的?

                • IOC:Spring的IOC容器,将对象之间的建立和依赖关系交给Spring,下降组件之间的耦合性。即Spring来控制对象的整个生命周期。其实就是日常说的DI或者IOC。
                • AOP:面向切面编程。能够将应用各处的功能分离出来造成可重用的组件。核心业务逻辑与安全、事务、日志等这些非核心业务逻辑分离,使得业务逻辑更简洁清晰。

                • 使用模板消除了样板式的代码。好比使用JDBC访问数据库。
                • 提供了对像关系映射(ORM)、事务管理、远程调用和Web应用的支持。

                Spring使用IOC容器建立和管理对象,好比在XML中配置了类的全限定名,而后Spring使用反射+工厂来建立Bean。BeanFactory是最简单的容器,只提供了基本的DI支持,ApplicationContext基于BeanFactory建立,提供了完整的框架级的服务,所以通常使用应用上下文。

                二、什么是IOC?

                IOC(Inverse of Control)即控制反转。能够理解为控制权的转移。传统的实现中,对象的建立和依赖关系都是在程序进行控制的。而如今由Spring容器来统一管理、对象的建立和依赖关系,控制权转移到了Spring容器,这就是控制反转。

                三、什么是DI?DI的好处是什么?

                DI(Dependency Injection)依赖注入。对象的依赖关系由负责协调各个对象的第三方组件在建立对象的时候进行设定,对象无需自行建立或管理它们的依赖关系。通俗点说就是Spring容器为对象注入外部资源,设置属性值。DI的好处是使得各个组件之间松耦合,一个对象若是只用接口来代表依赖关系,这种依赖能够在对象绝不知情的状况下,用不一样的具体类进行替换。

                IOC和DI实际上是对同一种的不一样表述

                四、什么是AOP,AOP的好处?

                AOP(Aspect-Orientid Programming)面向切面编程,能够将遍及在应用程序各个地方的功能分离出来,造成可重用的功能组件。系统的各个功能会重复出如今多个组件中,各个组件存在于核心业务中会使得代码变得混乱。使用AOP能够将这些多处出现的功能分离出来,不只能够在任何须要的地方实现重用,还能够使得核心业务变得简单,实现了将核心业务与日志、安全、事务等功能的分离。

                具体来讲,散布于应用中多处的功能被称为横切关注点,这些横切关注点从概念上与应用的业务逻辑是相分离的,可是又经常会直接嵌入到应用的业务逻辑中,AOP把这些横切关注点从业务逻辑中分离出来。安全、事务、日志这些功能均可以被认为是应用中的横切关注点。

                一般要重用功能,能够使用继承或者委托的方式。可是继承每每致使一个脆弱的对像体系;委托带来了复杂的调用。面向切面编程仍然能够在一个地方定义通用的功能,可是能够用声明的方法定义这个功能要在何处出现,而无需修改受到影响的类。横切关注点能够被模块化为特殊的类,这些类被称为切面(Aspect)。好处在于:

                • 每一个关注点都集中在一个地方,而非分散在多处代码中;
                • 使得业务逻辑更简洁清晰,由于这样能够只关注核心业务,次要的业务被分离成关注点转移到切面中了。

                AOP术语介绍

                通知:切面所作的工做称为通知。通知定义了切面是什么,以及在什么时候使用。Spring切面能够应用5种类型的通知

                • 前置通知(Before):在目标方法被调用之间调用通知功能;
                • 后置通知(After):在目标方法被调用或者抛出异常以后调用通知功能;
                • 返回通知(After-returning):在目标方法成功执行以后调用通知;
                • 异常通知(After-throwing):在目标方法抛出异常以后调用通知;
                • 环绕通知(Around):通知包裹了被通知的方法,在目标方法被调用以前和调用以后执行自定义的行为。

                链接点:能够被通知的方法

                切点:实际被通知的方法

                切面:即通知和切点的结合,它是什么,在什么时候何处完成其功能。

                引入:容许向现有的类添加新方法或属性,从而能够在无需修改这些现有的类状况下,让它们具备新的行为和状态。

                织入:把切面应用到目标对象并建立新的代理对象的过程。切面在指定的链接点被织入到目标对象中。在目标对象的生命周期里有多个点能够进行织入:

                • 编译期,切面在目标类编译时织入。
                • 类加载期,切面在目标类加载到JVM时被织入。
                • 运行期,切面在应用运行的某个时刻被织入,在织入切面时,AOP容器会为目标对象动态地建立一个代理对象。Spring AOP就是以这种方式织入切面的

                Spring AOP构建在动态代理基础之上,因此Spring对AOP的支持仅限于方法拦截。

                Spring的切面是由包裹了目标对象的代理类实现的。代理类封装了目标类,并拦截被通知方法的调用,当代理拦截到方法调用时,在调用目标bean方法以前,会执行切面逻辑。其实切面只是实现了它们所包装bean相同接口的代理

                五、AOP的实现原理:Spring AOP使用的动态代理。

                Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理经过反射来接收被代理的类,而且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。

                若是目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,能够在运行时动态的生成某个类的子类,注意,CGLIB是经过继承的方式作的动态代理,所以若是某个类被标记为final,那么它是没法使用CGLIB作动态代理的。

                Spring使用动态代理,代理类封装了目标类,当代理拦截到方法调用时,在调用目标bean的方法以前,会执行切面逻辑。

                六、Spring的生命周期?

                Spring建立、管理对象。Spring容器负责建立对象,装配它们,配置它们并管理它们的整个生命周期。

                • 实例化:Spring对bean进行实例化
                • 填充属性:Spring将值和bean的引用注入到bean对应的属性中
                • 调用BeanNameAware的setBeanName()方法:若bean实现了BeanNameAware接口,Spring将bean的id传递给setBeanName方法
                • 调用BeanFactoryAware的setBeanFactory()方法:若bean实现了BeanFactoryAware接口,Spring调用setBeanFactory方法将BeanFactory容器实例传入
                • 调用ApplicationContextAware的setApplicationContext方法:若是bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext方法将bean所在的应用上下文传入
                • 调用BeanPostProcessor的预初始化方法:若是bean实现了BeanPostProcessor,Spring将调用它们的叛postProcessBeforeInitialization方法
                • 调用InitalizingBean的afterPropertiesSet方法:若是bean实现了InitializingBean接口,Spring将调用它们的afterPropertiesSet方法
                • 若是bean实现了BeanPostProcessor接口,Spring将调用它们的postProcessAfterInitialzation方法
                • 此时bean已经准备就绪,能够被应用程序使用,它们将一直驻留在应用杀死那个下文中,直到该应用的上下文被销毁。
                • 若是bean实现了DisposableBean接口,Spring将调用它的destroy方法。

                七、Spring的配置方式,如何装配bean?bean的注入方法有哪些?

                • XML配置,如<bean id="">
                • Java配置即JavaConfig,使用@Bean注解
                • 自动装配,组件扫描(component scanning)和自动装配(autowiring),@ComponentScan@AutoWired注解

                bean的注入方式有:

                • 构造器注入
                • 属性的setter方法注入

                推荐对于强依赖使用构造器注入,对于弱依赖使用属性注入。

                八、bean的做用域?

                • 单例(Singleton):在整个应用中,只建立bean一个实例。
                • 原型(Prototype):每次注入或经过Spring应用上下文获取时,都会建立一个新的bean实例。
                • 会话(Session):在Web应用中,为每一个会话建立一个bean实例。
                • 请求(Request):在Web应用中,为每一个请求建立一个bean实例。

                默认状况下Spring中的bean都是单例的。

                九、Spring中涉及到哪些设计模式?

                • 工厂方法模式。在各类BeanFactory以及ApplicationContext建立中都用到了;
                • 单例模式。在建立bean时用到,Spring默认建立的bean是单例的;
                • 代理模式。在AOP中使用Java的动态代理;
                • 策略模式。好比有关资源访问的Resource类
                • 模板方法。好比使用JDBC访问数据库,JdbcTemplate。
                • 观察者模式。Spring中的各类Listener,如ApplicationListener
                • 装饰者模式。在Spring中的各类Wrapper和Decorator
                • 适配器模式。Spring中的各类Adapter,如在AOP中的通知适配器AdvisorAdapter

                十、MyBatis和Hibernate的区别和应用场景?

                Hibernate :是一个标准的ORM(对象关系映射) 框架; SQL语句是本身生成的,程序员不用本身写SQL语句。所以要对SQL语句进行优化和修改比较困难。适用于中小型项目。

                MyBatis: 程序员本身编写SQL, SQL修改和优化比较自由。 MyBatis更容易掌握,上手更容易。主要应用于需求变化较多的项目,如互联网项目等。

                海量数据处理

                首先要了解几种数据结构和算法:

                • HashMap,记住对于同一个键,哈希出来的值必定是同样的,不一样的键哈希出来也可能同样,这就是发生了冲突(或碰撞)。
                • BitMap,能够当作是bit数组,数组的每一个位置只有0或1两种状态。Java中能够使用int数组表示位图,arr[0]是一个int,一个int是32位,故能够表示0-31的数,同理arr[1]可表示32-63...实际上就是用一个32位整型表示了32个数。
                • 大/小根堆,O(1)时间可在堆顶获得最大值/最小值。利用小根堆可用于求Top K问题。
                • 布隆过滤器。使用长度为m的bit数组和k个Hash函数,某个键通过k个哈希函数获得k个下标,将k个下标在bit数组中对应的位置设置为1。对于每一个键都重复上述过程,获得最终设置好的布隆过滤器。对于新来的键,使用一样的过程,获得k个下标,判断k个下标在bit数组中的值是否为1,如有一个不为1,说明这个键必定不在集合中。若全为1,也可能不在集合中。就是说:查询某个键,判断不属于该集合是绝对正确的;判断属于该集合是低几率错误的。由于多个位置的1多是由不一样的键散列获得。

                对上亿个无重复数字的排序,或者找到没有出现过数字,注意由于无重复数字,而BitMap的0和1正好能够表示该数字有没有出现过。若是要求更小的内存,能够先分出区间,对落入区间的进行计数。必然有的区间数量未满,再遍历一次数组,只看该区间上的数字,使用BitMap,遍历完成后该区间中必然有没被设置成0的的地方,这些地方就是没出现的数。

                数据在小范围内波动,好比人类年龄,并且数据容许重复,可用计数排序处理数值排序或查找没有出现过的值,计数的桶中频次为0的就是没有出现过的数。

                数据是数字,要找最大的Top K,直接用大小为K的小根堆,不断淘汰最小元素便可。

                数据是数字或非数字,要找频次最高的Top K。可以使用HashMap统计频次,统计出频次最大的前K个便可。统计频次选出前K的过程能够用小根堆。还能够用Hash分流的方法,即用一个合适的hash函数将数据分到不一样的机器或者文件中,由于对于一样的数据,因为hash函数的性质,必然被分配到同一个文件中,所以不存在相同的数据分布在不一样的文件这种状况。对每一个文件采用HashMap统计频次,用小根堆选出Top K,而后汇总所有文件,从全部部分结果的Top K中再利用小根堆获得最终的Top K。

                查找数值的排名,好比找到中位数。好比将数划分区间,对落入每一个区间的数进行计数。而后能够得知中位数落在哪一个区间,再遍历全部数,此次只关心落在该区间的数,不划分区间的对其进行计数,就能够找出中位数。

相关文章
相关标签/搜索