【面经】美团春招三轮面经分享~涵盖众多知识点

第一轮

  1. 自我介绍

  2. 介绍第一个项目,但是没提问第一个项目问题

  3. 讲一下第二个项目架构,如何通信?

  4. ArrayList和LinkedList简介和区别
    ArrayList和LinkedList都是Java集合框架中的List接口的实现类。它们都用于保存一组对象,并且允许对这组对象进行增加和删除等操作。但是,ArrayList和LinkedList之间有一些重要的区别:

    1. 数据结构不同: ArrayList使用动态数组来存储元素,而LinkedList使用指针链表来存储元素。这意味着在ArrayList中,元素是存储到一个连续的内存块中,而在LinkedList中,元素存储在一系列不同的内存块中。因此,ArrayList在随机访问元素时更加高效,而LinkedList在插入和删除元素时更加高效。
    2. 插入和删除性能不同: 由于ArrayList使用的是动态数组,所以在插入和删除元素时需要对其它元素进行移动,这会导致性能较差。而LinkedList使用的指针链表,插入和删除元素只需要修改指针的指向即可,所以它在插入和删除元素时更加高效。
    3. 随机访问性能不同: 由于ArrayList中的元素存储在连续内存块中,因此随机访问时,ArrayList比LinkedList更高效。而在LinkedList中,由于每个元素存储在不同的内存块中,访问元素需要遍历整个链表,性能较差。

    综上,如果需要经常访问指定位置的元素,或者只需要在列表的末尾进行添加或删除,则使用ArrayList更好。如果需要在列表任意位置添加或删除元素,则使用LinkedList更好。

  5. HashMap底层实现,扩容机制,执行流程
    HashMap是Java中用来存储键值对的数据结构,它的底层实现是基于哈希表。
    下面是HashMap的具体实现细节,包括底层数据结构、扩容机制以及执行流程:

    1. 底层数据结构: HashMap内部维护了一个Entry数组,每个Entry对象保存了一个键值对,即key-value。而且HashMap主要利用散列码(hash code)去查找相应的元素,hash code是一个int类型的整数。
    2. 扩容机制: 当HashMap中的元素个数达到容量的75%时,就会触发扩容操作。HashMap的容量每次扩展为原来的2倍,重新计算哈希值,然后把元素移动到新的数组中,这个过程称为rehash。扩容的目的是为了保证哈希表在元素个数足够大时,仍然能够保持良好的性能。
    3. 执行流程:
      ①当put一个元素时,首先根据key的hashcode计算索引位置,如果该位置为null,则直接放入,结束操作。如果该位置不为null,那么就比较该位置的key和放入的key是否相等,如果相等则替换value值,结束操作。如果不相等,则称为hash冲突,需要将该元素通过链表的形式放入该位置后面的某个位置上。
      ②当get一个元素时,首先根据key的hashcode计算索引位置,如果该位置为null,则返回null。如果该位置不为null,则比较该位置的key和传入的key是否相等,如果相等,则返回对应value值。如果不相等,则在该位置后面的链表中查找对应的元素,找到则返回相应value值,否则返回null。

    以上是HashMap的主要实现细节,它的高效性在于利用了哈希表的快速查找能力,同时它的设计也考虑到了扩容问题,具有一定的健壮性。

  6. HashMap为何不安全?不安全主要包括哪些方面?
    HashMap在多线程环境下是不安全的,主要有以下两个方面:

    1. 线程安全问题: 由于HashMap内部结构是数组+链表或者数组+红黑树,多线程环境下可能会导致链表或红黑树的不一致性。例如,线程A在将一个新的键值对插入到HashMap中的过程中,可能会因为被线程B修改了链表中的某个节点而导致插入不成功。这时可以考虑使用线程安全的ConcurrentHashMap。
    2. 扩容问题: 由于HashMap在插入数据达到一定阈值的时候,需要进行扩容操作,这种扩容操作在多线程环境下会导致死锁和数据不一致等问题。在进行扩容操作时,需要重新计算键值对的位置,由于这个过程是单线程的,因此多线程同时执行可能会产生不一致性。
      需要注意的是,对于单线程环境下,HashMap是安全的,因为只有一个线程对其进行读取和修改操作。如果需要在多线程环境下使用HashMap,可以通过使用线程安全的ConcurrentHashMap来保证安全性。
  7. 线程安全的ConcurrentHashMap的实现原理
    线程安全的ConcurrentHashMap的实现原理主要基于分段锁技术,它将整个HashMap分成多个小的部分,每个部分都有一个独立的锁,不同的线程在访问不同的部分时,不会发生锁竞争,从而提高了并发性能。
    接下来具体介绍ConcurrentHashMap的实现原理:

    1. 分段锁技术: ConcurrentHashMap的内部结构是由多个Segment组成,每个Segment就是一个小的HashMap,所有的Segment组成了一个完整的HashMap。每个Segment内部都有一个独立的锁,所以不同的线程访问不同的Segment时不会发生锁竞争。因此,在多线程环境下,多个线程可以同时操作不同的Segment,从而提高了并发性能。
    2. hash值分配: ConcurrentHashMap中的元素是通过hash值分配到Segment中的。在put一个元素时,ConcurrentHashMap首先通过hash值找到对应的Segment,然后在Segment中进行操作,这样就避免了在整个hash表上进行锁定的情况。这样就可以提高ConcurrentHashMap在高并发下的吞吐量。
    3. 支持高并发更新操作: ConcurrentHashMap在支持高并发读操作的同时,还支持高并发更新操作。它使用了一种叫做CAS(compare and swap)的机制,在更新操作时不会发生锁竞争,从而提高了并发性能。在CAS操作中,当一个线程要更新某个值时,它首先需要检查该值是否和预期的值一致,如果一致,则进行更新操作,否则重新尝试。

    通过分段锁技术和CAS机制,ConcurrentHashMap实现了高并发场景下的线程安全。相比于HashMap,它支持高并发,而且在单线程情况下其性能表现也非常优秀。

  8. 什么是Hash冲突?如何解决Hash冲突?

    哈希冲突(Hash冲突)指的是在哈希表中,两个或多个不同的数据映射到了同一个地址上,导致数据被覆盖的现象。哈希表中出现冲突是不可避免的,因为无论哈希函数设计得再好,总会有不同的输入数据会导致哈希函数返回相同的哈希值。因此,哈希表的设计需要考虑到冲突的情况,并提供解决方法。
    一般来说,Hash冲突有两种解决方法:

    1. 开放地址法(又称探测再散列法): 在开放地址法中,当发现哈希冲突时,就继续寻找下一个空的散列地址,直到找到一个空的散列地址,或者数组已经被填满,不能再找到空的散列地址为止。具体来说,常用的开放地址法有线性探测、二次探测、双重散列等。
    2. 链地址法: 在链地址法中,哈希表中每个元素都指向一个链表,链表中存储哈希值相同的元素,当需要查找元素时,首先根据哈希值确定所在的链表位置,然后遍历该链表查找目标元素。如果链表中只有一个元素,而且该元素恰好就是我们要查找的元素,那么这种方法的效率是很高的。但是,如果链表很长,那么效率会非常低下。因此,在哈希表的设计中,我们需要尽量避免哈希冲突的情况,减少链表的长度。

    在实际应用中,我们需要根据数据量大小和访问方式等因素,综合考虑选择开放地址法还是链地址法等哈希表的设计方案。例如,对于大规模的数据集合,如果使用链地址法,会给内存造成大量的浪费,因此开放地址法会更加适用;对于查询次数较多,而插入和删除操作较少的情况,应该选择开放地址法;对于数据量较少,但需要频繁进行插入和删除操作的情况,可以选择链地址法。

  9. mysql索引场景题,记不清是什么了

  10. 锁机制
    锁机制主要用于多个线程对共享资源的访问控制,可以让同一时间只有一个线程对共享资源进行访问,从而保证数据的正确性,避免数据竞争等问题。
    在Java中,主要有以下几种锁机制:

    1. synchronized: synchronized是Java中内置的一种锁机制,可以将一段代码块或方法声明为同步代码块或同步方法,以进行加锁,防止并发访问。synchronized可以保证在同一时间内只有一个线程可以访问同步代码块或同步方法。它在底层实现上是基于monitor机制实现的。
    2. ReentrantLock: ReentrantLock是Java中提供的一种可重入锁机制,与synchronized相比,它提供了更高的灵活性和可控性。通过lock()和unlock()方法可以手动进行锁和解锁控制,还可以设置公平锁或非公平锁、重试机制等参数。
    3. ReadWriteLock: ReadWriteLock是一种适合读多写少的情况下的锁机制,它可以将锁分为读锁和写锁。在读操作时,多个线程可以同时获得读锁,但是在写操作时,只能有一个线程获得写锁。ReadWriteLock相比于synchronized和ReentrantLock,在读多写少的情况下,效率更高。
    4. StampedLock: StampedLock是JDK1.8中引入的一种乐观锁 机制,它提供了三种锁模式:写锁、悲观读锁和乐观读锁。在读锁和写锁互斥的情况下,乐观读锁可以提高程序的并发性能。
    5. Semaphore: Semaphore是Java中的一种信号量机制,它可以控制并发线程的数量。可以通过acquire()和release()方法控制资源的数量,从而实现对共享资源的控制。
    6. CountDownLatch: CountDownLatch是Java中的一个倒计数器,可以让某一个线程等待多个线程完成任务之后再执行。CountDownLatch会在构造函数中接收一个数字(即计数器),每个线程执行完任务后会将计数器减1,直到计数器为0时,等待的线程才会继续执行。
    7. CyclicBarrier: CyclicBarrier也是一个同步辅助类,它允许多个线程在某个屏障点(也就是barrier)处等待彼此。当所有线程都到达屏障点时,CyclicBarrier会执行指定的任务,然后所有线程继续运行。

    锁机制在多线程编程中具有重要的作用,但是使用不当会造成死锁、饥饿等问题。因此,在使用锁机制时,需要小心谨慎地编写代码,避免出现问题。

  11. 可重入锁和lock区别
    可重入锁和Lock的区别主要包括以下几点:

    1. 概念不同: 可重入锁是线程的一种锁机制,具有线程重入的特性。线程在持有锁时,可以再次获取锁,而且每获取一次锁,就必须释放相同次数的锁。而Lock是Java提供的一个接口,定义了一组锁机制相关的操作。
    2. 接口不同: 可重入锁的实现主要有ReentrantLock和ReentrantReadWriteLock;Lock接口有很多实现,如ReentrantLock、ReentrantReadWriteLock、StampedLock等。
    3. 粒度不同: 可重入锁的粒度相对较小,可以针对一个块或方法进行锁定,适用于对性能要求较高的情况。而Lock的粒度相对较大,可以针对整个代码块或方法进行锁定。
    4. 锁定方式不同: 可重入锁通过synchronized关键字来实现锁定,它是JVM底层进行实现的,不需要用户手动创建。而Lock是手动创建的一种锁,通过调用Lock接口中定义的方法来进行加锁和解锁。
    5. 功能不同: 相较于synchronized,ReentrantLock提供了更加灵活的功能,例如可以设置公平锁、限时等待、中断响应等机制,而Lock接口还提供了更多的功能,例如读写分离锁、乐观锁等。
    6. 性能不同: 可重入锁在竞争激烈的情况下,性能相对较差,因为每次重入都需要判断当前线程是否已经持有该锁,这样会消耗一定的时间。而Lock的性能相对较好,因为它的实现可以选用自旋锁和CAS等高效的机制。
    7. 错误处理不同: 可重入锁在执行加锁和解锁操作时,如果出现异常,JVM会自动释放锁资源,避免死锁的发生。而使用Lock时,需要在finally块中释放锁资源,否则在锁定代码中发生异常时,锁没有被释放,会导致死锁。
    8. 可见性不同: synchronized保证了同一时刻只有一个线程可以执行某个代码块,同时还保证了线程在获取锁之前对共享变量的修改对于其他线程是可见的。即一个线程修改了某个共享变量的值,其他线程在获取该变量时,能读到新的值。而ReentrantLock和Lock接口在基本功能上与synchronized是相同的,但并未提供可见性的语义保证,也就是说,没有获取该锁的线程是无法读取到其他线程对共享变量所做的修改。
  12. 分布式锁介绍,结合简历项目讲解

  13. Redisson原理
    Redisson是一个基于Redis实现的Java Redis客户端,提供了分布式锁、分布式集合、分布式对象等功能。其主要原理是基于Redis的原生数据结构,使用Redis的分布式特性和高效性,实现了对分布式数据结构的封装和访问。
    Redisson实现的分布式锁原理示例如下:

    1. Redisson客户端向Redis服务器发送SETNX命令,当目标Key不存在时才能设置成功,即获取到锁。
    2. 不断轮询Redis服务器,使用过期时间避免死锁。
    3. 当获取到锁的客户端完成操作后,必须将锁手动释放,并且只能释放当前客户端获取的锁。

    Redisson实现的分布式集合原理示例如下:

    1. Redisson客户端向Redis服务器发送SADD或SMEMBERS命令,实现添加元素或者读取整个集合。
    2. Redisson支持的是Java集合接口,所以可以方便地对集合进行操作。
    3. 操作集合时,Redisson通过Redis的命令将所有元素存储在一个Redis的Key下。

    Redisson实现的分布式对象原理示例如下:

    1. Redisson将Java对象序列化为二进制数据,存储到Redis服务器上,并在对象对应的Redis Key上设置过期时间。
    2. Redisson客户端从Redis服务器中读取二进制对象数据,并反序列化为Java对象。
    3. Redisson支持锁机制,避免多个客户端同时访问同一个Java对象。 总的来说,Redisson通过添加一些额外的功能,如分布式锁、分布式集合、分布式对象等,使Redis的原生数据结构更能适应于分布式场景,提供更方便的分布式数据结构的使用方式。
  14. synchronized原理
    synchronized是Java中实现同步机制的关键字,它可以用于修饰方法或代码块,能够保证在同一时刻只有一个线程能够访问共享资源。
    synchronized原理如下:

    1. 当一个线程试图访问synchronized代码块或方法时,它首先尝试获取对象的锁。如果当前对象没有被锁住,则该线程获得锁并可以访问对象,同时所有其他线程将被阻塞。如果对象已被锁住,那么该线程将被阻塞,直到锁定该对象的线程执行完成并释放锁。这种方式可以保证同一时刻只有一个线程能够访问对象,从而避免了数据竞争和不一致性的问题。
    2. 在Java中,每个对象都有一个内部锁,也称为监视器锁(monitor lock)。
      synchronized可以用在不同的类型上,如synchronized方法和synchronized代码块。synchronized代码块是基于对象的锁,当synchronized代码块访问对象时,JVM会尝试获取该对象的锁。synchronized方法也是基于对象的锁,它的锁定范围是整个方法,通常使用在方法需要访问共享资源的场合。
    3. 需要注意的是,synchronized关键字相对于其他同步方法(比如ReentrantLock)而言,具有自动释放锁的特点,不用手动解锁,而是在代码块执行结束时,自动释放锁。
      另外,从Java 5开始,synchronized关键字还支持可重入锁的机制。即,同一个线程在方法或代码块内多次获得锁并释放锁是允许的,不会导致线程死锁。

    总之,synchronized是Java内置的一种同步机制,它通过获取并占用对象锁的方式,保证了共享资源的正确访问

  15. 锁的类型介绍,升级流程
    在Java中,锁的类型主要有以下三种:偏向锁、轻量级锁和重量级锁。

    1. 偏向锁:当对象只有一个线程访问时,JVM会给该线程加上偏向锁,即将对象头中的Mark Word修改为指向线程ID的偏向锁。如果另一个线程访问该对象,则偏向锁会升级为轻量级锁。
    2. 轻量级锁:如果一个线程正在占用锁,其他线程会自旋等待,而不会阻塞,直至获得锁。在轻量级锁状态下,锁对象头的Mark Word被修改为指向锁记录的指针,锁记录保存了持有锁的线程ID等信息。如果自旋失败,轻量级锁会升级为重量级锁。
    3. 重量级锁:多个线程都可以竞争锁,当锁无法被某一个线程拥有时,锁会升级为重量级锁,在锁对象头中,Mark Word指向阻塞队列,为每一个希望获取锁的线程分配等待队列和锁记录等信息,而不再使用锁记录。

    当多个线程竞争同一个锁时,JVM会根据竞争情况自动升级锁的类型,以保证并发程序的正确性和性能。下面是三种类型锁升级的流程:

    1. 偏向锁 -> 轻量级锁:当有多个线程访问同一个对象时,偏向锁无法满足要求,会升级为轻量级锁。此时JVM会尝试使用CAS (Compare and Swap) 操作将锁对象头中的 Mark Word 修改为一个指向锁记录的指针,锁记录中包含了锁的状态、持有锁的线程 ID 和指向对象头的指针等信息。如果 CAS 操作失败,则代表存在竞争,锁会升级为重量级锁。
    2. 轻量级锁 -> 重量级锁:当有多个线程竞争锁时,轻量级锁的 CAS 操作可能会失败,此时锁对象头中的 Mark Word 会被修改成指向阻塞队列的指针,锁记录中也会添加一个指向阻塞队列的指针,表示当前线程需要阻塞等待获取锁。其他线程在竞争锁时会进入阻塞队列,直到当前线程释放了锁。重量级锁的竞争激烈、性能较差,因此应该尽可能避免过长的持有时间。
    3. 重量级锁 -> 轻量级锁:如果持有重量级锁的线程释放了锁,此时JVM会尝试让其中一个阻塞队列中的线程转化为获得锁的线程,方式是重新将锁对象头中的 Mark Word 修改为指向锁记录的指针,状态也会变成轻量级锁。如果此时有其他线程正在竞争同一个锁,那么该锁会直接升级为重量级锁。
  16. 死锁产生原因?如何解决?
    死锁的产生原因通常有以下几个:

    1. 竞争同一个资源,例如多个线程同时竞争一个锁;
    2. 线程等待条件成环,例如线程 A 正在等待线程 B 释放资源,而线程 B 又在等待线程 C 释放资源,而线程 C 正在等待线程 A 释放资源;
    3. 加锁的顺序不一致,例如线程 A 先获取锁 1 再获取锁 2,而线程 B 先获取锁 2 再获取锁 1;
    4. 资源未及时释放,例如一个线程占有了某个资源却没有及时释放;
    5. 程序设计问题,例如并发编程中没有考虑线程同步和协作等问题。

    解决死锁问题的方法通常有:

    1. 加锁顺序控制:线程在访问多个资源的时候按照相同的顺序加锁,释放锁的顺序和加锁顺序相反,可以有效避免死锁问题;
    2. 资源分配算法:按照某个顺序分配资源,避免产生死锁;
    3. 避免循环等待:破坏循环等待条件;
    4. 超时机制:设定超时时间,如果等待超过一定时间就主动释放资源;
    5. 检测与恢复:检测到死锁之后,主动进行资源回收和释放,让线程继续执行;
    6. 合理的程序设计和代码优化。这需要注意线程同步协作的设计,尽量避免锁的嵌套,避免产生不必要的等待和互斥。
  17. http各个版本介绍以及各自特点
    HTTP(Hypertext Transfer Protocol,超文本传输协议)是一种用于在网络上发送和接收数据的标准协议。HTTP的演进历程经历了许多版本,这里简单介绍一下HTTP各个版本的特点:

    1. HTTP/0.9:是HTTP的最初版本,只支持GET方法,没有Header等概念,不能传输二进制数据。
    2. HTTP/1.0:支持GET、POST等多种方法,加入了Header、状态码等概念,但每次请求都需要重新建立TCP连接,效率低下,不支持断点续传等高级特性。
    3. HTTP/1.1:是目前使用最广泛的HTTP版本,支持复用TCP连接、流水线请求、断点续传、Host头信息等特性,大大提高了HTTP的效率和性能,广泛应用于Web应用程序。
    4. HTTP/2:是HTTP/1.1的升级版本,主要特点是使用多路复用技术,允许在一个TCP连接上并行发送多个请求,提高了请求响应速度,并且支持Header压缩、服务端推送等高级特性,但是需要服务器和客户端都支持才能发挥其高效率的优点。
    5. HTTP/3:是由Google设计,基于QUIC协议实现的一种新的HTTP版本,它的特点是减少TCP连接建立所需要的RTT,降低网络延迟,从而提高网络吞吐量,同时加强了安全性。
  18. TCP为什么可靠?
    TCP是一种面向连接的可靠的协议,它之所以可靠,主要由以下几个方面的特点所造就:

    1. 三次握手建立连接:TCP在通信开始前,客户端和服务器要进行三次握手协商,确保双方都已准备好进行通信。这三次握手过程中,TCP会交换一些信息,以确保连接的可靠性和有效性。
    2. 确认和重传机制:TCP会对传输的数据进行编号,并且在接收端对每个数据包进行确认,确保发送的数据正确无误。如果发送端未收到确认消息,就会重传相应的数据包,直到收到接收端的确认消息。
    3. 流量控制:TCP有一个流量控制机制,可以控制发送方发送的数据量以及接收方的接收速度,避免发送的数据大于接收方的处理能力所造成的数据丢失或拥堵。
    4. 拥塞控制:TCP也有拥塞控制机制,在网络拥塞时减缓发送速度,避免网络继续拥塞丢包率变得更高,数据传输效率降低等问题。

    综上,TCP采用了一系列的措施来确保数据的可靠性和稳定性,避免了网络传输中遇到的众多问题,例如数据丢失、错误、重复等等。因此,TCP是一种可靠性非常高的协议。

  19. Redis数据结构类型,各自介绍以及使用场景?
    Redis支持多种数据结构类型,每种类型都有自己的特点和适用场景,具体如下:

    1. 字符串类型(string):Redis最为基础的数据结构类型,简单的键值对结构,可设置过期时间,适用于缓存、计数器、消息队列等场景。
    2. 列表类型(list):由多个元素组成的列表,可以在头尾添加删除元素,可在列表的任意位置插入元素,适用于队列、栈等数据结构、消息队列等场景。
    3. 集合类型(set):不重复元素的无序集合,支持多种集合运算如交集、并集等操作,可进行随机取样,适用于共同关注、排名查询等场景。
    4. 有序集合类型(sorted set):集合的升级版,有关联的元素不再是无序的,而是可以通过分数进行排序,支持范围查询和多元素排序,适用于排名、排行榜等场景。
    5. 哈希类型(hash):存储多个键值对,相当于内部嵌套了多个键值对的结构,适用于存储对象、存储简单的KV结构、存储用户信息等场景。
    6. 位图类型(bitmap):使用bitmap类型可以将多个二进制位存储在一个字符串中,它支持多种位运算操作,适用于进行权限、权限管理等场景。
  20. Redis中的set和HashSet底层实现原理?
    Redis中的Set和HashSet均是基于哈希表实现的数据结构。

    1. Set是一个无序的、不可重复的元素集合,可以认为是一个特殊的HashMap(哈希表),它将元素存储成一个Map的键值,其中所有的键(key)都是相同的,而值(value)则都为null。由于Set的底层实现是基于HashMap,因此它的插入、查找和删除元素的时间复杂度都是O(1)。
    2. HashSet也是一个无序的、不可重复的元素集合,它是Set的一个子类。在HashSet中,元素是存储在一个HashMap中的,对于每个元素,它的值存储在HashMap的value中,而它的哈希码则作为HashMap的key。因此,HashSet也具有Set的所有特性,但是它可以快速地根据哈希码来查找和定位元素。

    总的来说,Set和HashSet底层都是基于哈希表实现的数据结构,它们的存储和查询效率非常高,而且对于大规模元素的存储和处理,它们的性能表现也是非常优异的。

  21. bitmap底层实现原理
    Bitmap是一种基于位的数据结构,它将一个区间映射到一个二进制数组当中,每个元素只占用一个二进制位(通常是0或1)。Bitmap可以用来高效地存储和查询某个数字是否在一个区间内出现过或者某些数据是否存在,具有高效、节省空间的优点,常用于海量数据的存储和处理,例如网络爬虫的去重操作、数据的压缩存储、Bloom Filter等场景。

    Bitmap的底层实现原理非常简单,其主要思想是用一个二进制数组(也可以看成是一个长的二进制数)来表示一段数据区间的出现情况。例如,如果要查询数字7是否在一个范围[1,10]中出现过,可以使用一个长度为10的二进制数组,第7个位置为1,其它位置为0,表示数字7在区间[1,10]中出现过。

    下面简单介绍一下Bitmap的操作:

    初始化: 首先,需要确定要处理的数值范围,然后根据数值范围创建一个合适大小的二进制数组,如果数值范围很大,可以采用分块的策略。
    插入元素: 将对应的二进制位设为1(取或运算)。
    删除元素: 将对应的二进制位设为0(取反再与原来的值进行与运算)。
    查询元素: 判断对应的二进制位是否为1。
    位运算: 例如对两个Bitmap进行交集、并集或差集等操作,可以通过对二进制数组进行相应的位运算来实现。

    需要注意的是,Bitmap只适用于数据范围较小的场景,否则会造成较大的空间浪费和效率问题。例如,如果需要处理范围为0~10亿之间的整数,需要创建一个长度为10亿的二进制数组,这是不现实的。
    因此,在实际使用时需要根据数据范围的大小选择合适的方案,例如可以采用分段存储、哈希函数等技巧来优化空间和效率。

  22. Redis缓存雪崩、击穿、穿透是什么?各自如何解决?
    Redis缓存雪崩、击穿、穿透是缓存中常见的三个问题,解决这些问题可以提高Redis缓存的性能和可靠性。

    1.缓存雪崩:
    缓存雪崩是指在某一个时间段内,缓存中的大量数据都失效,导致大量请求直接落到数据库上,从而导致数据库访问压力骤增,甚至宕机。

    解决缓存雪崩的方法有:

    (1)设置缓存失效时间随机,避免在同一时间大量数据失效
    (2)引入限流措施,对高并发请求进行限流,避免过多的请求给缓存造成压力
    (3)提高缓存的可用性,添加缓存高可用集群、数据备份等机制,避免单点故障

    2.缓存击穿
    缓存击穿是指一个对缓存中不存在的数据进行大量并发访问,导致请求直接落到数据库上,从而导致数据库访问压力骤增,甚至宕机。

    解决缓存击穿的方法有:

    (1)设置缓存空值,当某个键值映射的缓存中没有数据时,将这个键值映射到空值。那么在下一次查询该键值时,缓存会直接返回空值,避免了请求落到数据库的情况。
    (2)采用“懒加载”的方式,假设并发查询某个数据,当缓存中没有时,只让一个线程去查询数据库,其他并发线程等待。当第一个线程查询数据库获得数据后,其他线程从缓存中获取到数据,进行访问。

    3.缓存穿透: 缓存穿透是指当一个对缓存中不存在的数据进行大量并发查询,导致请求直接落到数据库上,从而导致数据库访问压力骤增,甚至宕机。

    解决缓存穿透的方法有:

    (1)采用布隆过滤器(Bloom Filter)等机制,通过对即将查询的键进行哈希,然后与Bitmap比对,若不存在直接返回查询失败结果,避免了查询数据库操作。
    (2)将查询结果的缓存时间设置为较短的时间,例如1分钟,避免在查询失败时持续查询数据库,造成数据库压力过大。

  23. IOC原理
    IOC(Inversion of Control)原理即控制反转,认为对象的创建、依赖和调用控制由框架控制,而不是由我们手动控制。直接实例化对象显然是不可取的,因为依赖关系可能比较复杂,如果手动管理的话会造成大量的重复代码,增加了代码的维护成本。因此,IOC由Spring框架提供了一种解决方案。
    Spring IOC的基本实现逻辑:

    在Spring容器中预先定义好对象的实例化和依赖关系,并由Spring容器统一管理,使得框架根据在配置文件中定义的对象之间的依赖关系,自动在初始化时按需创建并向各个需要它们的地方自动注入它们,让程序员从繁琐的对象实例化和管理的过程中解放出来,专注于解决系统的业务逻辑。

    Spring IOC的实现有两种方式:

    基于XML配置和注解配置。在XML配置方式下,Spring根据预先编写的XML配置文件,通过对象的反射和JavaBean规范自动创建对象,自动注入依赖,并由Spring容器管理对象的生命周期,如何创建、何时销毁等。
    在注解配置方式下,Spring会将需要被Spring容器管理的对象加上对应的注解,如@Service、@Component等,从而实现同XML配置方式相同的功能。

    Spring IOC的关键组件是BeanFactory和ApplicationContext:

    BeanFactory是Spring IOC容器最基本的接口,提供了基本的IOC操作方法,但是它只实现了最基本的功能,ApplicationContext是BeanFactory的子接口,提供了更多的功能和更灵活的配置方式,包括事件发布、Bean生命周期管理等高级功能。

    Spring IOC实现的基本原理就是反射和依赖注入。

    反射用来动态地实例化对象和依赖关系的自动注入。而依赖注入是通过在对象之间建立依赖关系,在程序运行时完成对象的组装和初始化,从而达到对象复用和提高程序的可维护性。因此,可以说Spring
    IOC的核心原理是反射和依赖注入。

  24. AOP原理
    AOP(Aspect Oriented Programming)面向切面编程,是一种新的程序设计范型,可以使程序逻辑分离,提高代码的可重用性、可维护性、可扩展性和可测试性。AOP原理就是在不改变原有业务逻辑的前提下,通过将业务逻辑和某个横切关注点进行隔离,达到增强代码重用和跨越模块的目的。

    AOP实现核心原理是代理模式和动态代理技术。

    代理模式是指通过代理对象来与真实对象进行沟通。在AOP中,程序通过代理对象来实现切面功能的插入。动态代理技术则是通过反射机制运行时创建代理类,动态地实现代理类。

    Spring AOP使用的是JDK动态代理或者CGLIB动态代理。
    使用JDK动态代理时,被代理的对象必须实现一个接口,而CGLIB动态代理则可以代理任何一个非final类的对象。

    AOP的关键技术是切面和通知。
    切面是与业务逻辑无关的横切关注点,通知则是切面代码在何时、何地执行的规则。AOP切面包括@Before、@After、@Around、@AfterReturning和@AfterThrowing等关键注解,它们定义了在目标方法之前、之后、甚至代替目标方法执行时,AOP框架切入的位置和切入的代码内容。而通知的分类包括前置通知、后置通知、环绕通知、返回通知和异常通知。
    Spring AOP还提供了切点和切点表达式来定义具体的切面,以及增强器来定义通知的实现方式。
    切点是一个表达式,用来定义哪些类的哪些方法需要被切入;切点表达式则是使用切点来指定切面的作用范围和细节。增强器则是AOP框架中最核心的部分之一,它通过连接点和通知来定义AOP所需的各种信息,并管理AOP操作的全过程。

    总之,AOP实现的核心原理是代理模式和动态代理技术。切面和通知是AOP中的重要概念,用来定义在哪里和怎样应用AOP。通过定义切点、切点表达式和增强器,可以非常灵活地实现AOP。Spring AOP提供了多种实现方式,例如基于XML配置和基于注解的方式,使得AOP的使用变得非常方便。

  25. 接口和抽象类区别

    接口和抽象类都属于Java中的抽象类型,它们都不能被直接实例化,只能被子类实现或继承。它们的主要区别如下:

    1. 对于字段(成员变量)的支持:接口只能定义常量,而抽象类可以定义普通属性和常量。
    2. 对于构造函数的支持:接口没有构造函数,而抽象类可以有构造函数。
    3. 对于方法的支持:接口中只有抽象方法,而抽象类中可以有非抽象方法,还可以重载方法。
    4. 关于实现的规范:接口是一种约定或契约,定义了子类需要实现哪些方法,而抽象类则是一种模板或者基类,提供了部分实现,子类需要继承和实现。
    5. 多继承支持:Java中的类只能单继承,一个类只能继承一个抽象类,但可以实现多个接口。
  26. static修饰的访问非static修出现的问题,以及为什么会出现这个问题,那么应该如何访问?
    在Java中,static修饰的成员变量或方法属于类本身,而不是属于某个实例,可以直接通过类名访问。而非static修饰的成员变量或方法则属于类的实例,需要先创建对象后才能访问。
    因此,如果在静态方法中访问非静态成员变量,会出现编译错误,因为静态方法中不能直接使用实例变量,因为只有通过实例化对象后,才能访问非静态成员变量。而如果在非静态方法中访问静态成员变量,则没有问题,可以直接访问静态成员变量。

    如何在静态方法中访问非静态变量:

    1. 在静态方法中创建对象,然后通过对象去访问非静态变量。
    2. 将非静态变量改为静态变量,这样静态方法就可以直接访问了。但是这种方式会破坏程序的设计结构,不建议使用。
  27. springMVC执行流程
    SpringMVC执行流程是整个SpringMVC框架中最为关键的流程,它主要包括以下几个步骤:

    1. 客户端发送请求:客户端向服务器发起一个请求。
    2. 前端控制器接收请求:请求到达服务器后,由前端控制器(DispatcherServlet)接收并进行处理。前端控制器是整个流程的中心,负责协调各个组件之间的工作。
    3. 处理器映射器解析请求:前端控制器通过调用处理器映射器(HandlerMapping)来解析请求,并确定并返回处理请求的处理器(Handler)。
    4. 处理器适配器调用处理器:前端控制器通过调用处理器适配器(HandlerAdapter)来执行处理器,根据处理器的类型和定义的方法来调用特定的处理器方法。
    5. 处理器返回ModelAndView对象:处理器方法执行完成之后,通常会返回一个ModelAndView对象。ModelAndView对象包含了视图名称和模型数据等信息。
    6. 视图解析器解析视图:前端控制器将ModelAndView对象传递给视图解析器(ViewResolver)。视图解析器将根据ViewName(视图名称)来解析出一个具体的视图对象。
    7. 视图对象渲染响应:前端控制器将请求和响应传递给视图对象,并调用视图对象来渲染响应内容。通常,视图会读取模型数据并将其展示到视图中,生成HTML等内容。
    8. 响应返回给客户端:视图渲染完成之后,前端控制器将响应返回给客户端,完成整个处理过程。

    总的来说,SpringMVC执行流程是一个典型的MVC模式,将请求和响应分别封装到不同的类中进行处理,使得每个组件职责清晰,各司其职。掌握整个执行流程可以更好地理解SpringMVC框架的工作机制,为编写高效的SpringMVC应用提供参考。

  28. Redis集群,以及过期策略

  29. Redis跳表介绍

  30. 项目中的nginx如何使用的?如何代理?项目中他属于几级代理?

  31. 双亲委派机制讲解,为什么这样做?什么时候打破?
    双亲委派机制是Java类加载器的一种机制,它要求所有的Java类加载器在加载某个类时,都会优先委托父类加载器加载,只有在父类加载器找不到该类时,才由当前类加载器自行加载该类。这样就形成了一种层次关系,保证了Java类库的安全性和稳定性。

    1. 双亲委派机制的优势在于防止重复加载,并保证Java核心类库的稳定性和安全性。Java类库被划分为三个等级:启动类、扩展类和应用程序类,每个类加载器只会加载其对应等级的类,这样可以避免了有恶意软件尝试替换Java核心类库的情况。
    2. 但有时候,我们需要打破双亲委派机制,这种情况一般是在我们编写一些自定义的类加载器时,由于自定义的类加载器需要在自己的类路径下寻找类,并不一定能够在父类加载器中找到,所以这时就需要打破双亲委派机制。这种情况下,我们需要编写自定义的类加载器,重写loadClass()方法,在方法中实现自己的类加载逻辑。
  32. 负载均衡策略
    负载均衡是指将访问请求分摊到多个服务器上,使得单个服务器的负载降低,进而提高服务的可用性和性能。常见的负载均衡策略包括以下几种:

    1. 随机负载均衡(Random):简单随机选择一台服务器进行请求处理。优点是简单实用,缺点是没有考虑服务器负载情况,可能导致负载过高的问题。
    2. 轮询负载均衡(Round Robin):按照服务器列表顺序依次轮询分配请求。优点是均匀分配请求,但缺点是无法根据服务器负载情况进行动态负载均衡。
    3. 最少连接负载均衡(Least Connections):选择正在处理连接最少的一台服务器进行请求处理。优点是能够根据服务器的负载情况动态选择服务器,缺点是需要实时监测服务器连接数,开销比较大。
    4. IP哈希负载均衡(IP Hash):按照客户端IP地址哈希值选择相应的服务器进行请求处理,保证同一客户端的请求只会被分配给同一台服务器进行处理。优点是保证了一定的持久性,缺点是在服务器动态扩容时需要重新计算哈希分配。
    5. 加权轮询负载均衡(Weighted Round Robin):在轮询算法的基础上,给每台服务器指定一个权重值,高权重的服务器获得更多的请求。优点是能够根据服务器的处理能力进行负载均衡,缺点是需要手动指定权重值。

    以上是常见的几种负载均衡策略,不同的负载均衡策略适用于不同的场景,选择合适的负载均衡策略能够有效提高服务的可用性和性能。

  33. spring生命周期,作用域
    Spring的生命周期包含了Bean的创建、初始化、使用和销毁四个阶段。

    1.创建阶段:Spring容器通过反射机制实例化Bean,并调用Bean的构造方法。
    2. 初始化阶段:Spring容器为Bean设置属性值和其他Bean引用,调用Bean的初始化方法,可通过实现InitializingBean接口或在配置文件中指定init-method方法指定初始化方法。
    3. 使用阶段:Bean对象可以被其他对象引用并调用其方法。
    4. 销毁阶段:当Spring容器关闭时,会调用Bean的销毁方法,可通过实现DisposableBean接口或在配置文件中指定destroy-method方法指定销毁方法。

    Spring中的作用域定义了Bean的生命周期和使用范围。

    1. singleton(单例)作用域:Spring容器只创建一个Bean实例,所有请求都返回同一个Bean实例。该作用域是默认的作用域,适用于无状态的Bean。
    2. prototype(原型)作用域:每次请求都会创建一个新的Bean实例。适用于具有状态的Bean,但相对于singleton作用域,容易造成内存的浪费。
    3. request作用域:每个HTTP请求都会创建一个新的Bean实例,该作用域仅适用于WebApplicationContext环境。
    4. session作用域:每个HTTP Session都会创建一个新的Bean实例,该作用域仅适用于WebApplicationContext环境。
    5. globalSession作用域:一般与Portlet相关,表示全局的HTTP Session作用域。 除了singleton作用域之外,其他作用域都是非单例的,即每次请求都会创建一个新的实例,因此在考虑作用域时,需要考虑Bean是否有状态,以及对内存占用的影响。

算法题

  • 手写单例
  • 合并两个有序链表

第二轮

  1. spring与springboot对比

  2. 线程池好处、原理、执行流程、参数介绍
    线程池好处:

    • 优化资源利用:避免频繁创建和销毁线程,重复利用线程资源,提高CPU利用率和系统性能;
    • 提高响应速度:线程池中有一定数量的线程,等待外部任务的到来,可以快速响应任务,减少任务等待的时间;
    • 精密控制线程数:可根据实际业务需求自由调整线程池的线程数,确保任务不被阻塞;
    • 失败处理机制:线程池可以处理任务执行失败的情况,保证线程池的稳定运行。

    线程池原理:

    线程池的核心组件是线程池管理器,线程池就是通过管理器来管理一组线程。线程池的管理器中维护了多条线程,这些线程可以处理不同的任务。当有任务需要处理时,线程池管理器就会把任务分配给一条等待的线程,这样就不用每次都创建和销毁线程。

    执行流程:

    1. 首先申请创建一个线程池;
    2. 在线程池中提前开启一定数量的线程,线程处于等待任务的状态;
    3. 当有任务到来,线程池会从池中挑选一个空闲线程,分配任务给它;
    4. 当线程执行完任务后,将结果返回,并再次处于等待状态。

    参数介绍:
    线程池的常用参数如下:

    • corePoolSize:线程池中的基本线程数;
    • maximumPoolSize:线程池中最大的线程数;
    • keepAliveTime:线程的活动时间;
    • unit:时间单位,在keepAliveTime参数中使用;
    • workQueue:任务队列,用于存放等待处理的任务,一般是一个阻塞队列;
    • threadFactory:线程工厂,用于创建线程;
    • handler:线程池对拒绝任务的处理策略。

  3. TCP和UPD对比,各自使用场景
    TCP和UDP是两种不同的传输层协议,TCP在传输数据的过程中提供可靠性保证,而UDP则提供更快的速度和更低的延迟。二者各有优劣,根据不同的需求和场景可以选择不同的协议。
    TCP:

    优点:
    • 可靠性高:TCP提供可靠的数据传输,可通过确认应答和重传机制保证数据传输的可靠性;
    • 有序性:TCP保证数据传输的有序性,确保数据按照发送的顺序到达目的地;
    • 支持流量控制:TCP提供流量控制机制,可避免数据过载;
    • 适用于大量数据传输:TCP适合传输大量数据或者重要数据。

    缺点:
    • 传输速度较慢:TCP的确认应答和重传机制带来了额外的开销,传输速度相对较慢;
    • 需要保持连接:TCP需要维护连接状态,进行三次握手和四次挥手,对网络开销带来较大的负荷。

    使用场景:
    • 可靠传输场景:如文件传输、邮件等;
    • 需要有序性的场景:如视频直播、网络电话等;
    • 对延迟和网络波动要求不高的场景。

    UDP:

    优点: • 速度快:UDP没有确认应答和重传机制,没有建立连接的过程,传输速度更快;
    • 延迟低:UDP没有流量控制,数据包发送后立即就能到达目标地址;
    • 支持广播和组播:UDP支持广播和组播,能够一次将数据包发送给多个目标。

    缺点:
    • 可靠性低:UDP没有可靠性保证,数据包可能丢失,出现乱序等情况;
    • 特定的场景:UDP主要适用于特定的场景,如视频流媒体传输、在线游戏等。

    使用场景:
    • 需要快速传输的场景:如音视频流媒体传输、实时通信等;
    • 对可靠性和数据有序性要求不高的场景:如在线游戏的状态同步和广播、DNS查询等。

  4. 一次http流程,详细介绍各部分
    HTTP流程主要包括以下步骤:

    1. DNS解析:浏览器发送URL请求到DNS解析器,将域名解析成IP地址,获取服务器的IP地址。
    2. TCP连接:浏览器与服务器建立TCP连接,三次握手确认连接的可靠性,建立TCP连接后,服务器与客户端可以进行数据通信。
    3. 发送HTTP请求:浏览器向服务器发送HTTP请求,包括请求方法(GET、POST等)、请求头和请求体等内容。请求头包含请求的地址、请求方式、浏览器类型、cookie等信息,请求体包含需要传输的数据。
    4. 服务器处理请求:服务器接收到请求后,进行处理,例如从数据库中查询数据、生成动态页面等。
    5. 返回HTTP响应:服务器处理完请求后,将响应返回给浏览器,包括响应头和响应体。响应头包含HTTP协议版本、状态码、内容类型等信息,响应体包含服务器处理后的数据,可以是HTML、文本、JSON等格式。
    6. 浏览器解析渲染:浏览器接收到响应后,对响应进行解析,将HTML、CSS、JS等资源进行渲染,最终呈现给用户的结果是一个Web页面。
    7. TCP断开连接:请求处理完毕,服务器会主动关闭TCP连接,浏览器也会关闭TCP连接。 在HTTP协议中,请求和响应都是基于TCP传输的,因此需要进行TCP连接和请求响应的传输。

    虽然HTTP协议简单,但是在底层还是有很多网络细节需要处理的。例如在TCP连接中,数据包的重传、拥塞避免等机制需要维护;在HTTP响应中,数据压缩、缓存控制等机制也需要合理使用,从而提高网络传输的效率和质量。

  5. mysql事务、隔离机制、每个隔离级别会出现什么问题,如何解决脏读、幻读等问题
    MySQL是一种关系型数据库管理系统,支持事务的处理。在MySQL中,事务指的是一组SQL语句,要么全部执行,要么全部回滚。通过事务,MySQL可以确保多个并发用户或并发应用程序之间获得数据的一致性和完整性。
    MySQL事务隔离机制:
    MySQL在事务并发处理中,通过隔离机制来保证数据的一致性和完整性。MySQL支持四种隔离级别,分别为:

    1. 读未提交(Read Uncommitted):最低的隔离级别,允许一个事务读取未被另一个未提交事务修改的数据。会产生脏读(Dirty Read)。
    2. 读已提交(Read Committed):允许一个事务只能读取另一个已经提交的事务修改过的数据。会产生不可重复读(Non-Repeatable Read)。
    3. 可重复读(Repeatable Read):保证同一事务在多次查询相同记录时,结果一致。会产生幻读(Phantom Read)。
    4. 序列化(Serializable):最高的隔离级别,在执行每个事务时,都会将事务串行化。可以避免以上问题。

    每个隔离级别出现的问题:

    1. 脏读:指一个事务读取到了另一个事务修改但没有提交的数据。
    2. 不可重复读:指一个事务在同一时间内多次读取同一记录,由于在搜索过程中有另一个事务对其进行了修改,从而导致多次读出的结果不一致。
    3. 幻读:在一个事务中,同样的两个查询在不同的时间内得到了不同的结果,与不可重复读不同的是,此处指插入和删除操作。

    如何解决脏读、幻读等问题:

    1. 脏读问题可以通过提高隔离级别解决,将隔离级别提高至“读已提交”和“可重复读”即可。
    2. 不可重复读问题可以通过锁定行数据来解决,例如使用SELECT … FOR UPDATE语句,实现数据加锁,避免其他事务对其进行修改。
    3. 幻读问题可以通过锁定表数据来解决,例如使用SELECT … LOCK IN SHARE MODE语句,实现表级锁定,避免其他事务对其进行插入或删除操作。

    总之,在选择隔离级别时,需要根据业务的需求和性能要求做出具体的选择。对于需要高并发、对数据一致性和完整性要求严格的场景建议选择高隔离级别,例如“可重复读”和“序列化”隔离级别。

  6. MVCC介绍,结合上一问讲解
    MVCC,即多版本并发控制,是一种数据库管理系统中的并发控制方法。该方法的主要特点是在数据进行更新时,同时将更新前的数据存储下来,从而使得不同的事务能够同时并发执行而不会互相影响。MVCC方法可以提高数据库系统的并发性能和效率,减少锁竞争的时间,提高数据的访问效率。

    1. MVCC是一种在事务级别上实现的并发控制方式,它利用历史版本代替锁来实现高并发的读写操作。在MVCC中,每一个更新的数据都被赋予了一个时间戳,开启一个新的事务后,该事务可以在当前的版本中获得数据的一份拷贝用于修改,而其他的事务可以同时在不同版本中获取数据的拷贝。因此,每个事务看到的数据版本不同,从而实现了不同的并发访问。
    2. MVCC的实现需要在数据结构中增加时间戳属性,以记录每个数据的更新时间。当对一个数据进行更新时,系统会在数据对应的历史版本中创建一个新版本,并将新版本的时间戳设置为当前时间,更新新版本的数据内容。在读操作时,会读取当前数据版本的快照,而使用已经过时的版本时,系统可以通过比对时间戳确定版本是否合法。
    3. MVCC在实现快照读(Snapshot Read)时非常高效,因为得到的快照总是对应了一个有效版本的数据,不需要任何锁来保护数据一致性。同时,MVCC的占用锁的时间也非常短,这意味着与锁竞争相关的性能瓶颈大大降低。
    4. 在MySQL中,InnoDB存储引擎即支持了MVCC,通过实现多版本的快照读,实现了高效并发的读写操作。但是,在并发环境中,MVCC也可能会带来一些问题,例如幻读问题等。

    但总体来说,MVCC是一种比较成熟、高效的并发控制方式,适用于高并发的数据库系统和大型互联网应用中。

  7. Redis中的hash底层实现讲一下

  8. HashMap默认加载因子,结合扩容机制
    在Java中,HashMap是一个非常实用的数据结构,它可以用来存储键值对。HashMap内部采用了哈希表的数据结构来实现,可以快速地进行插入、查询和删除等操作。其默认的加载因子为0.75。
    当HashMap中存储的键值对数量达到了负载因子容量的值时(即负载因子初始化容量),就会触发扩容操作,将容量增大为原来的两倍,并重新散列,将已有的键值对重新映射到新的容器中。扩容增加的容量都是2的幂次方,这是为了保证关键字之间的散列值均匀,避免对HashTable中条目链表的分配操作造成过大的负荷。
    在Java 8之前,HashMap扩容后,会将原来的数组中的元素一一重新散列到新的数组中,因为在数组中,每个元素都是一个链表的头节点,通过重新散列,所有键值对的散列值均匀分布在新的桶中,不会出现链表长度过长的情况。但是,在链表长度过长时,查询效率会降低,因此Java 8之后,引入了红黑树,将链表长度过长的桶转化为红黑树,提高了查询效率,同时采用了一定的优化策略,如树与链表的自适应选择等。
    Java在实现HashMap时,同时也考虑了线程安全问题,提供了ConcurrentHashMap类,它支持高效的并发访问,但对于并发写操作时,增加了锁的粒度,以保证线程安全性。当然,这也会带来一定的性能损失。而对于只会读取数据的场景,可以采用无锁的方式避免线程安全问题,例如使用ConcurrentHashMap的keySet()方法获取元素集,然后在集合上进行迭代,这种方式仍然保证了高性能的同时,避免了线程安全问题。

  9. hashtable底层实现

    Hashtable是Java中的一个散列表,它继承自Dictionary类并实现了Map接口。

    Hashtable的底层实现原理基于哈希表,采用数组加链表的方式来解决哈希冲突。
    具体地,Hashtable底层维护一个Entry[]数组,每个Entry是一个键值对,它包含了键、值和指向下一个Entry的指针。当向Hashtable中添加一个键值对时,会对键进行哈希函数计算,得到一个对应的索引位置,如果该位置已有元素,就产生了哈希冲突。这时,Hashtable通过链表将哈希冲突的元素存储在同一位置处,链表中每一个节点都是一个Entry,它包含一个指向下一个Entry的指针。

    Hashtable的put()方法实现如下:

    1. 计算key的hash值,得到一个对应的索引位置;
    2. 遍历该位置的链表,查找是否有相同的key,如果有,则将该key对应的value替换为新的value;
    3. 如果没有,则新建一个Entry,将key、value存入Entry,并将Entry添加到链表的头部;
    4. 检查当前Hashtable的大小,如果超过了阈值(负载因子*Hashtable的大小),则进行扩容操作,重新计算每个键的索引位置。

    Hashtable的get()方法实现如下:

    1. 计算key的hash值,得到对应的索引位置;
    2. 遍历该位置的链表,查找是否有相同的key,如果有,则返回相应的value;
    3. 如果没有,则返回null。

    Hashtable的扩容算法相对比较简单,每当Hashtable的大小超过阈值时,就会将Hashtable的大小扩大一倍,并将原有的所有元素重新散列到新的数组中。
    需要注意的是,Hashtable在进行哈希计算时,会调用key的hashCode()方法,同时在检查key相等时,也会调用key的equals()方法。因此,在使用Hashtable时应该保证key的hashCode()方法和equals()方法正确实现,否则可能会导致哈希算法出错。
    而在并发环境下,Hashtable的性能较低,因此建议使用ConcurrentHashMap等并发的哈希表替代。

算法题

  • 有重复数字的全排列

以上全部面试完结~

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值