面试题汇总


sidebarDepth: 0

常见题

[[toc]]

基本题

java集合

1、List,Set区别

List
1.可以允许重复的对象。
2.可以插入多个null元素。
3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
4.常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。

Set
1.不允许重复对象
2.无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。
3.只允许一个 null 元素
4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。

avatar

2、HashSet 是如何保证不重复的

hash值计算和equals方式实现集合无重复元素:
1,如果hash码值不相同,说明是一个新元素,存.如果没有元素和传入对象(也就是add的元素)的hash值相等,那么就认为这个元素在table中不存在,将其添加进table
2,如果hash码值相同,且equles判断相等,说明元素已经存在,不存.
3,如果hash码值相同,且equles判断不相等,说明元素不存在,存.
4,如果有元素和传入对象的hash值相等,那么,继续进行equles()判断,如果仍然相等,那么就认为传入元素已经存在,不再添加,结束,否则仍然添加.

hash计算: 通过对象中的成员来计算出来的结果;如果成员变量是基本数据类型的值, 那么用这个值 直接参与计算;如果成员变量是引用数据类型的值,那么获取到这个成员变量的哈希码值后,再参数计算

3、HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?
avatar

avatar

avatar

两个可能出现线程不安全的地方:
1、put的时候导致的多线程数据不一致。
这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。
2、HashMap的get操作可能因为resize而引起死循环(cpu100%). 方法的功能是将原来的记录重新计算在新桶的位置,然后迁移过去。
resize方法核心:

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry e : table) {

            while(null != e) {
                Entry next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
avatar

我们假设有两个线程同时需要执行resize操作,我们原来的桶数量为2,记录数为3,需要resize桶到4,原来的记录分别为:[3,A],[7,B],[5,C],在原来的map里面,我们发现这三个entry都落到了第二个桶里面。
假设线程thread1执行到了transfer方法的Entry next = e.next这一句,然后时间片用完了,此时的e = [3,A], next = [7,B]。线程thread2被调度执行并且顺利完成了resize操作,需要注意的是,此时的[7,B]的next为[3,A]。此时线程thread1重新被调度运行,此时的thread1持有的引用是已经被thread2 resize之后的结果。线程thread1首先将[3,A]迁移到新的数组上,然后再处理[7,B],而[7,B]被链接到了[3,A]的后面,处理完[7,B]之后,就需要处理[7,B]的next了啊,而通过thread2的resize之后,[7,B]的next变为了[3,A],此时,[3,A]和[7,B]形成了环形链表,在get的时候,如果get的key的桶索引和[3,A]和[7,B]一样,那么就会陷入死循环。
如果在取链表的时候从头开始取(现在是从尾部开始取)的话,则可以保证节点之间的顺序,那样就不存在这样的问题了。
综合上面两点,可以说明HashMap是线程不安全的。

4、HashMap 的扩容过程

扩容时机: 当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值-就要自动扩容.当map中包含的Entry的数量大于等于threshold = loadFactor * capacity的时候,且新建的Entry刚好落在一个非空的桶上,此刻触发扩容机制,将其容量扩大为2倍。当size大于等于threshold的时候,并不一定会触发扩容机制,但是会很可能就触发扩容机制,只要有一个新建的Entry出现哈希冲突,则立刻resize
扩容(resize) 就是调用resize()方法,重新计算容量,将容量扩大为原来的2倍。

void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {//最大容量为 1 << 30
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];//新建一个新表
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;//是否再hash
        transfer(newTable, rehash);//完成旧表到新表的转移
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry e : table) {//遍历同桶数组中的每一个桶
            while(null != e) {//顺序遍历某个桶的外挂链表
                Entry next = e.next;//引用next
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);//找到新表的桶位置;原桶数组中的某个桶上的同一链表中的Entry此刻可能被分散到不同的桶中去了,有效的缓解了哈希冲突。
                e.next = newTable[i];//头插法插入新表中
                newTable[i] = e;
                e = next;
            }
        }
    }

1.在对HashMap进行扩容的时候,HashMap的容量会变为原来的两倍.通过以上我们知道HashMap的容量必须是2的幂,那么为什么要这么设计呢?答案当然是为了性能。在HashMap通过键的哈希值进行定位桶位置的时候,调用了一个indexFor(hash, table.length);方法。

/**
 * Returns index for hash code h.
 */
static int indexFor(int h, int length) {
    return h & (length-1);
}

可以看到这里是将哈希值h与桶数组的length-1(实际上也是map的容量-1)进行了一个与操作得出了对应的桶的位置,h & (length-1)。 通过限制length是一个2的幂数,h & (length-1)和h % length结果是一致的。这就是为什么要限制容量必须是一个2的幂的原因。
但是为什么不采用h % length这种计算方式呢? Java的%、/操作比&慢10倍左右,因此采用&运算会提高性能

2.扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容.
3.负载因子是可以修改的,默认0.75, 也可以大于1,但是建议不要轻易修改,除非情况非常特殊.

5、HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?

1.hash方法不同了
2.jdk1.8删除了indexFor方法
3.结点类名字由entry改成了node
4.put时,jdk1.8引入的红黑树,那就是存储结构由以前单纯的数组+链表,变成了数组+链表/红黑树
5.resize更加高效
6.初始化方式不同
详解: https://juejin.im/post/5aa5d8d26fb9a028d2079264

6、final finally finalize

1.final(关键字). 用于申明属性,方法和类,表示属性不可变,方法不可以被覆盖,类不可以被继承。
2.finally(用于异常处理). 是异常处理语句结构中,表示总是执行的部分.
3.finallize(用于垃圾回收). 表示是object类一个方法,在垃圾回收机制中执行的时候会被调用被回收对象的方法。允许回收此前未回收的内存垃圾。所有object都继承了 finalize()方法

7、强引用 、软引用、 弱引用、虚引用
avatar

Java中4种引用的级别和强度由高到低依次为:强引用 -> 软引用 -> 弱引用 -> 虚引用
当垃圾回收器回收时,某些对象会被回收,某些不会被回收。垃圾回收器会从根对象Object来标记存活的对象,然后将某些不可达的对象和一些引用的对象进行回收。

  1. 强引用(StrongReference)
    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:
    Object strongReference = new Object();
    复制代码当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
    如果强引用对象不使用时,需要弱化从而使GC能够回收,如下:
    strongReference = null;
    复制代码显式地设置strongReference对象为null,或让其超出对象的生命周期范围,则gc认为该对象不存在引用,这时就可以回收这个对象。具体什么时候收集这要取决于GC算法。
    public void test() {
    Object strongReference = new Object();
    // 省略其他操作
    }
    复制代码在一个方法的内部有一个强引用,这个引用保存在Java栈中,而真正的引用内容(Object)保存在Java堆中。
    当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象会被回收。
    但是如果这个strongReference是全局变量时,就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。
    ArrayList的Clear方法:

在ArrayList类中定义了一个elementData数组,在调用clear方法清空数组时,每个数组元素被赋值为null。
不同于elementData=null,强引用仍然存在,避免在后续调用add()等方法添加元素时进行内存的重新分配。
使用如clear()方法内存数组中存放的引用类型进行内存释放特别适用,这样就可以及时释放内存。

  1. 软引用(SoftReference)
    如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

软引用可用来实现内存敏感的高速缓存。

// 强引用
String strongReference = new String("abc");
// 软引用
String str = new String("abc");
SoftReference softReference = new SoftReference(str);

复制代码软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
ReferenceQueue referenceQueue = new ReferenceQueue<>();
String str = new String("abc");
SoftReference softReference = new SoftReference<>(str, referenceQueue);

str = null;
// Notify GC
System.gc();

System.out.println(softReference.get()); // abc

Reference reference = referenceQueue.poll();
System.out.println(reference); //null

复制代码
注意:软引用对象是在jvm内存不够的时候才会被回收,我们调用System.gc()方法只是起通知作用,JVM什么时候扫描回收对象是JVM自己的状态决定的。就算扫描到软引用对象也不一定会回收它,只有内存不够的时候才会回收。

当内存不足时,JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:
if(JVM内存不足) {
// 将软引用中的对象引用置为null
str = null;
// 通知垃圾回收器进行回收
System.gc();
}
复制代码也就是说,垃圾收集线程会在虚拟机抛出OutOfMemoryError之前回收软引用对象,而且虚拟机会尽可能优先回收长时间闲置不用的软引用对象。对那些刚构建的或刚使用过的"较新的"软对象会被虚拟机尽可能保留,这就是引入引用队列ReferenceQueue的原因。
应用场景:
浏览器的后退按钮。按后退时,这个后退时显示的网页内容是重新进行请求还是从缓存中取出呢?这就要看具体的实现策略了。

如果一个网页在浏览结束时就进行内容的回收,则按后退查看前面浏览过的页面时,需要重新构建;
如果将浏览过的网页存储到内存中会造成内存的大量浪费,甚至会造成内存溢出。

这时候就可以使用软引用,很好的解决了实际的问题:
// 获取浏览器对象进行浏览
Browser browser = new Browser();
// 从后台程序加载浏览页面
BrowserPage page = browser.getPage();
// 将浏览完毕的页面置为软引用
SoftReference softReference = new SoftReference(page);

// 回退或者再次浏览此页面时
if(softReference.get() != null) {
    // 内存充足,还没有被回收器回收,直接获取缓存
    page = softReference.get();
} else {
    // 内存不足,软引用的对象已经回收
    page = browser.getPage();
    // 重新构建软引用
    softReference = new SoftReference(page);
}

复制代码3. 弱引用(WeakReference)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
String str = new String("abc");
WeakReference weakReference = new WeakReference<>(str);
str = null;
复制代码JVM首先将软引用中的对象引用置为null,然后通知垃圾回收器进行回收:
str = null;
System.gc();
复制代码
注意:如果一个对象是偶尔(很少)的使用,并且希望在使用时随时就能获取到,但又不想影响此对象的垃圾收集,那么你应该用Weak Reference来记住此对象。

下面的代码会让一个弱引用再次变为一个强引用:
String str = new String("abc");
WeakReference weakReference = new WeakReference<>(str);
// 弱引用转强引用
String strongReference = weakReference.get();
复制代码同样,弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
简单测试:
GCTarget.java
public class GCTarget {
// 对象的ID
public String id;

// 占用内存空间
byte[] buffer = new byte[1024];

public GCTarget(String id) {
    this.id = id;
}

protected void finalize() throws Throwable {
    // 执行垃圾回收时打印显示对象ID
    System.out.println("Finalizing GCTarget, id is : " + id);
}

}
复制代码GCTargetWeakReference.java
public class GCTargetWeakReference extends WeakReference {
// 弱引用的ID
public String id;

public GCTargetWeakReference(GCTarget gcTarget,
          ReferenceQueue queue) {
    super(gcTarget, queue);
    this.id = gcTarget.id;
}

protected void finalize() {
    System.out.println("Finalizing GCTargetWeakReference " + id);
}

}
复制代码WeakReferenceTest.java
public class WeakReferenceTest {
// 弱引用队列
private final static ReferenceQueue REFERENCE_QUEUE = new ReferenceQueue<>();

public static void main(String[] args) {
    LinkedList gcTargetList = new LinkedList<>();

    // 创建弱引用的对象,依次加入链表中
    for (int i = 0; i < 5; i++) {
        GCTarget gcTarget = new GCTarget(String.valueOf(i));
        GCTargetWeakReference weakReference = new GCTargetWeakReference(gcTarget,
            REFERENCE_QUEUE);
        gcTargetList.add(weakReference);

        System.out.println("Just created GCTargetWeakReference obj: " +
            gcTargetList.getLast());
    }

    // 通知GC进行垃圾回收
    System.gc();

    try {
        // 休息几分钟,等待上面的垃圾回收线程运行完成
        Thread.sleep(6000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    // 检查关联的引用队列是否为空
    Reference reference;
    while((reference = REFERENCE_QUEUE.poll()) != null) {
        if(reference instanceof GCTargetWeakReference) {
            System.out.println("In queue, id is: " +
                ((GCTargetWeakReference) (reference)).id);
        }
    }
}

}
复制代码运行WeakReferenceTest.java,运行结果如下:

可见WeakReference对象的生命周期基本由垃圾回收器决定,一旦垃圾回收线程发现了弱引用对象,在下一次GC过程中就会对其进行回收。

  1. 虚引用(PhantomReference)
    虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
    应用场景:
    虚引用主要用来跟踪对象被垃圾回收器回收的活动。
    虚引用与软引用和弱引用的一个区别在于:

虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);

复制代码程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

8、Java反射

Java 反射机制在程序运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种 动态的获取信息 以及 动态调用对象的方法 的功能称为 java 的反射机制。
反射机制很重要的一点就是“运行时”,其使得我们可以在程序运行时加载、探索以及使用编译期间完全未知的 .class 文件。换句话说,Java 程序可以加载一个运行时才得知名称的 .class 文件,然后获悉其完整构造,并生成其对象实体、或对其 fields(变量)设值、或调用其 methods(方法)。
https://juejin.im/post/598ea9116fb9a03c335a99a4

9、Arrays.sort 实现原理和 Collection.sort 实现原理

排序时小数组使用快排(插入排序):Use Quicksort on small arrays
之后考虑归并排序.
Arrays.sort
事实上Collections.sort方法底层就是调用的array.sort方法,

public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a);
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }
//void java.util.ComparableTimSort.sort()
static void sort(Object[] a, int lo, int hi, Object[] work, int workBase, int workLen) {
        assert a != null && lo >= 0 && lo <= hi && hi <= a.length;

        int nRemaining  = hi - lo;
        if (nRemaining < 2)
            return;  // Arrays of size 0 and 1 are always sorted

        // If array is small, do a "mini-TimSort" with no merges
        if (nRemaining < MIN_MERGE) {
            int initRunLen = countRunAndMakeAscending(a, lo, hi);
            binarySort(a, lo, hi, lo + initRunLen);
            return;
        }

legacyMergeSort(a):归并排序
ComparableTimSort.sort():Timsort排序

备注:
Timsort排序是结合了合并排序(merge sort)和插入排序(insertion sort)而得出的排序算法。(参考:Timsort原理介绍)
Timsort的核心过程
TimSort 算法为了减少对升序部分的回溯和对降序部分的性能倒退,将输入按其升序和降序特点进行了分区。排序的输入的单位不是一个个单独的数字,而是一个个的块-分区。其中每一个分区叫一个run。针对这些 run 序列,每次拿一个 run 出来按规则进行合并。每次合并会将两个 run合并成一个 run。合并的结果保存到栈中。合并直到消耗掉所有的 run,这时将栈上剩余的 run合并到只剩一个 run 为止。这时这个仅剩的 run 便是排好序的结果。
综上述过程,Timsort算法的过程包括
(0)如何数组长度小于某个值,直接用二分插入排序算法
(1)找到各个run,并入栈
(2)按规则合并run

10、LinkedHashMap的应用

HashMap是无序的,当我们希望有顺序地去存储key-value时,就需要使用LinkedHashMap了
详细 https://www.jianshu.com/p/8f4f58b4b8ab

11、cloneable接口实现原理

Cloneable接口是Java开发中常用的一个接口, 它的作用是使一个类的实例能够将自身拷贝到另一个新的实例中,注意,这里所说的“拷贝”拷的是对象实例,而不是类的定义,进一步说,拷贝的是一个类的实例中各字段的值。
如果一个类不实现该接口就直接调用clone()方法的话,即便已将clone()方法重写为public,那还是会抛出“不支持拷贝”异常。因此,要想使一个类具备拷贝实例的功能,那么除了要重写Object类的clone()方法外,还必须要实现Cloneable接口。
在开发过程中,拷贝实例是常见的一种操作,如果一个类中的字段较多,而我们又采用在客户端中逐字段复制的方法进行拷贝操作的话,将不可避免的造成客户端代码繁杂冗长,而且也无法对类中的私有成员进行复制,而如果让需要具备拷贝功能的类实现Cloneable接口,并重写clone()方法,就可以通过调用clone()方法的方式简洁地实现实例拷贝功能。

12、异常分类以及处理机制

avatar

在 Java 中,所有的异常都有一个共同的祖先 Throwable(可抛出)。Throwable 指定代码中可用异常传播机制通过 Java 应用程序传输的任何问题的共性。
Throwable: 有两个重要的子类:Exception(异常)和 Error(错误),二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception(异常):是程序本身可以处理的异常.
Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM 常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
通常,Java的异常(包括Exception和Error)分为可查的异常(checked exceptions)和不可查的异常(unchecked exceptions).
可查异常(编译器要求必须处置的异常):正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
不可查异常(编译器不要求强制处置的异常):包括运行时异常(RuntimeException与其子类)和错误(Error)。
Exception 这种异常分两大类运行时异常和非运行时异常(编译异常)。程序中应当尽可能去处理这些异常。
运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

13、wait和sleep的区别
avatar

avatar

sleep 让线程从 【running】 -> 【阻塞态】 时间结束/interrupt -> 【runnable】
wait 让线程从 【running】 -> 【等待队列】notify -> 【锁池】 -> 【runnable】

14、数组在内存中如何分配

几乎所有的程序设计语言都支持数组。Java也不例外。当我们需要多个类型相同的变量的时候,就考虑定义一个数组。在Java中,数组变量是引用类型的变量,同时因为Java是典型的静态语言,因此它的数组也是静态的,所以想要使用就必须先初始化(为数组对象的元素分配空间)。

1.数组的初始化方式及其内存分配
对于Java数组的初始化,有以下两种方式,这也是面试中经常考到的经典题目:

静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度,如:

1 //只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为4
2 String[] computers = {"Dell", "Lenovo", "Apple", "Acer"};  //①
3 //只是指定初始值,并没有指定数组的长度,但是系统为自动决定该数组的长度为3
4 String[] names = new String[]{"多啦A梦", "大雄", "静香"};  //②
动态初始化:初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配初始值,如:
1 //只是指定了数组的长度,并没有显示的为数组指定初始值,但是系统会默认给数组数组元素分配初始值为null
2 String[] cars = new String[4];  //③

前面提到,因为Java数组变量是引用类型的变量,所以上述几行初始化语句执行后,三个数组在内存中的分配情况如下图所示:
Java数组及其内存分配
[图片上传失败...(image-f1374c-1559907045613)]
由上图可知,静态初始化方式,程序员虽然没有指定数组长度,但是系统已经自动帮我们给分配了,而动态初始化方式,程序员虽然没有显示的指定初始化值,但是因为Java数组是引用类型的变量,所以系统也为每个元素分配了初始化值null,当然不同类型的初始化值也是不一样的,假设是基本类型int类型,那么为系统分配的初始化值也是对应的默认值0。

对于多维数组,假设有这么一段代码:
1 int[][] nums = new int[2][2];2 nums[0][1] = 2;
那么他在内存中的分配情况如下:
[图片上传失败...(image-434b2c-1559907045613)]

Java数组及其内存分配
由上图可知,并没有真正的多维数组,它的本质其实是一维数组

Java 并发

1、synchronized 的实现原理以及锁优化?

https://www.jianshu.com/p/c5058b6fe8e5

2、volatile 的实现原理?

3、Java 的信号灯?

4、synchronized 在静态方法和普通方法的区别?

5、怎么实现所有线程在等待某个事件的发生才会去执行?

6、CAS?CAS 有什么缺陷,如何解决?

7、synchronized 和 lock 有什么区别?

8、Hashtable 是怎么加锁的 ? TODO
9、HashMap 的并发问题?
10、ConcurrenHashMap 介绍?1.8 中为什么要用红黑树?
11、AQS
12、如何检测死锁?怎么预防死锁?

13、Java 内存模型?
14、如何保证多线程下 i++ 结果正确?
15、线程池的种类,区别和使用场景?
16、分析线程池的实现原理和线程的调度过程?
17、线程池如何调优,最大数目如何确认?
18、ThreadLocal原理,用的时候需要注意什么?
19、CountDownLatch 和 CyclicBarrier 的用法,以及相互之间的差别?
20、LockSupport工具
21、Condition接口及其实现原理
22、Fork/Join框架的理解
23、分段锁的原理,锁力度减小的思考
24、八种阻塞队列以及各个阻塞队列的特性

Spring

1、BeanFactory 和 FactoryBean?
2、Spring IOC 的理解,其初始化过程?
3、BeanFactory 和 ApplicationContext? 国际化 时间广播 url访问
4、Spring Bean 的生命周期,如何被管理的?
5、Spring Bean 的加载过程是怎样的?
6、如果要你实现Spring AOP,请问怎么实现?
7、如果要你实现Spring IOC,你会注意哪些问题?
8、Spring 是如何管理事务的,事务管理机制?
9、Spring 的不同事务传播行为有哪些,干什么用的?
10、Spring 中用到了那些设计模式? 工厂 单例 适配器 模板 观察者
11、Spring MVC 的工作原理?
12、Spring 循环注入的原理?
13、Spring AOP的理解,各个术语,他们是怎么相互工作的?
14、Spring 如何保证 Controller 并发的安全? 避免定义实例变量 threadLocal变量 scope=prototype

Netty

1、BIO、NIO和AIO done
2、Netty 的各大组件 done
3、Netty的线程模型 done
4、TCP 粘包/拆包的原因及解决方法 done
https://blog.csdn.net/scythe666/article/details/51996268
5、了解哪几种序列化协议?包括使用场景和如何去选择
6、Netty的零拷贝实现 done 直接堆外内存 compositebuf transfer.to wrap
7、Netty的高性能表现在哪些方面 done

1.io线程模型
使用reactor模式,同步非阻塞。这决定了可以用最少的资源做更多的事。
2.内存零拷贝
使用直接缓存
3.内存池设计
申请的内存可以重用,主要指直接内存。
内部实现是用一颗二叉查找树管理内存分配情况。
4.串形化处理socket读写,避免锁,即一个指定socket的消息是串形化处理的。这样性能比多个线程同时 处理一个socket对应消息要好,因为多线程处理会有锁。
5.提供对protobuf等高性能序列化协议支持

分布式相关

1、Dubbo的底层实现原理和机制
https://blog.csdn.net/u013322876/article/details/72846054

2、描述一个服务从发布到被消费的详细过程

3、分布式系统怎么做服务治理
https://www.cnblogs.com/Zachary-Fan/p/service_manage_discovery.html

4、接口的幂等性的概念

5、消息中间件如何解决消息丢失问题
kafka生成端:基于ack机制
kafka消费端:
大家都知道 Kafka 会自动提交 offset,那么只要 关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是 可能会有重复消费 ,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。
broker端:

  • 给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。
  • 在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。
  • 在 producer 端设置 acks=all :这个是要求每条数据,必须是 写入所有 replica 之后,才能认为是写成功了 。
  • 在 producer 端设置 retries=MAX (很大很大很大的一个值,无限次重试的意思):这个是 要求一旦写入失败,就无限重试 ,卡在这里了。

6、Dubbo的服务请求失败怎么处理 超时重试
7、重连机制会不会造成错误 接口幂等
8、对分布式事务的理解
9、如何实现负载均衡,有哪些算法可以实现?
在集群负载均衡时,Dubbo提供了多种均衡策略,缺省为random随机调用。

  • Random LoadBalance
    随机,按权重设置随机概率。
    在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。

  • RoundRobin LoadBalance
    轮循,按公约后的权重设置轮循比率。
    存在慢的提供者累积请求问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。

  • LeastActive LoadBalance
    最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。
    使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。

  • ConsistentHash LoadBalance
    一致性Hash,相同参数的请求总是发到同一提供者。
    当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

10、Zookeeper的用途,选举的原理是什么?
https://www.jianshu.com/p/e35104ec6e5a

11、数据的垂直拆分水平拆分。

12、zookeeper原理和适用场景

13、zookeeper watch机制
ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。
ZooKeeper 的 Watcher 机制主要包括客户端线程、客户端 WatchManager 和 ZooKeeper 服务器三部分。在具体工作流程上,简单地讲,客户端在向 ZooKeeper 服务器注册 Watcher 的同时,会将 Watcher 对象存储在客户端的 WatchManager 中。当 ZooKeeper 服务器端触发 Watcher 事件后,会向客户端发送通知,客户端线程从 WatchManager 中取出对应的 Watcher 对象来执行回调逻辑。

14、redis/zk节点宕机如何处理
zk:保证过半数节点可用
redis节点挂掉:
A、某个主节点和所有从节点全部挂掉,我们集群就进入faill状态。
B、如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态.
C、如果集群任意master挂掉,且当前master没有slave.集群进入fail状态

集群就会把这个主节点的一个从节点设置为新的主节点

15、分布式集群下如何做到唯一序列号

  • 利用数据库
  • 利用redis,如果是集群,则每台机器分配一个值

16、如何做一个分布式锁
https://www.jianshu.com/p/350a5f891f11

17、用过哪些MQ,怎么用的,和其他mq比较有什么优缺点,MQ的连接是线程安全的吗?

kafka生产端是单线程,线程安全
kafka的消费端是非线程安全,确保消费端的线程数和分区数能够映射上。

18、MQ系统的数据如何保证不丢失

19、列举出你能想到的数据库分库分表策略;分库分表后,如何解决全表查询的问题

20、zookeeper的选举策略
https://www.cnblogs.com/ASPNET2008/p/6421571.html

21、全局ID

数据库 done

1、mysql分页有什么优化
https://www.cnblogs.com/geningchao/p/6649907.html

2、悲观锁、乐观锁

乐观锁:使用版本号或者时间戳
悲观锁:使用了排他锁来实现(select **** for update)。文章开头说到,innodb加行锁的前提是:必须是通过索引条件来检索数据,否则会切换为表锁。

3、组合索引,最左原则

4、mysql 的表锁、行锁

表锁:表级锁和M锁
行锁:读锁和写锁

5、mysql 性能优化
https://www.cnblogs.com/zhouyusheng/p/8038224.html
6、mysql的索引分类:
ans:有序数组(静态存储引擎) + b+树
B+,hash;什么情况用什么索引

7、事务的特性和隔离级别
事务特性:ACID
事务隔离级别:RR、RC、RUC、SERLALIZABLE 分别解决可重复读、脏读、什么也解决不了、解决幻读
幻读:ABA操作,结果变成ABC,前面A为事务,后面为行数

缓存

1、Redis用过哪些数据数据,以及Redis底层怎么实现
底层数据结构:简单动态字符串、链表、字典、跳跃表、整数集合、压缩列表
实际应用数据结构:字符串对象、列表对象(list)、哈希对象(hash)、集合对象(set)、有序集合(sort set)对象
字符串对象:int、raw或者embstr(后面两个是字符串的变种)
列表对象:压缩列表 —> 链表
哈希对象:压缩列表 -> 字典
集合对象:整数集合、字典
有序集合:跳跃表、压缩列表

2、Redis缓存穿透、缓存雪崩
缓存穿透:指查询一个数据库一定不存在的数据 布隆过滤器拦截
缓存雪崩:由于原有缓存失效(过期),新缓存未到期间。所有请求都去查询数据库,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

设置差异化的过期时间,使用异步定时任务更新过期时间
3、如何使用Redis来实现分布式锁

SETNX key val
当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
expire key timeout
为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

4、Redis的并发竞争问题如何解决(服务器 + 客户端)方式
ans:
4.1、使用zk分布式锁
4.2、使用redis.setux锁
4.3、使用watch multi来实现乐观锁机制
4.4、redis.incr自增机制
https://www.cnblogs.com/shamo89/p/8385390.html

5、Redis持久化的几种方式,优缺点是什么,怎么实现的

ans:AOF 和 RDB
RDB:fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的 RDB 替换掉,这样的好处就是可以copy-on-write。以一定的频率保存数据,DB文件需要保存整个数据集的状态。
优点:这种文件非常适合用于进行备份
缺点:如果你需要尽量避免在服务器故障时丢失数据,那么RDB不适合你。

AOF:需要在配置文件中开启(默认是no),appendonly yes开启AOF之后,Redis 每执行一个修改数据的命令,都会把它添加到 AOF文件中,当Redis重启时,将会读取AOF文件进行“重放”以恢复到 Redis 关闭前的最后时刻
优点:使用AOF持久化会让Redis变得非常耐久
缺点:AOF 文件的体积通常要大于RDB文件的体积,数据恢复需要处理巨大的文件载入

6、Redis的缓存失效策略

定时删除:使用定时器,定时删除数据,占用CPU时间
惰性删除:程序只有在取出键时,才会对键进行定期删除,CPU友好,内存不友好
定期删除:整合这两种策略,每隔一段时间执行一次过期键删除操作,每次限制执行的时长和频率。

7、Redis集群,高可用原理 sential+Master/Slave
redis集群实现:
客户端实现: 典型的是支持一致性哈希的客户端
代理层实现: 典型代表twemproxy、codis
redis服务端实现: redis cluster
http://bbs.redis.cn/forum.php?mod=viewthread&tid=2266&highlight=redis%2B%E9%9B%86%E7%BE%A4

8、Redis缓存分片

集群采用分片的方式来实现数据共享,提供复制和故障转移功能。16384个槽,分配到不同节点
当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息。这样当客户端要查找某个 key 时,可以直接定位到目标节点。
集群的槽位配置信息通过slots_to_keys跳跃表来保存槽和键之间的关系。

9、Redis的数据淘汰策略

volatile-lru:从设置了过期时间的数据集中,选择最近最久未使用的数据释放;
allkeys-lru:从数据集中(包括设置过期时间以及未设置过期时间的数据集中),选择最近最久未使用的数据释放;
volatile-random:从设置了过期时间的数据集中,随机选择一个数据进行释放;
allkeys-random:从数据集中(包括了设置过期时间以及未设置过期时间)随机选择一个数据进行入释放;
volatile-ttl:从设置了过期时间的数据集中,选择马上就要过期的数据进行释放操作;
noeviction:不删除任意数据(但redis还会根据引用计数器进行释放),这时如果内存不够时,会直接返回错误

JVM

1、详细jvm内存模型
2、讲讲什么情况下回出现内存溢出,内存泄漏?
3、说说Java线程栈
4、JVM 年轻代到年老代的晋升过程的判断条件是什么呢?
5、JVM 出现 fullGC 很频繁,怎么去线上排查问题?
6、类加载为什么要使用双亲委派模式,有没有什么场景是打破了这个模式?
7、类的实例化顺序
8、JVM垃圾回收机制,何时触发MinorGC等操作
9、JVM 中一次完整的 GC 流程(从 ygc 到 fgc)是怎样的
10、各种回收器,各自优缺点,重点CMS、G1
11、各种回收算法
12、OOM错误,stackoverflow错误,permgen space错误

你可能感兴趣的:(面试题汇总)