做网站如何对接支付,宁阳县网络seo,淄博企业网站,微信小程序开发价格现在已经到了面试招聘比较火热的时候#xff0c;准备面试的过程中#xff0c;一定要多看面经#xff0c;多自测#xff01;
今天分享的是一位贵州大学的同学分享的快手一面面经。
快手一面主要会问一些基础问题#xff0c;也就是比较简单且容易准备的常规八股#xff0…现在已经到了面试招聘比较火热的时候准备面试的过程中一定要多看面经多自测
今天分享的是一位贵州大学的同学分享的快手一面面经。
快手一面主要会问一些基础问题也就是比较简单且容易准备的常规八股通常不会问项目。到了二面会开始问项目各种问题也挖掘的更深一些。 很多同学觉得这种基础问题的考查意义不大实际上还是很有意义的这种基础性的知识在日常开发中也会需要经常用到。例如线程池这块的拒绝策略、核心参数配置什么的如果你不了解实际项目中使用线程池可能就用的不是很明白容易出现问题。而且其实这种基础性的问题是最容易准备的像各种底层原理、系统设计、场景题以及深挖你的项目这类才是最难的
1、Long 的长度和范围为什么要减 1
先来复习一下 Java 中的 8 种基本数据类型 6 种数字类型 4 种整数型byte、short、int、long 2 种浮点型float、double 1 种字符类型char 1 种布尔型boolean。
这 8 种基本数据类型的默认值以及所占空间的大小如下
基本类型位数字节默认值取值范围byte810-128 ~ 127short1620-32768-2^15 ~ 327672^15 - 1int3240-2147483648 ~ 2147483647long6480L-9223372036854775808-2^63 ~ 92233720368547758072^63 -1char162u00000 ~ 655352^16 - 1float3240f1.4E-45 ~ 3.4028235E38double6480d4.9E-324 ~ 1.7976931348623157E308boolean1falsetrue、false
可以看到像 byte、short、int、long能表示的最大正数都减 1 了。这是为什么呢这是因为在二进制补码表示法中最高位是用来表示符号的0 表示正数1 表示负数其余位表示数值部分。所以如果我们要表示最大的正数我们需要把除了最高位之外的所有位都设为 1。如果我们再加 1就会导致溢出变成一个负数。
对于 boolean官方文档未明确定义它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位但是实际中会考虑计算机高效存储因素。
另外Java 的每种基本类型所占存储空间的大小不会像其他大多数语言那样随机器硬件架构的变化而变化。这种所占存储空间大小的不变性是 Java 程序比用其他大多数语言编写的程序更具可移植性的原因之一《Java 编程思想》2.2 节有提到。
2、JAVA 异常的层次结构
Java 异常类层次结构图概览 Java 异常类层次结构图
在 Java 中所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类: Exception :程序本身可以处理的异常可以通过 catch 来进行捕获。Exception 又可以分为 Checked Exception (受检查异常必须处理) 和 Unchecked Exception (不受检查异常可以不处理)。 **Error**Error 属于程序无法处理的错误 我们没办法通过 catch 来进行捕获不建议通过catch捕获 。例如 Java 虚拟机运行错误Virtual MachineError、虚拟机内存不够错误(OutOfMemoryError)、类定义错误NoClassDefFoundError等 。这些异常发生时Java 虚拟机JVM一般会选择线程终止。
3、JAVA 的集合类有了解么
Java 集合 也叫作容器主要是由两大接口派生而来一个是 Collection接口主要用于存放单一元素另一个是 Map 接口主要用于存放键值对。对于Collection 接口下面又有三个主要的子接口List、Set 和 Queue。
Java 集合框架如下图所示 Java 集合框架概览
注图中只列举了主要的继承派生关系并没有列举所有关系。比方省略了AbstractList, NavigableSet等抽象类以及其他的一些辅助类如想深入了解可自行查看源码。 List(对付顺序的好帮手): 存储的元素是有序的、可重复的。 Set(注重独一无二的性质): 存储的元素不可重复的。 Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序存储的元素是有序的、可重复的。 Map(用 key 来搜索的专家): 使用键值对key-value存储类似于数学上的函数 yf(x)x 代表 keyy 代表 valuekey 是无序的、不可重复的value 是无序的、可重复的每个键最多映射到一个值。
4、ArrayList 和 LinkedList 区别 是否保证线程安全 ArrayList 和 LinkedList 都是不同步的也就是不保证线程安全 底层数据结构 ArrayList 底层使用的是 Object 数组LinkedList 底层使用的是 双向链表 数据结构JDK1.6 之前为循环链表JDK1.7 取消了循环。注意双向链表和双向循环链表的区别下面有介绍到 插入和删除是否受元素位置的影响 ArrayList 采用数组存储所以插入和删除元素的时间复杂度受元素位置的影响。比如执行add(E e)方法的时候 ArrayList 会默认在将指定的元素追加到此列表的末尾这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话add(int index, E element)时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 LinkedList 采用链表存储所以在头尾插入或者删除元素不受元素位置的影响add(E e)、addFirst(E e)、addLast(E e)、removeFirst()、 removeLast()时间复杂度为 O(1)如果是要在指定位置 i 插入和删除元素的话add(int index, E element)remove(Object o),remove(int index) 时间复杂度为 O(n) 因为需要先移动到指定位置再插入和删除。 是否支持快速随机访问 LinkedList 不支持高效的随机元素访问而 ArrayList实现了 RandomAccess 接口 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。 内存空间占用 ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间因为要存放直接后继和直接前驱以及数据。
我们在项目中一般是不会使用到 LinkedList 的需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替并且性能通常会更好就连 LinkedList 的作者约书亚 · 布洛克Josh Bloch自己都说从来不会使用 LinkedList 。 另外不要下意识地认为 LinkedList 作为链表就最适合元素增删的场景。我在上面也说了LinkedList 仅仅在头尾插入或者删除元素的时候时间复杂度近似 O(1)其他情况增删元素的平均时间复杂度都是 O(n) 。 补充内容: 双向链表和双向循环链表 双向链表 包含两个指针一个 prev 指向前一个节点一个 next 指向后一个节点。 双向链表
双向循环链表 最后一个节点的 next 指向 head而 head 的 prev 指向最后一个节点构成一个环。 双向循环链表 补充内容:RandomAccess 接口 public interface RandomAccess {
}查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以在我看来 RandomAccess 接口不过是一个标识罢了。标识什么标识实现这个接口的类具有随机访问功能。
在 binarySearch) 方法中它要判断传入的 list 是否 RandomAccess 的实例如果是调用indexedBinarySearch()方法如果不是那么调用iteratorBinarySearch()方法 public static Tint binarySearch(List? extends Comparable? super T list, T key) {if (list instanceof RandomAccess || list.size()BINARYSEARCH_THRESHOLD)return Collections.indexedBinarySearch(list, key);elsereturn Collections.iteratorBinarySearch(list, key);}ArrayList 实现了 RandomAccess 接口 而 LinkedList 没有实现。为什么呢我觉得还是和底层数据结构有关ArrayList 底层是数组而 LinkedList 底层是链表。数组天然支持随机访问时间复杂度为 O(1)所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素时间复杂度为 O(n)所以不支持快速随机访问。ArrayList 实现了 RandomAccess 接口就表明了他具有快速随机访问功能。RandomAccess 接口只是标识并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的
5、HashMap 有了解么它的底层实现为什么线程不安全
HashMap 主要用来存放键值对它基于哈希表的 Map 接口实现是常用的 Java 集合之一是非线程安全的。
HashMap 可以存储 null 的 key 和 value但 null 作为键只能有一个null 作为值可以有多个
JDK1.8 之前 HashMap 由 数组链表 组成的数组是 HashMap 的主体链表则是主要为了解决哈希冲突而存在的“拉链法”解决冲突。JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化当链表长度大于等于阈值默认为 8将链表转换成红黑树前会判断如果当前数组的长度小于 64那么会选择先进行数组扩容而不是转换为红黑树时将链表转化为红黑树以减少搜索时间。
HashMap 默认的初始化大小为 16。之后每次扩充容量变为原来的 2 倍。并且 HashMap 总是使用 2 的幂作为哈希表的大小。
JDK1.7 及之前版本在多线程环境下HashMap 扩容时会造成死循环和数据丢失的问题。
数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在这里以 JDK 1.8 为例进行介绍。
JDK 1.8 后在 HashMap 中多个键值对可能会被分配到同一个桶bucket并以链表或红黑树的形式存储。多个线程对 HashMap 的 put 操作会导致线程不安全具体来说会有数据覆盖的风险。
举个例子 两个线程 1,2 同时进行 put 操作并且发生了哈希冲突hash 函数计算出的插入下标是相同的。 不同的线程可能在不同的时间片获得 CPU 执行的机会当前线程 1 执行完哈希冲突判断后由于时间片耗尽挂起。线程 2 先完成了插入操作。 随后线程 1 获得时间片由于之前已经进行过 hash 碰撞的判断所有此时会直接进行插入这就导致线程 2 插入的数据被线程 1 覆盖了。
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// ...// 判断是否出现 hash 碰撞// (n - 1) hash 确定元素存放在哪个桶中桶为空新生成结点放入桶中(此时这个结点是放在数组中)if ((p tab[i (n - 1) hash]) null)tab[i] newNode(hash, key, value, null);// 桶中已经存在元素处理hash冲突else {// ...
}还有一种情况是这两个线程同时 put 操作导致 size 的值不正确进而导致数据覆盖的问题 线程 1 执行 if(size threshold) 判断时假设获得 size 的值为 10由于时间片耗尽挂起。 线程 2 也执行 if(size threshold) 判断获得 size 的值也为 10并将元素插入到该桶位中并将 size 的值更新为 11。 随后线程 1 获得时间片它也将元素放入桶位中并将 size 的值更新为 11。 线程 1、2 都执行了一次 put 操作但是 size 的值只增加了 1也就导致实际上只有一个元素被添加到了 HashMap 中。
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {// ...// 实际大小大于阈值则扩容if (size threshold)resize();// 插入后回调afterNodeInsertion(evict);return null;
}6、CoucurHashMap 和 HashTable
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 底层数据结构 JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组链表 实现JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样数组链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组链表 的形式数组是 HashMap 的主体链表则是主要为了解决哈希冲突而存在的 实现线程安全的方式重要 在 JDK1.7 的时候ConcurrentHashMap 对整个桶数组进行了分割分段(Segment分段锁)每一把锁只锁容器其中一部分数据下面有示意图多线程访问容器里不同数据段的数据就不会存在锁竞争提高并发访问率。 到了 JDK1.8 的时候ConcurrentHashMap 已经摒弃了 Segment 的概念而是直接用 Node 数组链表红黑树的数据结构来实现并发控制使用 synchronized 和 CAS 来操作。JDK1.6 以后 synchronized 锁做了很多优化 整个看起来就像是优化过且线程安全的 HashMap虽然在 JDK1.8 中还能看到 Segment 的数据结构但是已经简化了属性只是为了兼容旧版本 Hashtable(同一把锁) :使用 synchronized 来保证线程安全效率非常低下。当一个线程访问同步方法时其他线程也访问同步方法可能会进入阻塞或轮询状态如使用 put 添加元素另一个线程不能使用 put 添加元素也不能使用 get竞争会越来越激烈效率越低。
下面我们再来看看两者底层数据结构的对比图。
Hashtable : Hashtable 的内部结构
https://www.cnblogs.com/chengxiao/p/6842045.html
JDK1.7 的 ConcurrentHashMap Java7 ConcurrentHashMap 存储结构
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 数组中的每个元素包含一个 HashEntry 数组每个 HashEntry 数组属于链表结构。
JDK1.8 的 ConcurrentHashMap Java8 ConcurrentHashMap 存储结构
JDK1.8 的 ConcurrentHashMap 不再是 Segment 数组 HashEntry 数组 链表而是 Node 数组 链表 / 红黑树。不过Node 只能用于链表的情况红黑树的情况需要使用 TreeNode。当冲突链表达到一定长度时链表会转换成红黑树。
TreeNode是存储红黑树节点被TreeBin包装。TreeBin通过root属性维护红黑树的根结点因为红黑树在旋转的时候根结点可能会被它原来的子节点替换掉在这个时间点如果有其他线程要写这棵红黑树就会发生线程不安全问题所以在 ConcurrentHashMap 中TreeBin通过waiter属性维护当前使用这棵红黑树的线程来防止其他线程的进入。
static final class TreeBinK,V extends NodeK,V {TreeNodeK,V root;volatile TreeNodeK,V first;volatile Thread waiter;volatile int lockState;// values for lockStatestatic final int WRITER 1; // set while holding write lockstatic final int WAITER 2; // set when waiting for write lockstatic final int READER 4; // increment value for setting read lock
...
}7、线程池有了解么讲一下。
顾名思义线程池就是管理一系列线程的资源池其提供了一种限制和管理线程资源的方式。每个线程池还维护一些基本统计信息例如已完成任务的数量。
这里借用《Java 并发编程的艺术》书中的部分内容来总结一下使用线程池的好处 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 提高响应速度。当任务到达时任务可以不需要等到线程创建就能立即执行。 提高线程的可管理性。线程是稀缺资源如果无限制的创建不仅会消耗系统资源还会降低系统的稳定性使用线程池可以进行统一的分配调优和监控。
线程池一般用于执行多个不相关联的耗时任务没有多线程的情况下任务顺序执行使用了线程池的话可让多个不相关联的任务同时执行。
8、线程池配置无界队列了之后拒绝策略怎么搞什么时候用到无界对列
线程池配置无界队列了之后拒绝策略其实就失去了意义因为无论有多少任务提交到线程池都会被放入队列中等待执行不会触发拒绝策略。不过这样可能堆积大量的请求从而导致 OOM。因此一般不推荐使用误解队列。
假设不是无界队列的话如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时ThreadPoolTaskExecutor 定义一些拒绝策略: ThreadPoolExecutor.AbortPolicy抛出 RejectedExecutionException来拒绝新任务的处理。 ThreadPoolExecutor.CallerRunsPolicy调用执行自己的线程运行任务也就是直接在调用execute方法的线程中运行(run)被拒绝的任务如果执行程序已关闭则会丢弃该任务。因此这种策略会降低对于新任务提交速度影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话你可以选择这个策略。 ThreadPoolExecutor.DiscardPolicy不处理新任务直接丢弃掉。 ThreadPoolExecutor.DiscardOldestPolicy此策略将丢弃最早的未处理的任务请求。
举个例子
Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 这代表你将丢失对这个任务的处理。对于可伸缩的应用程序建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时此策略为我们提供可伸缩队列这个直接查看 ThreadPoolExecutor 的构造函数源码就可以看出比较简单的原因这里就不贴代码了。
9、MVCC 讲一下
MVCC 是多版本并发控制方法即对一份数据会存储多个版本通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号版本号是唯一的。
MVCC 在 MySQL 中实现所依赖的手段主要是: 隐藏字段、read view、undo log。 undo log : undo log 用于记录某行数据的多个版本的数据。 read view 和 隐藏字段 : 用来判断当前版本数据的可见性。
关于 InnoDB 对 MVCC 的具体实现可以看这篇文章[InnoDB 存储引擎对 MVCC 的实现]( InnoDB 存储引擎对 MVCC 的实现) 。
10、事务特性、隔离级别
关系型数据库例如MySQL、SQL Server、Oracle 等事务都有 ACID 特性 ACID 原子性Atomicity事务是最小的执行单位不允许分割。事务的原子性确保动作要么全部完成要么完全不起作用 一致性Consistency执行事务前后数据保持一致例如转账业务中无论事务是否成功转账者和收款人的总额应该是不变的 隔离性Isolation并发访问数据库时一个用户的事务不被其他事务所干扰各并发事务之间数据库是独立的 持久性Durability一个事务被提交之后。它对数据库中数据的改变是持久的即使数据库发生故障也不应该对其有任何影响。 这里要额外补充一点只有保证了事务的持久性、原子性、隔离性之后一致性才能得到保障。也就是说 A、I、D 是手段C 是目的 想必大家也和我一样被 ACID 这个概念被误导了很久! 我也是看周志明老师的公开课《周志明的软件架构课》[1]才搞清楚的多看好书。 AID-C
另外DDIA 也就是 《Designing Data-Intensive Application数据密集型应用系统设计》[2] 的作者在他的这本书中如是说 Atomicity, isolation, and durability are properties of the database, whereas consis‐ tency (in the ACID sense) is a property of the application. The application may rely on the database’s atomicity and isolation properties in order to achieve consistency, but it’s not up to the database alone. 翻译过来的意思是原子性隔离性和持久性是数据库的属性而一致性在 ACID 意义上是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性但这并不仅取决于数据库。因此字母 C 不属于 ACID 。 《Designing Data-Intensive Application数据密集型应用系统设计》这本书强推一波值得读很多遍豆瓣有接近 90% 的人看了这本书之后给了五星好评。另外中文翻译版本已经在 GitHub 开源地址https://github.com/Vonng/ddia[3] 。 SQL 标准定义了四个隔离级别 READ-UNCOMMITTED(读取未提交) 最低的隔离级别允许读取尚未提交的数据变更可能会导致脏读、幻读或不可重复读。 READ-COMMITTED(读取已提交) 允许读取并发事务已经提交的数据可以阻止脏读但是幻读或不可重复读仍有可能发生。 REPEATABLE-READ(可重复读) 对同一字段的多次读取结果都是一致的除非数据是被本身事务自己所修改可以阻止脏读和不可重复读但幻读仍有可能发生。 SERIALIZABLE(可串行化) 最高的隔离级别完全服从 ACID 的隔离级别。所有的事务依次逐个执行这样事务之间就完全不可能产生干扰也就是说该级别可以防止脏读、不可重复读以及幻读。 隔离级别脏读不可重复读幻读READ-UNCOMMITTED√√√READ-COMMITTED×√√REPEATABLE-READ××√SERIALIZABLE×××
MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。
SERIALIZABLE 隔离级别是通过锁来实现的READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过 SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ可重读。我们可以通过SELECT tx_isolation;命令来查看MySQL 8.0 该命令改为SELECT transaction_isolation;
mysql SELECT tx_isolation;
-----------------
| tx_isolation |
-----------------
| REPEATABLE-READ |
-----------------关于 MySQL 事务隔离级别的详细介绍可以看看我写的这篇文章MySQL 事务隔离级别详解[4]。