CVTE/顺丰/依图科技/去哪面试真题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/strivenoend/article/details/84800915

​​​​​

  1. 说一下单例模式的各种写法,手写一种线程安全的

  2. 第一种(懒汉,线程不安全):

    Java代码  收藏代码

  3. public class Singleton {  
  4.     private static Singleton instance;  
  5.     private Singleton (){}  
  6.   
  7.     public static Singleton getInstance() {  
  8.     if (instance == null) {  
  9.         instance = new Singleton();  
  10.     }  
  11.     return instance;  
  12.     }  
  13. }  
  14.  

     这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。

    第二种(懒汉,线程安全):

    Java代码  收藏代码

  15. public class Singleton {  
  16.     private static Singleton instance;  
  17.     private Singleton (){}  
  18.     public static synchronized Singleton getInstance() {  
  19.     if (instance == null) {  
  20.  

     这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。

    第三种(饿汉):

    Java代码  收藏代码

  21. public class Singleton {  
  22.     private static Singleton instance = new Singleton();  
  23.     private Singleton (){}  
  24.     public static Singleton getInstance() {  
  25.     return instance;  
  26.     }  
  27. }  
  28.  

     这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。

    第四种(饿汉,变种):

    Java代码  收藏代码

  29. public class Singleton {  
  30.     private Singleton instance = null;  
  31.     static {  
  32.     instance = new Singleton();  
  33.     }  
  34.     private Singleton (){}  
  35.     public static Singleton getInstance() {  
  36.     return this.instance;  
  37.     }  
  38. }  
  39.  

     表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。

  40.         instance = new Singleton();  
  41.     }  
  42.     return instance;  
  43.     }  
  44. }  
  45. ArrayList如何扩容?(常考)

  46. ArrayList是一个可动态添加删除数据的集合,底层数据结构是数组。当添加数据的容量大于底层数组容量时则会产出扩容,即通过生成数组来实现。它的主要核心就是扩容机制(当插入时所需要的长度超过数组原本的长度时则需要扩容)。本文主要抓ArrayList的重点分析。

    接下来抱着这几个问题来分析一下ArrayList的源码

    1.ArrayList怎么判断是否需要扩容的?

    2.ArrayList是怎么扩容的,怎么操作数组实现的?

    3.ArrayList在扩容做了什么优化?

    4.ArrayList为什么是线程不安全的?

    5.ArrayList为什么增删慢(源码角度看)?

    6.ArrayList的缩容机制是怎样的?

    ArrayList类与成员变量:
    //继承自AbstractList
    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
     
    //默认初始容量
    private static final int DEFAULT_CAPACITY = 10;
     
    //默认元素集合
    private static final Object[] EMPTY_ELEMENTDATA = {};
     
    //无参构造实例默认元素集合
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
     
    //存放元素的数组
    transient Object[] elementData;
     
    //记录ArrayList中存储的元素的个数
    private int size;
    构造函数:
    //可指定大小的构造函数
    public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }
     
    //默认构造函数
    public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
     
    //传入一个为Collection的对象
    public ArrayList(Collection<? extends E> c) {
            elementData = c.toArray();
            if ((size = elementData.length) != 0) {
                // c.toArray might (incorrectly) not return Object[] (see 6260652)
                if (elementData.getClass() != Object[].class)
                    elementData = Arrays.copyOf(elementData, size, Object[].class);
            } else {
                // replace with empty array.
                this.elementData = EMPTY_ELEMENTDATA;
            }
        }
    三个构造函数目的都是为初始化elementData,一般我们使用中间默认的即可。如需使用其它的可按情况使用,比如当我们知道需要使用的容量那就用第一个指定容量大小,避免频繁扩容。

    插入add():
    //从尾部插入数据
    public boolean add(E e) {
            //判断是否扩容进行扩容
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //在数组后面添加元素e
            elementData[size++] = e;
            return true;
        }
     
    ensureCapacityInternal方法为了判断是否扩容,跟进去

    private void ensureCapacityInternal(int minCapacity) {
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
        }
     
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
     
            // overflow-conscious code
            if (minCapacity - elementData.length > 0) //如果所需容量大于现数组容量,则进行扩容
                //扩容方法
                grow(minCapacity);
        }
    这里我们就可以解决上面说的第1个问题了。

    根据所需容量是否大于现数组容量来进行扩容,调用grow方法,继续跟进去

    grow方法是ArrayList扩容的核心方法:
    private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            
            //增加1.5倍的长度
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            
            //当增加1.5倍后还是不够所需要长度,则直接用所需长度来扩容
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            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);
        }
    通过oldCapacity + (oldCapacity >> 1)(右移左移运算的思路:先转二进制计算后再转回十进制)来实现扩容1.5倍的长度,再通过Arrays.copyOf实现扩容,这里就解决了第2个问题。

    第3个问题的解决也在这里,我们可以看到,它是先扩容1.5倍,当增加后还是不够用,则直接使用所需要的长度作为数组的长度。为什么呢?这里ArrayList是做了优化,它先扩1.5倍,如果这次够用下次再来扩容可能就可以不用扩容。如果每次都用所需要的长度来扩容,那么以后每次增加元素都会进行一次扩容操作,当增加后还是不够用的时候,ArrayList无法知道到底给你多少容量才合适,所以就直接使用你所需的长度。扩容的时候底层是需要操作数组的(Arrays.copyOf),经常扩容会消耗性能的。

    我们看到ArrayList里面的方法都没有添加锁,即当多个线程同时调用的话会会引发不可预知的错误。这是第4个问题的。

    删除remove方法:
    public boolean remove(Object o) {
            //如果传入元素为null,则循环判断元素为null的进行删除
            if (o == null) {
                for (int index = 0; index < size; index++)
                    if (elementData[index] == null) {
                        fastRemove(index);    //删除
                        return true;
                    }
            } else {           //循环数组进行判断删除
                for (int index = 0; index < size; index++)
                    if (o.equals(elementData[index])) {
                        fastRemove(index);    //删除
                        return true;
                    }
            }
            return false;
        }
    循环整个数组,判断是否相等进行删除。因为ArrayList 允许空值,所以源码这里进行了多一次的判断是否为null的情况。可以看到核心删除方法是fastRemove。

    private void fastRemove(int index) {
            modCount++;
            int numMoved = size - index - 1;
            if (numMoved > 0)
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            elementData[--size] = null; // clear to let GC do its work
        }
    这个核心方法的思路很简单,定位到当前需要删除的元素位置,然后把后面元素的位置+1顶上来一个位置,,然后把最后一个位置设置为null,size同时减1,实现删除。就好比一根三节棍,把中间那截给删了,原来后面的就顶上来跟前面的接在了一起,成了两节棍。

    注:其实从这里就可以解决第5个问题,在它删除元素时候后面的元素需要移动上去,采用了System.arraycopy复制,当移动多了自然性能就低了,这就是删除慢的原因。为什么插入慢呢,我想大家都明白了,删除的时候后面得向前移动,那插入的时候同理得向后移动。(通过指定位置插入元素的情况,因为ArrayList可以从尾部,也可以指定位置插入)。

    思考一个问题,当我们需要向ArrayList增加大量元素时它会扩容成一个大数组,当我们不需要那么多元素的时候把它删了,那数组虽然元素被删了它还是会在内存中占了空间,好比一个大盆子装一个小鸡蛋。ArrayList没有自动来处理这个问题,它提供了一个方法trimToSize()方法。我们可以手动调用此方法触发ArrayList 的缩容机制。这样就可以释放多余的空间,提高空间利用率。

    public void trimToSize() {
            modCount++;
            if (size < elementData.length) {
                elementData = (size == 0)
                  ? EMPTY_ELEMENTDATA
                  : Arrays.copyOf(elementData, size);
            }
        }
    size是记录元素数据,当size(元素数量)小于此数组的长度(elementData.length),说明数组有空闲空间位置没有被使用,那就把数组转换成长度当前元素数量的长度。这就是第6个问题ArrayList的缩容机制

  47. 场景题:生产者消费者模式可解

  48. 设计模式了解哪些,手写一下观察者模式

  49. 一个十进制的数在内存中是怎么存的?

  50. 为啥有时会出现4.0-3.6=0.40000001这种现象?(这个没回答上来,让我回去看看

  51. Hibernate中有哪几种数据库语句写法?我只回答了Sql和Hql,另一种没回答上来,让我回去查查

  52. 3. JVM,垃圾回收算法,垃圾回收器

    4. 会哪些排序算法,解释一下快排原理

    6. 一个学生表,一个课程成绩表,怎么找出学生课程的最高分数

    7. 一个数的因子只能是3,5,7,问第n个这样的数是多少?(剑指offer丑数那题变型,我回答的不好

  53. 怎么解决你这个系统高并发的问题?

    我说可以用负载均衡来平衡流量,扩大服务器规模,面试官说你数据库服务器不要处理嘛,我赶紧补了一句可以用缓存

    4. 负载均衡怎么配置?

    只看过介绍,没配置过阿

    5. 缓存找到了数据怎么配置,找不到又怎样处理?画一下

    6. 规定1分钟之内只能处理1000个请求,你怎么实现,手撕代码

    写好了之后,面试官一再强调一分钟是相对时间,感觉我的写法面试官并不满意,然后这个问题纠缠了很久

    7. 怎么求一个二叉树的深度?手撕代码

    8. 两个数组A和B,怎么求解两个数组中和为S的所有组合(组合中一个元素是A的,一个元素是B的)

    我说先排序,然后头尾指针

  54. 问了数据库的隔离级别

  55. forward与redirect区别,说一下你知道的状态码,redirect的状态码是?

  56. 算法题:二叉树层序遍历,进一步提问:要求每层打印出一个换行符

  57. 8.那ConcurrentHashMap内部是如何实现的?每个segment是个什么数据结构?ConcurrentHashMap如何扩容,内部结构?(HashTable)
    由多个segment组成,每个segment是一个HashEntry数组,HashEntry是链表。
    实现同步的方法是segment继承了ReentrantLock类;Hashtable实现线程安全的方法是使用synchronized

    6.序列化,以及json传输

  58. hashMap和ConcurrentHashMap的区别
  59. 总结ConcurrentHashMap就是一个分段的hashtable ,根据自定的hashcode算法生成的对象来获取对应hashcode的分段块进行加锁,不用整体加锁,提高了效率
  60. HashMap底层,负载因子,为啥是2^n

    ConcurrentHashMap锁加在了哪些地方

  61. 算法题:求一个数组中连续子向量的最大和

  62. 找出数组中和为S的一对组合,找出一组就行

猜你喜欢

转载自blog.csdn.net/strivenoend/article/details/84800915