首页 > 笔经面经 > 阿里1688 Java实习 一面凉经

阿里1688 Java实习 一面凉经 内部员工回复

头像
语月人
编辑于 2021-03-02 14:43:05 APP内打开
赞 43 | 收藏 315 | 回复11 | 浏览5327

这本来是一篇2月28号就能够发表的面经,但是我在27号面试后两天先后考完驾照的科三科四,拿到驾照后才认认真真开始回顾记录。这就是献祭了我第一次面试的驾照吗。。。
本人22届本科毕业生,面试岗位为阿里巴巴1688 Java开发实习生。整个流程中,面试官非常体贴,给我印象很好,没有我想象的咄咄逼人,更多的是引导我往正确的方向思考。整体面试的题目其实都是基础问题。无奈本人基础并不扎实,加上第一次面试的心态十分紧张,整场面试下来就是面试官提溜着我往前走。面试完后,我询问面试官复习知识点相关的方法和技巧,面试官也给我提出一些建议,收获还是很丰富的(更多是让我知道我到底有多菜)。

注:以下总结均为本人从各种博客,书籍中找到相关知识点后复制或者总结的。不能够保证正确,如果有错误或问题,请友善的指出、批评、讨论,您的每个提问或者解答,都是帮我不断进步的动力。还请大家多多指教!!

Java 中的常见集合:

  • List

    • ArrayList

    • LinkedList

  • Set

    • HashSet

    • TreeSet

  • Map

    • HashMap

    • TreeSet

HashMap 的底层数据结构:

数组、链表红黑树(二叉查找树)

红黑树的具体原理

红黑树在数据结构课上就没看明白,现在还是一头雾水,截止到现在还是研究中,如果有大佬能够指点,还请不吝赐教![抱拳]

HashMap 的线程不安全问题

网上的一些博客我都没看明白(菜),这个我能看懂,画的图也很清晰,直接贴上连接,下面的分析就是自己照着葫芦画瓢,不准确的地方还请请指出
HashMap面试题,看这一篇就够了!

数据覆盖问题

执行 put 操作时,可能会导致出现数据覆盖的问题:

JDK1.7 版本下,插入某个节点采用的是头插法。设有线程A 和线程B同时进行 put 操作,A 和 B 的 key 同时都指向同一个数组下标 table[i]。A先获取table[i]的头节点,将自己插入的节点作为新头节点准备插入时,时间片使用完,轮到B 执行插入完成。这时候再轮到A插入,就会覆盖B插入的节点,从而导致数据覆盖。

扩容导致死循环(基于 JDK1.7)

首先我们要知道在 JDK1.7 中 HashMap 的扩容过程是通过头插进行的。这个过程需要记住当前节点 e 和下一个节点 e.next。看完原博客内容就差不多能明白了,我只是重新以我的思路说一遍。

void transfer(Entry[] newTable, boolean rehash) {

    int newCapacity = newTable.length;
    for (EntryK,V> e : table) {
        while(null != e) {
            EntryK,V> 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;
       }
    }
}

对原博客的图进行解析:

  • 扩容之前,线程B在遍历 table 时获取到一个节点 e(e = a),并且获取到下一个节点 next(next=b)。B的时间片使用完毕

  • 这时A获取到时间片,完成扩容。这时他的节点链表变为 c->b->a->null

  • B获得时间片,进行扩容。这时 B 的 e 引用依旧是 a ,同理 next 引用的是 b。这时我们执行 newTable[i] = e;这一句,则会直接指向a节点,而且b节点就是下一个 e 节点。

  • 循环到下一个节点,e引用的为 b 节点,b节点的下一个节点还是a节点,newTable[i]= e将头节点设置为 b 节点。

  • 又循环到 a 节点,这时将 a 节点作为头节点再次插入,a.next = b 。形成环形链表

写到后面这个逻辑我觉得没问题,但是表述不清晰,看原博会更好点。

循环链表问题在 JDK1.8 得到解决,将头插法改进为尾插法,从而保证了安全性。

数据丢失

一些博客里会说 JDK1.8 中多线程HashMap有数据丢失,我的理解其实就是多线程下对链表的插入操作,多个线程同时获取到尾节点 tail,某个线程插入后没来得及将插入节点A改为尾节点,就被另一个线程获取到时间片。将节点B插入到尾节点tail,这时就出现了一个孤立的节点A。

加一个引申的问题:HashMap rehash 过程是什么样的?各个变量(capacity、size、threshold、loadFactor)意义,他们和扩容的关系?

创建线程的几种方式

  • 继承Thread类创建线程类,重写 run 方法

  • 通过 Runnable 接口创建线程类,重写 run 方法

  • 通过 Callable 和 FutureTask 创建线程

    1. 创建 Callable 接口实现类,重写 call 方法

    2. 创建 Callable 类的实例,使用 FutureTask 类对象包装该实例,FutueTask 对象封装了 Callable 对象 call 方法的返回值

    3. 将 FutureTask 作为 Thread 对象的 target 属性传入,并启动线程。

    4. 调用 FutureTask 对象的 get 方法,获取返回值。

线程池

线程池的优点:

  1. 采用线程池的方案,将线程重复利用,从而保证系统的效率,避免过多资源浪费在创建销毁线程上。

  2. 提高相应速度,请求或者任务到达可以直接相应处理

  3. 将任务提交和执行分离,降低耦合

  4. 提高线程的可管理性。使用线程池统一分配,调优,监控。

设计一个线程池的思路:

面试时候就问我让我讲一讲设计一个线程池的具体思路,我面试前几天还有好好的阅读了一些 ThreadPoolExecutor 的源码,但是一紧张全忘了。下面的思路是我自己认认真真思考,并参考了好多博客得到的思路,不一定保证正确,但是确实是我这次面经中我最花心思总结的。
顺便贴一些博客和书籍供参考:
《Java并发编程的艺术》
https://cloud.tencent.com/developer/article/1673042
https://dunwu.github.io/javacore/concurrent/Java%E7%BA%BF%E7%A8%8B%E6%B1%A0.html
https://mp.weixin.qq.com/s/q0Qt-ha9ps12c15KMW7NfA
https://www.jianshu.com/p/94852bd1a283

从一个线程池的生命周期进行思考:

后面的8、9、10 点 我都没有把握,我主要在线程池销毁线程的时机,线程空闲的处理上没有搞懂。

  • 8 中的“空闲线程”是不是只指的第二次创建的线程;怎么样才算是“空闲”,是不是工作队列中的任务都执行完,才算呢?
  • 10 中的所有任务执行完毕后,所有线程该何去何从
  1. 启动线程池时,我们需要预先创建并启动若干个线程以用来接收传入的任务。

  2. 创建的线程名称,优先级等待属性需要统一设置,我们可以通过一个特定或默认的工厂进行批量生产

  3. 启动线程后,我们还要对线程进行重复利用,那么需要容器来存取线程

  4. 启动的线程数必须要有限制,不然无尽的线程数会使cpu频繁切换上下文,从而使cpu资源严重浪费,同时大量的线程也会占用大量的内存空间,导致 OOM

  5. 如果传入新任务,但所有线程都在执行任务中,无暇顾及传入的任务。需要将其缓存下来,等待任务完毕的某个线程接收该任务继续工作

  6. 继续传入新任务,导致超出缓存大小,或者缓存过大占用大量内存空间进而 OOM。那么可以增加若干个线程,加快处理任务的进度

  7. 如果还是不停的有任务加入,但是依照现在的资源状况,既不允许将任务缓存,也不能允许增加线程进行处理。就只好寻找一个策略来处理这些的任务。

  8. 任务逐渐处理完毕,不需要新创建的线程就能够应对了,可以将这些线程销毁来降低资源的消耗。不过考虑到万一销毁后马上又有大批新任务处理不过来,于是设置一个空闲等待的销毁时间,这段时间里这些线程还是空闲,则销毁,反之则继续运行。(这里我搞不清到什么时候就判定新创建线程是空闲线程)

  9. 如果运行过程中,需要将线程池停下来,要么所有线程马上停止,要么正在运行的线程运行完毕后停止

  10. 所有任务全部都处理完毕了,所有线程都处于空闲状态了,需要将所有线程封存起来,以尽力降低空闲线程的消耗。

根据上面的10点,就可以得知线程池中比较重要的一些属性和方法:

  1. prestartAllCoreThreads() 方法预热线程池,不断向线程池中加入新的worker线程,直到达到 核心线程数 corePoolSize

  2. 线程工厂 threadFactory ,批量创建线程,定制线程的名称。

  3. 工作线程池 workers,使用 HashSet 用于存取 work 线程。

  4. 核心线程数 corePoolSize,维持线程最基本的线程数,注意尽量不要设置数值很大,如 Integer.MAX_VALUE。不然创建大量线程导致CPU和内存资源过于紧张浪费。

  5. 工作队列 workQueue,是一个阻塞队列,保存等待执行的任务。有很多类,但是注意尽量不要使用无界队列 (界限最大值为Integer.MAX_VALUE),任务入队过多会导致内存紧张进而 OOM,并且会使 maximumPoolSize失效

  6. 最大线程数 maximumPoolSize,用于在阻塞队列满了后,继续创建新线程执行任务。同理不得设置过大

  7. 拒绝策略、饱和策略 handler,在线程池和队列都满的情况下,需要采取一种策略处理新提交的任务。Java 提供的策略有:

    • AbortPolicy :直接抛异常

    • CallerRunPolicy:只用调用者所在的线程来运行任务

    • DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

    • DiscardPolicy:丢弃任务,但是不抛出异常。

  8. 空闲线程存活时间 keepAliveTime,在线程空闲后如果超过这个时间就将其销毁。我认为是销毁的新创建的线程。

  9. shutDown 和 shutDownNow 方法

依旧是 8 9 10点有些拿不准,就不写了。如果某位大佬对这个地方熟悉,请评论区指出!谢谢!

HTTP 的结构

HTTP消息 - HTTP | MDN (mozilla.org)

  1. 起始行:用于区分是响应报文还是请求报文。在请求报文就是请求行,在响应报文就是状态行

    请求行 = 方法 + url + 版本

    状态行 = 版本 + 状态码 + 短语

  2. 首部行:说明浏览器、服务器、或者报文主题的一些信息。首部行有好几行,每行都有换行和回车

  3. 数据体:包括请求携带的信息或者和服务器返回的HTML页面。

三次握手、四次挥手

具体过程

具体过程把课本上的过程走一遍即可

为什么握手要三次,二次、四次不行吗?

主要是防止已经失效的链接请求报文段突然又传到了服务器,因为产生了错误。假设出现一种异常:A发出的第一个连接的请求报文段没有丢失,而是在网络中的某个结点滞留了,一直延迟到连接释放以后的某个时间才到达B,本来这是一个早已经失效的报文段,但是B接收到失效的连接请求后,就会误以为A又发送了一次新的连接请求,于是就向A发出确认报文段,同意建立了连接。如果没有三次握手,新的连接就建立了,之后会一直等A发送数据,资源就会被白白的浪费了。

四次挥手中服务器为何会出现 time-wait 现象?

  • 为了保证A发送的最后一个ACK报文能够到达B.因为这个ACK报文可能会丢失,然后B收不到A发出的ACK报文段的确认。B会超时重传这个FIN+ACK的报文段。A在2MSL重新传一次确认,重新启动2MSL计时器,最后AB都进入CLOSED状态。

  • 防止出现已失效的连接请求报文段。A在发送完最后一个ACK报文段后,在经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。

设计模式-单例模式

我并没有完全了解Java的设计模式,遇到这问题就只答出了下面思路,但是后面的多线程安全问题是一点都不会

私有的构造方法 + 通过构造方法创建私有的静态成员变量 + 公有的静态方法返回成员变量

我这个总结就是按照这篇博客总结的,如果不明白可以看看

用单例模式来讲讲线程安全

懒汉模式: 懒加载,需要时创建

public class Singleton {

    private static Singleton singleton = null;

    private Singleton() {

    }

    public staic Singleton getSingleton() {

        if (singleton == null) {

            singleton = new Singleton();

        }

        return singelton;

    }

}

饿汉模式:类加载时就将实例创建在堆中,浪费内存

public class Singleton {

    private static Singleton singleton = new Singleton;

    private Singleton() {

    }

    public staic Singleton getSingleton() {

        return singelton;

    }

}

单例模式的线程安全问题:

如上所示,如果使用饿汉模式,则没有线程安全问题,获取的是同一个 singleton。但是对于懒汉模式,则会出现安全问题。

例如线程A已经获取到锁,并进入 if 判断语句,如果没有创建对象,需要创建一个对象。但是此时时间片用尽。线程B运行。此时依旧是没有对象,进入 if 语句中,创建一个对象,并返回。时间片交给A,A仍需要创建一个新的对象,并返回。这样就导致出现两个实例,从而出现安全问题。

解决方案:

synchronized 同步方法:

通过对获取方法加锁,保证安全性。

public staic synchronized Singleton getSingleton() {

        if (singleton == null) {

            singleton = new Singleton();

        }

        return singelton;

    } 

但是这样的并发效率低,同一时间只允许一个线程访问。限制 cpu 资源,性能差。

双重同步锁单例模式

不在方法中添加 synchronized 关键字,而是在 if 判断为空时加锁,加锁后再次判断是否为空。通过双重判断和加锁既能够保证为懒汉模式,有能够保证在多线程同时判断实例为空时,声明变量的安全。

public static Singleton getSingleton() {

    if (singleton == null) {

        synchronized(Singleton.class) {

            if (singelton == null) {

                singleton = new Singleton();

            }

        }

    }

    return singleton;

}

但是,在创建变量singleton = new Singleton();时,还是有线程安全问题的:

jvm 在执行该行代码时,会进行以下工作:

  1. memory = allocate(); 分配对象的内存空间

  2. ctorInstance(memory); 初始化对象

  3. singleton = memory; singleton 引用分配好的空间

对于上述操作,jvm有可能会对2 和 3 进行排序,从而变成:

  1. memory = allocate(); 分配对象的内存空间

  2. singleton = memory; singleton 引用分配好的空间

  3. ctorInstance(memory); 初始化对象

这样重排后,执行到 2 就可以得到 singleton 不为空的结果,但其实singleton 还并没有进行初始化。假如线程A在进入第二个 if 后,重排序导致 singleton 引用到了未初始化的对象,恰好cpu被线程B抢占,在判断第一个 if 过程中就发现 singleton 不为空,直接返回了一个没有初始化的对象,从而导致线程安全问题的出现。

volatile + 双重检测机制

改进很简单,由于这是因为jvm的重排序导致的,那么我们只需要将重排序取消掉即可。那么将单例对象加上 volatile 关键字修饰即可。

具体代码不多展示了。

枚举模式-最安全

直接贴原帖子的代码:

public class Singleton {

    // 私有构造函数

    private Singleton() {

    }

    public static Singleton getInstance() {

        return SingletonEnum.INSTANCE.getInstance();

    }

    private enum SingletonEnum {

        INSTANCE;

        private Singleton singleton;

        // JVM保证这个方法绝对只调用一次

        SingletonEnum() {

            singleton = new Singleton();

        }

        public Singleton getInstance() {

            return singleton;

        }

    }

}

redis 数据结构

Redis(1)——5种基本数据结构

  1. SDS 以及和 C 的字符串比较

  2. 链表

  3. 字典 以及扩容、rehash

  4. 集合

  5. 有序集合

redis 根据场景设计:如果一个热点数据,被大量访问怎么办?

这个问题直接给我干碎了,本来就没怎么用过 redis,看过一本书,具体设计还是要靠经验了。。。我没有经验呀。

这个博客的链接我给忘记了,找到就给贴上

  1. 利用二级缓存:

    利用 ehcache(不清楚不了解),或者一个 HashMap,将 key - value 直接存储到 jvm 堆内存中,访问时,直接通过查询 map 中的 key 取出,不需要走到 redis 层。

  2. 备份热 key:

    设置集群,请求时,根据一定规则访问集群中某个的机器。

单点登录

单点登录(SSO)看这一篇就够了
这篇下面的一些讨论挺有参考价值的 什么是单点登录(SSO)

一定要看上面两篇介绍,看完就能大体明白了。

聊聊我做的项目

这个就是看个人项目

总结

这次面试是我第一次正式工作面试,很紧张,加上忙着考驾照,更是毫无胜算(偷偷说一句这次面试安排实在是太快了,我是正月15上午投递简历,下午就约我面试,我脑子一抽,就把面试时间定到了第二天晚7点...)。面试完我就陷入了自我怀疑中,我知道得马上把面经写下来,但是这种无力感真的让我一点写的心情都没有。还好呢,我的学长主动找到我(我没脸找他),帮我分析了问题,给我讲了一些面试技巧,让我恢复好心情。然后告诉我这个难度其实只有中等偏下... 我太菜了

图片说明

不过这都还好吧,能明白自己学的知识并不牢固,自己还很菜很菜,还要继续学习,就是这场面试的最大的意义了。

最后,第一次写这样的面经,多少会有问题。如果大家对我写的这篇面经有疑问,批评,建议,讨论等等,欢迎在评论区指出,只要是善意的,我都会一一接受。如果能够帮到你,点一个大大的赞,更是我的荣幸。谢谢大家!!!

更多模拟面试

11条回帖

回帖
加载中...
话题 回帖

推荐话题

相关热帖

笔经面经近期热帖

历年真题 真题热练榜 24小时
技术(软件)/信息技术类
查看全部

近期精华帖

热门推荐