• 问 我们先来说一下: HashMap 的工作原理是什么?

HashMap底层是通过数组和单向链表实现的,数组中的每个元素都是一个链表,而链表中的每个节点都是一个Entry对象,它里面存储的才是真正的KV数据,然后我们在存储键值对的时候,会先去调用哈希方法计算Key的哈希值,然后去模这个数组的长度,就能拿到存储Key的数组的下标,然后呢,我们遍历这个下标里面存储的这个单向链表,把里面每一个Key,还有插入的Key进行equal比较,如果在这个时候返回true的话,就直接更新Value值,如果全部比较,都返回了False,那就直接把新的键值对插入到这个链表中,在put的过程中,当哈希表中存储的键值对超过了数组长度乘以负载因子的时候,就会将这个数组扩容两倍,还有就是在插入链表的时候,如果链表长度超过了一个阈值,就会从链表转化成红黑树,然后我们那个时候阈值默认是8,在查询KV数据的时候,我们会先调用哈希方法计算Key的哈希值,然后去模数组的长度,这样就能拿到存储Key的数组下标,然后呢我们在遍历其中的链表,进行equa比较,得到Value 值,那哈希表的核心原理呢 ,其实就是hash值过滤出Key 的下标位置,equal解决hash 冲突的问题

  • 问: 你刚才提到Hash表中链表超长之后会变成红黑树,那这里为什么要使用红黑树呢?

红黑树呢是二叉查找树的一种,它的查找算法就类似于一种二分查找法,而时间复杂度O(log n),在数据比较多的时候要比链表查询的回(n)时间复杂度要好很多

  • 问: 那我们回不可以不使用链表﹖直接使用红黑树呢?或者使用二叉搜索树或是AVL树之类的数据结构呢?

之所以没有一直使用红黑树,可能是因为时间和空间的折中吧,在哈希冲突比较少的时候,我们即使转化成红黑树之后,在时间复杂度上带来的收益不是特别大,而且插入的时候效率可能还会降低,毕竟是要进行复杂的红黑树旋转操作,空间上每个节点需要维护更多的指针,就有种得不偿失的感觉,然后我们之所以选择红黑树而不是选择搜索树是因为二叉搜索树在一些极端的情况下会变成一个倾斜的结构查找效率就会退化成了链表,红黑树呢是一个平衡树,可以防止二叉搜索树的这种退化,然后可以保持平衡,但是红黑树呢叉不像完全平衡二叉树那样有严格的这种平衡条件,所以红黑数的插入效率要高于完全平衡二叉树,所以我们会选择AVL树,这时候既可以处理极端情况的退化,又可以兼顾查询和插入的效率,所以是一个不错的选择

  • 问:HashMap是线程安全的吗?在并发环境下,如果需要一个哈希结构﹐你会怎么实现呢?

这时候我会使用ConcurrentHashMap

  • 问: ConcurrentHashMap 的底层实现有了解吗?

ConcurrentHashMap在JDK 1.7和1.8之间的实现差距是比较大的,在JDK 1.7中底层是使用数组+链表的结构,然后并发方面是分段加锁的思想,它是将数组分为了16个段也就是segment,然后每个Segment配一把锁,我们在进行读写每个segment的时候都需要获取对应的锁嘛,所以最多能有16个线程并发去操作ConcurrentHashMap,然而在JDK 1.8里面,ConcurrentHashMap 的这个底层结构,和前面我们说的HashMap是相同的,也有引入了一个红黑树的优化,我们在并发方面,就不会再使用分段加锁的方式,而是采用CAS +synchronized关键字的方式去实现更加细粒度的锁来去提高这个并发度,然后把这个锁控制在更加细粒度的哈希桶级别,再写入这个键值对的时候锁住哈希桶的链表头节点,这样就不会影响其他的哈希桶写入,然后就能够提高并发度

  • 问:你刚才提到ConcurrentHashMap中使用的是synchronized关键字,那使用 ReentrantLock来作为锁,是不是也是可以的

嗯是可以的,但是synchronized关键字会更好一点,JDK1.6里,对synchronized关键字进行了一些优化,引入了从偏向锁、轻量级锁、重量级锁,这些在 ReentrantLock中都是没有的,然后随着这个JDK版本的升级,synchronized可以得到更多的提升,但是ReentrantLock是Java 代码实现的,很难有特别大的提升了,所以说让我选的话,我优先会使用synchronized关键字,然后次之才会选择 ReentrantLock

  • 问:简单展开介绍一下synchronized关键字锁的一些优化吧

首先是偏向锁,就是程序运行的过程当中,始终只有一个线程去获取synchronized锁,Java对象头里面会记录这个线程的ID,所以我们下次再获取这个,synchronized锁的时候,只需要比较这个线程ID就行,当运行过程中,出现第二个线程去,去请求synchronized锁的时候,但是没有发生锁竞争的情况下,synchronized就会就会升级为轻量级锁,这个时候呢,这第二个线程就会尝试自旋的方式获取锁,因为很快就能拿到锁嘛,所以这第二个线程也不会阻塞,如果两个线程同时去竞争锁,synchronized就会升级为重量级锁,这个时候只有一个线程能获取到锁另一个线程呢就会阻塞等待锁

  • 问:在JVM里面垃圾回收大概有几种算法呢?

主要有三种算法标记清除、标记复制、标记整理,标记清除的核心呢就是先标记出存活的对象,没有被标记的对象就是需要被回收的垃圾对象了,然后垃圾回收器直接回收这些垃圾对象就行,然后标记复制算法的核心呢就是把内存分成对等的两部分,每次只使用其中的一部分,在使用中的这部分满了之后呢就先标记出存活的对象,然后把存活的对象拷贝到空闲的另一部分当中,留在另一部分的对象当中,全部都需要回收的垃圾直接清理掉就可以,然后这样就完成了一次GC,在这之后空闲的部分就会变成使用中的状态,原来使用中的那部分,就会切换成空闲的状态,就会一直重复这个循环,最后一个是标记整理算法的核心呢,其实就是先标记出存储的对象,然后把所有存活的对象整理到,内存空间的另一端,没有被标记的对象就可以被覆盖,或者是释放

  • 问:这些垃圾回收算法有哪些优缺点呢?分别适用于哪些场景进行使用呢?

标记清除算法主要的问题呢,就是它会产生内存的碎片,这些内存的碎片随着这个系统运行久了之后,就会导致我们没有办法分配大量连续的内存,这样的话就会多次触发垃圾回收的操作,然后这个标记复制算法最主要的问题呢,就是它会导致我们只用50%的空间,因为另外50%是空闲的嘛,所以说就比较浪费空间而且需要复制大量对象的话,比较耗时间,所以比较适合,处理存活对象少、垃圾对象比较多的场景,然后标记整理算法的本质和标记清除算法的本质呢,其实是差不多的,只不过标记整理算法会把,所有存活的对象移动到一起,从垃圾收集的停顿时间上来看,移动对象的话肯定是耗时更久,但是分配对象的耗时就会降下来,JVM的内存呢就是分代的设计,分成了年轻代、老年代、永久代,之所以会进行分代设计,就是因为Java对象基本上都是临时的对象很快就会被回收掉,所以年轻代就会采用标记复制的方式,因为每次复制的时候存活下来的对象很少,所以复制的数据就会很少,然后老年代的话,都是经历过几次GC的对象,这些对象我们认为它可能会继续存活下去,就不太适合使用标记复制算法,所以老年代会采用这种标记清除的方式,你比如说CMS这种回收器,它就采用了标记清除的方式,但是标记清除的方式呢就会产生内存碎片,当这种内存碎片比较多的时候,也会触发标记整理的回收方式,然后把这些碎片都清除掉

  • 问:在进行标记的时候,JVM是怎样确定一个对象是否能够被回收呢?

在JVM进行垃圾回收的时候,都会通过一个叫可达性分析的这个东西来确定这个对象是不是可达的,如果可达的话这个对象就不能被回收如果不可达这个对象就是可以被回收的,可达性分析呢会从Gc root开始出发,然后顺着他的引用查找对象,这些能被查找的对象呢,就是可达的对象,GC root就包括我们,类里面的静态属性◇,我们栈中的局部变量、,常量引用还有JNI发出来的引用这些都是GCroot

  • 问: 看你用过MySQL数据库,,在使用过程中如果发现MySQL 响应比较慢·有什么排查思路吗?

如果MySQL响应比较慢的话,我可能会先想到是慢查询,这样的话我会先去,打开 MySQL的慢查询日志,收集一段时间的慢查询,然后找出耗时最长的那几个慢查询SQL,我再进行分析比如说我会拿explain命令去看看这些,SQL语句有没有走索引 ,如果在这个时候发现有的慢查询SQL,没有走索引的话,我会尝试改造这些SQL语句去走索引,然后如果说发现这些,SQL没有办法被改造的话,可以考虑在表上去添加索引,然后我们在改造SQL或者是添加索引的时候,都需要符合最左前缀的这个规则,然后在表比较大的时侯 ,即使是SQL语句走了这个索引,可能性能也不是特别的好,这个时候呢我们可以,考虑对表进行切分,然后表的切分呢主要是分为两种,一个是水平切分另一个是垂直切分,水平切分的话呢就是把一个行数在千万级别的这个大表,按照这个业务主键切分成100张或者是1000张,然后垂直切分的话就是这张表里面的列比较多,我可以按照业务逻辑把相关的列切到同一张表里面去除了这种分表之外呢我们还可以分库就比如说我们已经拆完了1000张表了,然后我们可以把后缀在0-100的表放到同一个MySQL实例里面,然后100-200的放到另一个MYSQL实例中里面,这样的话我们就可以根据切分的业务主键把请求路由到不同的MYSQL实例里面去,这样的话每一个MySQL实例承担的流量就会比较小然后性能的话也会有提高,另一个角度考虑的话呢,就可以进行读写分离,MSYQL它是支持一主多从的这种分布式模式,主库只是用来处理写操作,然后从库的话呢用来处理读操作,在读流量比较大的场景当中,我们可以增加从库来扛这个流量,这样的话也可以提高MySQL的性能,如果存在热点数据的话,除了MySQL本身的改造之外呢我们还可以考虑引入缓存,我们可以把这些热点数据放到缓存当中,通过缓存去缓解数据库的压力从而提高MySQL响应速度,这些呢就是我从索引、单表数据量、流量还有热点数据这几个角度去提高MySQL性能的方法

  • 问: 为什么走索引查询比不走索引快呢?

MySQL中索引的底层是B+ 树,B+树是平衡查找树的一种,在B+树的一个节点里面,会存放多条数据,这每一条数据呢,其实都是按照顺序存放的,然后每个数据的左右都会有一个指针指向大于它和小于它的子节点,然后在使用B+树查询的时候,就有点类似于呃二分查找的感觉,可以快速淘汰掉那些不符合条件的索引值,然后B+树只在叶子节点存储真正的数据信息,在其他节点里面呢存储的都是索引的值,这样的话B+树的结构占的空间就非常少,磁盘里面基本一个页基本上都是16k,我们通过几次IO操作就可以把需要的所有索引数据加载到这个内存里面去了,然后扫描一个大表的时候因为要扫描整个表里面的数据嘛,所以这个IO次数就会非常非常多,要比这个加载索引O次数要多非常多,所以的话就是使用索引的话就会更快一些嘛,我们通过B+树定位到一条数据之后呢,我们就会根据这个索引叶子节点里面存储的这个信息去表里查询,就是回表查询嘛,如果那个索引里面的那个索引值,已经包括了我们需要查找的表查找的列也就是走了覆盖索引也就是全覆盖索引,所以这样的话我们就不用再回表了,这样的话速度也会更快一些

  • 问: 从B+树索引是怎么处理范围查询的呢?

B+树的叶子节点会形成一个列表, 我们在进行范围查询的时候呢,只需要根据查询条件,通过索引定位到起始位置,然后顺着这个链表查询,直到不符合条件的索引位置,就可以得到查询条件内的数据地址了,然后再回表查询真正的数据就行

  • 问:你使用MySQL的时候,使用的是哪种数据库隔离级别?为什么要使用这个隔离级别呢?

我使用的是可重复读这个隔离级别可重复读可以防止脏读、幻读等等这些问题

  • 问: 能展开介绍一下可重复读是如何防止幻读的吗?

幻读,就是在一个事务里面多次执行相同的SELECT语句得到的结果不相同,MYSQL 它是通过MVCC 机制来防止幻读的,MVCC大概的含义就是为每个事务生成一个视图,然后在一个事务里面多次执行 SELECT语句的时候,都会从这一个视图里面去拿数据就不会发生幻读