面试相关

面试相关的一些问题

Posted by ZBX on April 9, 2020

一致性Hash

  • 单调性: 如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
  • 简单的说,单调性要求在移除 / 添加一个 cache(机器,ip)时,它能够尽可能小的改变已存在 key 映射关系。
  • 平衡性

Hash环的数据倾斜问题

一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题。

  • 虚拟节点机制
    • 即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。
    • 在实际应用中,通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。

如何避免线程死锁?

  • 破坏请求等待,一次性申请资源
  • 破坏不可剥夺,申请不到就放弃资源
  • 破坏循环等待,按序申请

ThreadLocal有什么缺陷

  • 每个Thread线程内部都有一个Map。Map里面存储线程本地对象(key)和线程的变量副本(value)

  • ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。
  • Entry继承自WeakReference(弱引用,生命周期只能存活到下次GC前),但只有Key是弱引用类型的,Value并非弱引用。当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。
  • 针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。

如何避免泄漏

  1. 使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
  2. JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。

常见软件架构模式

分层模式

这个模式常常用于构建大型系统,将系统分成不同的抽象层次,每一层都为上层提供好用的API,并屏蔽掉下层的细节。

  • 一般桌面程序
  • 电子商务网页程序

客户端-服务器模式

服务器组件对多个客户端组件提供服务。客户端向服务器端请求服务,服务端提供对应服务给这些客户端。

  • 在线应用,比如电子邮件、文档分享和银行业务

主从模式

主节点和多个从节点。主节点组件向多个独立的从节点组件分派任务,并根据从节点返回结果计算出最终结果。

  • 数据库复制,主数据库被视为权威来源并同步到从数据库
  • 连接到计算系统的外围设备(主从驱动)

管道-过滤器模式

该模式用于构建生产和处理数据流的系统。每个处理步骤封装在一个过滤器组件中。待处理的数据被传送到管道之中,这些管道可用于缓冲或者同步。

  • 编译器,接连的过滤器执行词义分析,语法分析,语义分析和代码生成
  • 生物资料学科的工作流

代理模式

用于构建组件解耦的分布式系统。这些组件通过远程调用彼此交互。代理组件负责多个组件的通信协调,服务器向代理公开他们的能力(服务和特性);客户端从代理中获取服务,然后代理重定向客户端到注册服务库中一个合适的服务。

  • 消息队列软件,比如 Apache ActiveMQ、Apache Kafka、RabbitMQ 和 JBoss Messaging

点对点模式

  • 文件分享网络,比如 GnutellaG2
  • 多媒体协议,比如 P2PTV 和 PDTP
  • 私媒体程序,比如 Spotify

事件总线模式

该模式主要处理事件,有4个主要组件:事件源,事件监听器,频道和事件总线。事件源发布消息到事件总线上的某个频道,监听器订阅某个频道,并得知在已订阅频道中发布的消息。

  • Android 开发
  • 通知服务

MVC 模式

划分交互程序为3个部分:模型——包含核心功能和数据,视图——显示信息给用户(多个视图可被定义),控制器——处理用户输入。它通过分割用户信息的内部陈述和呈现、接受方式来实现,解耦组件并允许高效的代码复用。

  • 主流编程语言的万维网程序架构
  • 网页框架,比如 Django 和 Rails

黑板模式

该模式对没有确定性方案策略的问题很有用。黑板模式由三个主要组件组成,黑板——包含解空间对象的结构化全局内存,知识源——有自拥表示的专门模块,控制组件——选择、配置和执行模块。所有组件都可访问黑板,可生成新的数据对象并添加到黑板中。在黑板中,可根据已有知识源的匹配规则,寻找某些类型的数据。

  • 语音识别
  • 车辆识别和跟踪
  • 蛋白质结构鉴定
  • 声呐信号解释

解释器模式

该模式用于设计解释特定语言编写的程序的组件。该组件主要指定怎么去评估程序代码行,也就是所谓的用某种语言写的语句或者表达式,基本点在于给语言符号分类。

  • 数据库查询语言,比如 SQL
  • 用于描述通信协议的语言

数据库与缓存数据一致性问题

  1. 缓存必须要有过期时间
  2. 保证数据库跟缓存的最终一致性即可,不必追求强一致性

Cache Aside Pattern

  1. 失效:程序先从缓存中读取数据,如果没有命中,则从数据库中读取,成功之后将数据放到缓存中
  2. 命中:程序先从缓存中读取数据,如果命中,则直接返回
  3. 更新:程序先更新数据库,在删除缓存

先更新数据库,再删除缓存

Executor vs ExecutorService vs Executors

  • Executor 和 ExecutorService 这两个接口主要的区别是:
    • ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口;
    • Executor 接口定义了 execute()方法用来接收一个Runnable接口的对象,而 ExecutorService 接口中的 submit()方法可以接受RunnableCallable接口的对象;
    • Executor 中的 execute() 方法不返回任何结果,而 ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。
    • 除了允许客户端提交一个任务,ExecutorService 还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。
  • Executors 类提供工厂方法用来创建不同类型的线程池。比如: newSingleThreadExecutor() 创建一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)来创建固定线程数的线程池newCachedThreadPool()可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。

InnoDB行级锁

InnoDB的行级锁定同样分为两种类型:共享锁排他锁,而在锁定机制的实现过程中为了让行级锁定和表级锁定共存,InnoDB也同样使用了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁这两种。

InnoDB行锁是通过给索引上的索引项加锁来实现的。所以,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。其他注意事项:

  • 在不通过索引条件查询的时候,InnoDB使用的是表锁,而不是行锁。
  • 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以即使是访问不同行的记录,如果使用了相同的索引键,也是会出现锁冲突的。

构造代码块

  1. 构造代码块的作用是给对象进行初始化。
  2. 对象一建立就运行构造代码块了,而且优先于构造函数执行。这里要强调一下,有对象建立,才会运行构造代码块,类不能调用构造代码块的,而且构造代码块与构造函数的执行顺序是前者先于后者执行
  3. 构造代码块与构造函数的区别是:构造代码块是给所有对象进行统一初始化,而构造函数是给对应的对象初始化,因为构造函数是可以多个的,运行哪个构造函数就会建立什么样的对象,但无论建立哪个对象,都会先执行相同的构造代码块。也就是说,构造代码块中定义的是不同对象共性的初始化内容。

UTF-8编码

UTF-8是Unicode的实现方式之一。

UTF-8全称:8bit Unicode Transformation Format,8比特Unicode通用转换格式。UTF-8是一种针对Unicode的可变长度字符编码。可以表示Unicode标准中的任何一个字符,且其编码中的第一个字节仍然与ASCII兼容。

UTF-8是一种变长的编码方式,可以使用1~6个字节对Unicode字符集进行编码,编码规则如下:

  1. 对于单字节的符号, 字节的第一位设为0, 后面7位为这个符号的unicode码. 因此对于 英语字母, UTF-8编码和ASCII码是相同的.
  2. 对于n字节的符号(n>1), 第一个字节的前n位都设为1, 第n+1位设为0, 后面字节的前 两位一律设为10. 剩下的没有提及的二进制位, 全部为这个符号的unicode码.
n Unicode符号范围 UTF-8编码方式
1 0000 0000 - 0000 007F 0xxxxxxx
2 0000 0080 - 0000 07FF 110xxxxx 10xxxxxx
3 0000 0800 - 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
4 0001 0000 - 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5 0020 0000 - 03FF FFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
6 0400 0000 - 7FFF FFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

注:在UTF-8编码中,英文字符占一个字节,中文字符占用3个字节。

NIO

  • 非阻塞IO,实现了IO多路复用中的Reactor模型,一个线程Thread使用一个选择器Selector通过轮询的方式去监听多个通道Channel上的事件,从而让一个线程就可以处理多个事件。
  • 通过配置监听的通道Channel为非阻塞,那么当Channel上的IO事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其他Channel,找到IO事件已经到达的Channel执行。
  • 对于IO密集型的应用具有很好的性能。

BigInteger实现原理

  • BigInteger存储大数的方式就是将数字存储在一个整型的数组中。

  • signum属性是为了区分:正负数和0的标志位,整数用1表示,负数用-1表示,零用0表示

  • mag是magnitude的缩写形式,mag数组存储BigInteger数值大小

    • big-endian 的顺序,即:高位字节存入低地址,低位字节存入高地址,依次排列的方式
  • 如何mag数组转换为原来的数串

    • 不断做除法取余

BigDecimal实现原理

public class BigDecimal {
    //值的绝对long型表示
    private final transient long intCompact;
    //值的小数点后的位数
    private final int scale;
 
    private final BigInteger intVal;
    //值的有效位数,不包含正负符号
    private transient int precision;
    private transient String stringCache;
     
    //加、减、乘、除、绝对值
    public BigDecimal add(BigDecimal augend) {}
    public BigDecimal subtract(BigDecimal subtrahend) {}
    public BigDecimal multiply(BigDecimal multiplicand) {}
    public BigDecimal divide(BigDecimal divisor) {}
    public BigDecimal abs() {}
}

影响服务器的最大请求数有哪些因素

  • 硬件的上的限制,比如CPU、内存

  • 文件句柄的限制

    • 文件句柄进程级限制

      • 在linux下每一个tcp连接都要占一个文件描述符,如果达到上限,就会出现错误:“Socket/File:Can’t open so many files”。操作系统对可以打开的最大文件数有限制。执行 ulimit -n 输出 1024,说明对于一个进程而言最多只能打开1024个文件,所以你要采用此默认配置最多也就可以并发上千个TCP连接。临时修改:ulimit -n 1000000/etc/security/limits.conf文件soft nofile 1000000

      • 永久修改

        编辑/etc/rc.local,在其后添加ulimit -SHn 1000000

    • 文件句柄全局限制

      • 执行 cat /proc/sys/fs/file-nr ,输出 9344 0 592026。分别为:1.已经分配的文件句柄数,2.已经分配但没有使用的文件句柄数,3.最大文件句柄数。
  • 端口范围限制

    • 操作系统上端口号1024以下是系统保留的,从1024-65535是用户使用的。

服务端如何识别每个请求对应哪个用户

  • Cookie通过在客户端记录信息确定用户身份Session通过在服务器端记录信息确定用户身份。

  • Session对象是在客户端第一次请求服务器的时候创建的。
  • Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,并维护该Session。
  • 为防止内存溢出,服务器会把长时间内没有活跃的Session从内存删除。这个时间就是Session的超时时间。如果超过了超时时间没访问过服务器,Session就自动失效了。

线程池

ThreadPoolExecutor 类

 /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */
    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               ) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

ThreadPoolExecutor 7 个参数:

  • corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销
  • unit : keepAliveTime 参数的时间单位。
  • threadFactory :executor 创建新线程的时候会用到。
  • handler :饱和策略。关于饱和策略下面单独介绍一下。

ThreadPoolExecutor 饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy:此策略将丢弃最早的未处理的任务请求。