2020求职笔记(三)

引子

有的地方,当你呼吸到那儿的空气,你身体里的每一根骨头都会告诉你,你不喜欢。

今天是崔小胖和我恋爱六周年的纪念日。有她在,我的世界就有光,任他千难万险,我都不会迷茫。加油吧,年轻人!

题目

1、如果一个对象作为 HashMap 的Key,那这个对象需要做些什么?在put一个键值对的时候,hashCode() 和 equals() 方法谁先被调用?两个对象hashCode()相等,equals()就一定会返回 true么?既然你说到了hash冲突,能详细介绍一下么?

如果一个对象作为HashMap的Key,那么我们需要重写这个对象的 hashCode() 和 equals() 方法。

在put一个键值对的时候时候,hashCode() 先调用;put(K key, V value) 底层实际调用的是 putVal(hash(key), key, value, false, true); 而这里的 hash(key) 方法就会首先调用到 key.hashCode();

  • 两个对象hashCode()相等,equals() 也不一定返回 true;
  • 两个对象 hashCode()不等,equals() 一定返回 false;
  • 两个对象 equals()返回true,hashCode() 一定相等;
  • 两个对象 equals()返回false,hashCode() 可能相等,可能不等;

原因是,可能存在hash冲突,使得两个不同的值在进行hash()函数计算过后,得到相同的hashCode;

对于hash冲突,常用的解决方案有两大类:

  • 开放寻址法
  • 链表法

对于链表法,我们常见的 HashMap,就采用的是这种方式来解决hash冲突。而对于 开放寻址法,又可以分为 线性探测法、二次探测法 和 双重散列法 等。

2、一个线程,调用start()方法两次,会有什么结果?一个线程在执行中如果需要另一个线程的执行结果,怎么处理?如果多个线程先后获取到了同一把锁,然后 wait(),你调用了 notifyAll() 了之后,这些线程怎么办?

如果对一个 Thread对象调用两次 start()方法,第一次正常调用,第二次会报错 IllegalThreadStateException

一个线程需要另一个线程的执行结果,可以采用下面两种方式:

  • Future模式;
  • Object.wait()、Object.notify()

在调用 notifyAll() 之后,会唤醒所有正在该对象上等待的线程,这些线程都会参与锁竞争,最终只有一个线程能持有锁,未拿到锁的,将阻塞。

3、Lock接口了解么?和synchronized关键字有什么区别?你提到了中断,当我们调用方法后,线程会立即被中断么?synchonized关键字是如何实现同步的?锁的可重入性是指什么呢?怎么实现的?你刚才还说到了Condition,一个ReentrantLock可以绑定多个Condition么?

对于Lock接口,我是结合着 ReentrantLock 来讲的,参考:https://blog.csdn.net/Zereao/article/details/105627948#t4

线程中断,涉及到的有三个方法:

// 为调用线程设置一个中断标记
public void interrupt();
// 检查调用线程是否被设置过中断标记
public boolean isInterrupted()

// Thread类的静态方法,判断当前线程是否被中断,并清除中断状态。如果连续两次调用该方法,则第二次调用将返回 false
public static boolean interrupted();

前两个是Thead类的实例方法,最后一个方法是Thread类的静态方法。需要注意的是,当我们调用interrupt()方法后,会出现下面两种情况之一:

  • 如果线程正在执行一个低级可中断的阻塞方法时,例如 Thread.sleep()、threadObj.join()、object.wait()等,线程将取消阻塞,并抛出 InterruptedException
  • 否则,只是给调用线程设置一个中断标记,并不会立即使线程中断;至于目标线程要如何响应这个中断标记,则需要自己去编码做处理。

对于synchronized关键字,在javac编译过后,会在同步块的前后插入 monitorentermonitorexit 两条字节码指令。这两条字节码指令都需要指定一个 reference 类型的参数来指明要锁定和解锁的对象。如果 Java代码中的 synchronized 明确指定了对象参数,那么就以这个对象的引用作为 reference;如果没有明确指定,那就根据 synchronized 修饰的方法类型(实例方法还是类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。

锁的可重入性是指一个线程能够反复进入被他自己持有锁的同步块。可重入性是通过锁关联的计数器来实现的:在执行 monitorenter 指令时,首先要去尝试获取对象的锁。如果这个对象没有被锁定,或者这个对象已经持有了那个对象的锁,就把锁的计数器的值增加 1,而在执行 monitorexit 指令的时候计数器的值就会减1。一旦计数器的值为0,锁就会被释放掉。

一个ReentrantLock可以绑定多个 condition,同时,一个ReentrantLock对象也可以多次调用 lock() 方法实现多次锁定,但与之对应的是,释放的时候也需要释放多次。

4、你们项目中一般哪种线程池用的比较多?能解释下 corePoolSize 和 maxmiumPoolSize 这两个参数有什么区别么?有哪些拒绝策略可选呢?这几个相关的配置,你们一般是怎么决定大小配置多少呢?

常见的线程池都定义在 Executors类中,有下面三种:

  • newFixedThreadPool():返回一个固定线程数量的线程池。
  • newCacheTheadPool():返回一个可动态调整数量的线程池。
  • newScheduledThreadPool():返回一个可以给定时间执行任务的线程池。

此外还有两个比较特殊的:

  • newSingleThreadExecutor:返回一个只有一个线程的线程池
  • newSingleThreadScheduledExecutor:只有一个线程的,可以给定时间执行任务的线程池

在我们的项目中,我们一般是通过ThreadPoolExecutor来自定义线程池的,上面几种常见的线程池实际上也是 ThreadPoolExecutor 的封装。

corePoolSize指定了通常情况下线程池中的线程数量。当我们向线程池中提交一个任务的时候:

  • 如果线程池中的线程数小于 corePoolSize,则会新建一个线程去执行任务;
  • 如果线程数等于corePoolSize,但是有的线程是空闲状态,则复用空闲线程去执行任务;
  • 如果所有线程都被占用,则当前线程进入等待队列;
  • 如果等待队列已满,并且线程池中的线程数小于 maxmiumPoolSize 时,则会新建线程,去执行任务;
  • 如果线程池中的线程数等于 maxPoolSize,并且队列已满,则执行拒绝策略;

拒绝策略有四种:

  • AbortPolicy:直接抛出异常
  • CallerRunsPolicy:在调用者线程中运行被丢弃的任务
  • DiscardPolicy:默默丢弃任务,不予任何处理
  • DiscardOldestPolicy:丢弃最老的一个任务,也就是即将被执行的那个任务

线程池的几个配置,我们一般都是根据经验来定各项数值的大小的,考虑的因素就是 任务并发量、主机配置等。

5、Java内存模型能聊一下么?这里的工作内存相当于Java内存区域中的哪一块呢?Java虚拟机栈中存放了哪些信息呢?volatile关键字能聊一下么?内存屏障能聊一下么?

2020求职笔记(三)_第1张图片

Java内存模型规定,所有的变量都存储在主内存中,每条线程都有自己的工作内存,工作内存保存了主内存中变量的副本。线程对变量的所有操作都必须在 工作内存 中,不能直接操作主内存中的变量。不同线程也不能访问对象的工作内存,线程之间的同步必须要通过主内存来完成。

这里的工作内存,相当于Java内存区域中的Java虚拟机栈,栈中存放了 局部变量表、操作数栈、动态链接、方法出口等信息。

对于volatile关键字,当一个变量被volatile修饰时,有两个效果:

  1. 保证了此变量在多线程环境下的可见性。
  2. 禁止指令重排优化

第一点,保证可见性。在多线程环境下,各工作内存中的变量可能仍然是不一致的,但是由于每次使用前,都需要从主内存刷新,使用完后必须刷新回主内存,Java执行引擎就看不见这种不一致,变现出来就是当一个线程更改了这个值,对其他线程来说是立即可见的。

第二点,禁止指令重排优化。这是通过“内存屏障”来实现的。所谓内存屏障,是指在volatile修饰的变量在赋值完成后,会多执行一条 lock前缀 空操作指令,这个操作就相当于一个“内存屏障”。指令重排时,后边的指令不能重排到内存屏障之前的位置。

对于lock前缀,其作用是 使本CPU的缓存写入内存,同时使其他CPU也无效化其缓存。

6、Spring是如何解决循环依赖的问题的?如果是构造方式注入呢?

Spring是通过“三级缓存”来解决循环依赖的问题的。在Spring中定义了三个Map,来作为缓存:

  • 一级缓存,singletonObjects,存放的是 已经实例化好的单例对象;
  • 二级缓存,earlySingletonObjects,存放的是 还没组装完毕就提前曝光的对象;
  • 三级缓存,singletonFactories,存放的是 即将要被实例化的对象的对象工厂;

当我们需要创建一个bean的时候,首先会从一级缓存singletonObjects中去尝试获取这个bean;如果没有,则会尝试去二级缓存earlySingletonObjects中获取;如果也没有,则会从三级缓存中去获取,找到对应的工厂,获取未完全填充完毕的bean。然后删除三级缓存的数据,并将这个bean填充到二级缓存。

假如依赖关系是 A -> B -> A 这样一个依赖关系。当需要获取A的时候,从一级、二级缓存中获取,都没有,于是就从三级缓存获取A,并将未完全填充完毕的A bean暴露到二级缓存。当继续填充A的其他属性的时候,发现A依赖了B。于是又从一级、二级缓存中去获取B,也没有,于是又从三级缓存获取B,并继续填充B的其他属性。此时发现B又依赖了A,从一级缓存获取A,没有,又从二级缓存获取A,将未完全填充完毕的A赋值给B。这样B就填充完毕了,B会被放到一级缓存中去,同时删除掉B的二级、三级缓存。B填充好了,A也就能填充好了。

如果是构造方式注入,Spring解决不了循环依赖的问题。Spring容器会将每一个正在创建的Bean的名称放到一个 Set中。如果在Bean的创建过程中如果发现自己已经在创建中了,就会抛出 BeanCurrentlyInCreationException 异常。

所以,要解决循环依赖的问题,我们一般使用 属性方式注入 或 setter方式注入。

7、docker网络模型说一下?

当我们在使用 docker run 创建容器时,可以使用 --net 来指定容器的网络模式。docker有下面4中网络模式:

  • host模式:此时容器和宿主机共同同一个网络空间,容器不会虚拟出自己的网卡、IP等。
  • container模式:此时新创建的容器和已经存在的容器共享一个网络空间,但各自的文件系统、系统进程列表还是隔离的。
  • bridge模式:默认的模式,它会为容器生成一个单独的子网络,每一个容器都有自己的网络空间。
  • none模式:此时容器拥有自己的网络空间,但是docker并不为容器进行任何网络配置。

8、TCP/IP四层架构和OSI七层架构之间是如何对应的?

2020求职笔记(三)_第2张图片

9、CPU调度算法有哪些?内核态和用户态有什么区别?

CPU调度算法有下面几种:

  • 先到先服务调度算法
  • 最短作业优先调度算法
  • 优先级调度算法
  • 时间片轮转算法
  • 多级队列调度
  • 多级反馈队列调度

具体的详情参考:https://www.cnblogs.com/PIRATE-JFZHOU/p/8094790.html

当一个进程运行用户自己的代码时,处于用户态;当进程因为系统调用执行内核代码时,处于内核态。

参考文档

1、《计算机网络》第七版,谢希仁·著,第一章

2、https://www.cnblogs.com/PIRATE-JFZHOU/p/8094790.html

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