牛客刷题-Java面试题库【动态更新添加题目】(2023.06.19更新)

20 篇文章 1 订阅
12 篇文章 0 订阅

讲在前面

✨ 牛客刷题日记–理解为重中之重

  1. 刷题一方面是持续的了解到自己哪方面比较欠缺,另一方面也是从各大厂的面试题可以看出当前所需的技术栈的偏重点,持续的巩固基础和查漏补缺,一如代码深似海–学无止境呐
  2. 对于刷题这件事,了解但表述不清是大问题,各个题目需要自己理解并且清晰的表达出来才算OK
  3. 相信教就是最好的学,即使咱们程序员是代码说话,但是当你能够清楚地表述出你的技术点,并且让别人也能够明白,那你一定是技术过硬并且是行业大咖了
  4. 晒出的解题为官方答复或者引用讨论区大神的优质解答,各位根据自己的 理解挑重点就好
  5. 当前主要聚集在Java基础Java框架和SQL模块,后续会整理出相应的模块,方便模块基础知识点巩固和查漏补缺

1 线程和协程的区别

  • 解题思路 得分点 地址空间、开销、并发性、内存 标准回答 进程和线程的主要差别在于它们是不同的操作系统资源管理方式。

    1. 进程有独立的地址空间,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间;
    2. 进程和线程切换时,需要切换进程和线程的上下文,进程的上下文切换时间开销远远大于线程上下文切换时间,耗费资源较大,效率要差一些;
    3. 进程的并发性较低,线程的并发性较高;
    4. 每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
    5. 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了 CPU 外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源;
    6. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮
  • 极简回答:

    • 协程是线程的抽象单位,减少了线程切换过程中的资源代价

引申一点-牛客很牛 问题是线程和协程的区别,解题思路是进程和线程的区别那么个人就浅浅分析一下协程

重点:区分并发编程的进程、线程、协程、纤程、管程之间区别

关于协程

  1. 是一种用户态的轻量级线程,协程的调度完全由用户控制,操作系统内核对协程一无所知;
  2. 协程的调度完全由应用程序来控制,操作系统不管这部分的调度;
  3. 协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。
  4. 一个线程可以包含一个或多个协程;
  5. 协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下纹和栈保存起来,在切换回来时恢复先前保运的寄存上下文和栈;
  6. 协程能保留上一次调用时的状态,看到这里各种生成器(生成器是被阉割的协程)的概念浮现出来了。

2 MySQL索引以及它们的好处和坏处

  • 解题思路 得分点 检索效率、存储资源、索引维护
  • 标准回答 索引就像指向表行的指针,是一种允许查询操作快速确定哪些行符合WHERE子句中的条件,并检索到这些行的其他列值的数据结构; 索引主要有普通索引、唯一索引、主键索引、外键索引、全文索引、复合索引几种; 在大数据量的查询中,合理使用索引的优点非常明显,不仅能大幅提高匹配where条件的检索效率,还能用于排序和分组操作的加速。 当时索引如果使用不当也有比较大的坏处:比如索引必定会增加存储资源的消耗;同时也增大了插入、更新和删除操作的维护成本,因为每个增删改操作后相应列的索引都必须被更新。
  • 加分回答 只要创建了索引,就一定会走索引吗? 不一定。 比如,在使用组合索引的时候,如果没有遵从“最左前缀”的原则进行搜索,则索引是不起作用的。 举例,假设在id、name、age字段上已经成功建立了一个名为MultiIdx的组合索引。索引行中按id、name、age的顺序存放,索引可以搜索id、(id,name)、(id, name, age)字段组合。如果列不构成索引最左面的前缀,那么MySQL不能使用局部索引,如(age)或者(name,age)组合则不能使用该索引查询。

口述简答:索引虽然会提高查询的效率,但会降低更新表的效率

举个简单的例子,索引就好比是一本书的目录,如果给书本排好了目录,那么只需要根据书本的目录,就可以定位到某个知识点的页码,而不用逐页逐页地查找,极大的节省查找书本内容的时间。

索引的好处: 1.能够提高数据的查询性能,降低数据库的IO成本。 2.能够提高数据分组、数据排序的性能。

索引的缺点: 1.索引需要占用一定的存储空间(创建和维护索引需要耗费时间和空间,会增加存储压力)2.对数据表的数据进行增、删、改、查时,MySQL内部需要对索引进行动态维护;如果是频繁进行修改的表,维护索引需要耗费更多的时间空间

个人理解毫无坏处哈哈哈,索引就是好用!

3 多线程【为什么使用多线程】

  • 解题思路 得分点 线程和进程的关系、为什么使用多线程
  • 标准回答 线程是操作系统调度的最小单元,它可以让一个进程并发地处理多个任务,也叫轻量级进程。所以,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈、局部变量,并且能够共享进程内的资源。由于共享资源,处理器便可以在这些线程之间快速切换,从而让使用者感觉这些线程在同时执行。 总的来说,操作系统可以同时执行多个任务,每个任务就是一个进程。进程可以同时执行多个任务,每个任务就是一个线程。一个程序运行之后至少有一个进程,而一个进程可以包含多个线程,但至少要包含一个线程。 使用多线程会给开发人员带来显著的好处,而使用多线程的原因主要有以下几点:
    1. 更多的CPU核心 现代计算机处理器性能的提升方式,已经从追求更高的主频向追求更多的核心发展,所以处理器的核心数量会越来越多,充分地利用处理器的核心则会显著地提高程序的性能。而程序使用多线程技术,就可以将计算逻辑分配到多个处理器核心上,显著减少程序的处理时间,从而随着更多处理器核心的加入而变得更有效率。
    2. 更快的响应时间 我们经常要针对复杂的业务编写出复杂的代码,如果使用多线程技术,就可以将数据一致性不强的操作派发给其他线程处理(也可以是消息队列),如上传图片、发送邮件、生成订单等。这样响应用户请求的线程就能够尽快地完成处理,大大地缩短了响应时间,从而提升了用户体验。 3. 更好的编程模型 Java为多线程编程提供了良好且一致的编程模型,使开发人员能够更加专注于问题的解决,开发者只需为此问题建立合适的业务模型,而无需绞尽脑汁地考虑如何实现多线程。一旦开发人员建立好了业务模型,稍作修改就可以将其方便地映射到Java提供的多线程编程模型上。

一个线程是程序中一个单一的控制流程,多线程就是单个程序中运行多个线程执行不同的任务,达到提高效率提高资源的利用率目的。

缺点:线程之间共享进程的资源,会容易造成死锁。对多个线程的管理会带来额外的CPU开销,同时系统上下文切换也会带来额外的负担。线程的终止会对程序造成影响。

  1. 线程是程序执行的最小单元,一个线程执行一个任务;各个线程共享程序的内存空间
  2. 一个进程可以拥有多个线程,各个线程之前共享进程的内存空间,有单独的栈内存,计数器,局部变量。
  3. 多线程优点:减少程序响应时间;提高cpu利用率;创建和切换开销小,数据共享效率高;简化程序结构
  4. 实现多线程有三种方式:继承Thread类,实现Runable接口,实现Callable接口

4 怎么保证线程安全

  • 解题思路 得分点 原子类、volatile、锁
  • 标准回答 保证线程安全的方式有很多,其中较为常用的有三种,按照资源占用情况由轻到重排列,这三种保证线程安全的方式分别是原子类、volatile、锁。 JDK从1.5开始提供了java.util.concurrent.atomic包,这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。在atomic包里一共提供了17个类,按功能可以归纳为4种类型的原子更新方式,分别是原子更新基本类型、原子更新引用类型、原子更新属性、原子更新数组。无论原子更新哪种类型,都要遵循“比较和替换”规则,即比较要更新的值是否等于期望值,如果是则更新,如果不是则失败。 volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”,从而可以保证单个变量读写时的线程安全。可见性问题是由处理器核心的缓存导致的,每个核心均有各自的缓存,而这些缓存均要与内存进行同步。volatile具有如下的内存语义:当写一个volatile变量时,该线程本地内存中的共享变量的值会被立刻刷新到主内存;当读一个volatile变量时,该线程本地内存会被置为无效,迫使线程直接从主内存中读取共享变量。 原子类和volatile只能保证单个共享变量的线程安全,锁则可以保证临界区内的多个共享变量的线程安全,Java中加锁的方式有两种,分别是synchronized关键字和Lock接口。synchronized是比较早期的API,在设计之初没有考虑到超时机制、非阻塞形式,以及多个条件变量。若想通过升级的方式让它支持这些相对复杂的功能,则需要大改它的语法结构,不利于兼容旧代码。因此,JDK的开发团队在1.5新增了Lock接口,并通过Lock支持了上述的功能,即:支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量(阻塞队列)。 加分回答 实现线程安全的方式有很多,除了上述三种方式之外,还有如下几种方式: 1. 无状态设计 线程安全问题是由多线程并发修改共享变量引起的,如果在并发环境中没有设计共享变量,则自然就不会出现线程安全问题了。这种代码实现可以称作“无状态实现”,所谓状态就是指共享变量。 2. 不可变设计 如果在并发环境中不得不设计共享变量,则应该优先考虑共享变量是否为只读的,如果是只读场景就可以将共享变量设计为不可变的,这样自然也不会出现线程安全问题了。具体来说,就是在变量前加final修饰符,使其不可被修改,如果变量是引用类型,则将其设计为不可变类型(参考String类)。 3. 并发工具 java.util.concurrent包提供了几个有用的并发工具类,一样可以保证线程安全: - Semaphore:就是信号量,可以控制同时访问特定资源的线程数量。 - CountDownLatch:允许一个或多个线程等待其他线程完成操作。 - CyclicBarrier:让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续运行。 4. 本地存储 我们也可以考虑使用ThreadLocal存储变量,ThreadLocal可以很方便地为每一个线程单独存一份数据,也就是将需要并发访问的资源复制成多份。这样一来,就可以避免多线程访问共享变量了,它们访问的是自己独占的资源,它从根本上隔离了多个线程之间的数据共享。

从理论上来说保证线程安全需要保证java内存模型的三大特性:原子性,可见性,有序性。从实际方法来说可以使用java.util.concurrent提供的原子类使用乐观的CAS方式来实现线程安全,使用jvm提供的synchronize这种悲观的处理方式来实现线程安全,使用jdk提供的reentrantLock,或是在某些特殊条件下使用volatile来保证数据的可见性和有序性来实现线程安全,但是volatile并不能一定保证线程安全。


通过synchronized关键字可以在单机情况下保证线程安全。

分布式情况下可以通过乐观锁、悲观锁以及分布式锁进行实现,保证线程安全。


保证线程安全的有三种方式:原子类、volatile、锁。

  1. 原子类:遵循CAS即“比较和替换”规则,比较要更新的值是否等于期望值,如果是则更新,如果不是则失败(单共享变量)
  2. volatile关键字:轻量级的synchronized,在多处理器开发中保证了共享变量的“可见性”,从而可以保证单个变量读写时的线程安全(单共享变量)
  3. synchronized,java中常用的锁有两种:synchronized+juc包下的lock锁。支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量

2023.04.03更新以下

5 死锁定义及发生的条件

  • 解题思路 得分点 争夺共享资源、相互等待、互斥条件、请求和保持条件、不剥夺条件、环路等待条件

  • 标准回答

    1. 死锁 两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。这些永远在互相等待的进程称为死锁进程。
    2. 产生死锁的必要条件 虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件:
      • 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放;
      • 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放;
      • 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放;
      • 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合 {P0,P1,P2,···,Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。
  • 死锁:两个或者两个以上的线程互相争夺对方的资源而不释放自己的资源,从而导致死锁的产生。
  • 死锁产生的条件:
    • 互斥:一个资源在同一个时刻只能由一个线程执行
    • 请求与保持:一个线程在请求被占用资源时,对已经获得的资源保持不放。
    • 循环等待:发生死锁时所有的线程都会形成一个死循环,一直阻塞。
    • 不可剥夺条件:线程对所获得的资源在未使用完时不能被其他线程剥夺,只能自己释放。 避免死锁的方法就是破坏死锁产生的条件。

6 进程间的通信方式

  • 解题思路 得分点 管道、命名管道、信号、消息队列、共享内存、内存映射、信号量、Socket
  • 标准答案 进程间通信主要包括:管道、命名管道、信号、消息队列、共享内存、内存映射、信号量、Socket:
    1. 管道 管道也叫无名(匿名)管道,它是是 UNIX 系统 IPC(进程间通信)的最古老形式,所有的 UNIX 系统都支持这种通信机制。管道本质其实是内核中维护的一块内存缓冲区,Linux 系统中通过 pipe() 函数创建管道,会生成两个文件描述符,分别对应管道的读端和写端。无名管道只能用于具有亲缘关系的进程间的通信。
    2. 命名管道 匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
    3. 信号 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
    4. 消息队列 消息队列就是一个消息的链表,可以把消息看作一个记录,具有特定的格式以及特定的优先级,对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程则可以从消息队列中读走消息,消息队列是随内核持续的。
    5. 共享内存 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快。
    6. 内存映射 内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
    7. 信号量 信号量主要用来解决进程和线程间并发执行时的同步问题,进程同步是并发进程为了完成共同任务采用某个条件来协调它们的活动。对信号量的操作分为 P 操作和 V 操作,P 操作是将信号量的值减 1,V 操作是将信号量的值加 1。当信号量的值小于等于 0 之后,再进行 P 操作时,当前进程或线程会被阻塞,直到另一个进程或线程执行了 V 操作将信号量的值增加到大于 0 之时。
    8. Socket 套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。Socket 一般用于网络中不同主机上的进程之间的通信。

1.管道:管道本质是内核中维护的一块内存缓冲区。

2.命名管道:因为无名管道只适用于具有亲缘关系的线程,所以就衍生出来命名管道,这样就可以实现了非亲缘关系进程之间的通信。

3.信号:一种通知机制。

4.消息队列:是一个消息链表,既可以读消息,也可以写消息。

5.共享内存:多个线程共享一片内存区域。

6.内存映射:就是将磁盘文件数据映射到内存,通过修改内存就能修改磁盘文件。

7socket接口,socket接口一般用于不同主机上进程之间的通信

7 谈谈你对MVC的理解

  • 解题思路 得分点 mvc概念,model、view、controller模块功能
  • 标准回答 MVC是一种设计模式,在这种模式下软件被分为三层,即Model(模型)、View(视图)、Controller(控制器)。
    1. Model代表的是数据,View代表的是用户界面,Controller代表的是数据的处理逻辑,它是Model和View这两层的桥梁。将软件分层的好处是,可以将对象之间的耦合度降低,便于代码的维护。 Model:指从现实世界中抽象出来的对象模型,是应用逻辑的反应;它封装了数据和对数据的操作,是实际进行数据处理的地方(模型层与数据库才有交互)。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据是中立的,模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。
    2. View:负责进行模型的展示,一般就是我们见到的用户界面。
    3. Controller:控制器负责视图和模型之间的交互,控制对用户输入的响应、响应方式和流程;它主要负责两方面的动作,一是把用户的请求分发到相应的模型,二是把模型的改变及时地反映到视图上。
  • 加分回答 为了解耦以及提升代码的可维护性,服务端开发一般会对代码进行分层,服务端代码一般会分为三层:表现层、业务层、数据访问层。
    • 在浏览器访问服务器时,请求会先到达表现层 最典型的MVC就是jsp+servlet+javabean模式。
    • 以JavaBean作为模型,既可以作为数据模型来封装业务数据,又可以作为业务逻辑模型来包含应用的业务操作。
    • JSP作为视图层,负责提供页面为用户展示数据,提供相应的表单(Form)来用于用户的请求,并在适当的时候(点击按钮)向控制器发出请求来请求模型进行更新。
    • Serlvet作为控制器,用来接收用户提交的请求,然后获取请求中的数据,将之转换为业务模型需要的数据模型,然后调用业务模型相应的业务方法进行更新,同时根据业务执行结果来选择要返回的视图。
    • 当然,这种方式现在已经不那么流行了,Spring MVC框架已经成为了MVC模式的最主流实现。 Spring MVC框架是基于Java的实现了MVC框架模式的请求驱动类型的轻量级框架。前端控制器是DispatcherServlet接口实现类,映射处理器是HandlerMapping接口实现类,视图解析器是ViewResolver接口实现类,页面控制器是Controller接口实现类。

2023.04.06更新

8 详细的说说Redis的数据类型

  • 解题思路 得分点 Redis5种数据结构
  • 标准回答
    • Redis主要提供了5种数据结构:字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(zset)。Redis还提供了Bitmap、HyperLogLog、Geo类型,但这些类型都是基于上述核心数据类型实现的。5.0版本中,Redis新增加了Streams数据类型,它是一个功能强大的、支持多播的、可持久化的消息队列。
    • string可以存储字符串、数字和二进制数据,除了值可以是String以外,所有的键也可以是string,string最大可以存储大小为2M的数据。
    • list保证数据线性有序且元素可重复,它支持lpush、blpush、rpop、brpop等操作,可以当作简单的消息队列使用,一个list最多可以存储2^32-1个元素
    • hash的值本身也是一个键值对结构,最多能存储2^32-1个元素
    • set是无序不可重复的,它支持多个set求交集、并集、差集,适合实现共同关注之类的需求,一个set最多可以存储2^32-1个元素
    • zset是有序不可重复的,它通过给每个元素设置一个分数来作为排序的依据,一个zset最多可以存储2^32-1个元素。
  • 加分回答 每种类型支持多个编码,每一种编码采取一个特殊的结构来实现 各类数据结构内部的编码及结构: string:编码分为int、raw、embstr;int底层实现为long,当数据为整数型并且可以用long类型表示时可以用long存储;embstr底层实现为占一块内存的SDS结构,当数据为长度不超过32字节的字符串时,选择以此结构连续存储元数据和值;raw底层实现为占两块内存的SDS,用于存储长度超过32字节的字符串数据,此时会在两块内存中分别存储元数据和值。 list:编码分为ziplist、linkedlist和quicklist(3.2以前版本没有quicklist)。ziplist底层实现为压缩列表,当元素数量小于2且所有元素长度都小于64字节时,使用这种结构来存储;linkedlist底层实现为双端链表,当数据不符合ziplist条件时,使用这种结构存储;3.2版本之后list一般采用quicklist的快速列表结构来代替前两种。 hash:编码分为ziplist、hashtable两种,其中ziplist底层实现为压缩列表,当键值对数量小于2,并且所有的键值长度都小于64字节时使用这种结构进行存储;hashtable底层实现为字典,当不符合压缩列表存储条件时,使用字典进行存储。 set:编码分为inset和hashtable,intset底层实现为整数集合,当所有元素都是整数值且数量不超过2个时使用该结构存储,否则使用字典结构存储。 zset:编码分为ziplist和skiplist,当元素数量小于128,并且每个元素长度都小于64字节时,使用ziplist压缩列表结构存储,否则使用skiplist的字典+跳表的结构存储。
  • Redis的数据结构分为5种,字符串(String)、哈希(hash)、列表(list)、集合(set)、有序集合(zset)
    • 字符串类型(String),其底层是一个简单的动态字符串,可以存储字符串,数字和二进制数据。
    • 哈希(hash)底层结构为ziplist和hashtable两种,其存储的是键值对。
    • 列表(list),存储线性有序且可重复的数据,底层结构有ziplist和linkedList,quicklist三种。
    • 集合(set)无序不可重复,底层结构分为inset和HashTable两种。
    • 有序集合(zset),有序不可重复,通过给每个元素设置一个分数来作为排序的依据,底层结构为ziplist和skiplist

9 乐观锁和悲观锁

  • 解题思路 得分点 乐观锁、悲观锁定义及使用场景
  • 标准回答 乐观锁:乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量**,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。 悲观锁:悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。 加分回答 两种锁的使用场景 乐观锁: GIT,SVN,CVS等代码版本控制管理器,就是一个乐观锁使用很好的场景,例如:A、B程序员,同时从SVN服务器上下载了code.html文件,当A完成提交后,此时B再提交,那么会报版本冲突,此时需要B进行版本处理合并后,再提交到服务器。这其实就是乐观锁的实现全过程。如果此时使用的是悲观锁,那么意味者所有程序员都必须一个一个等待操作提交完,才能访问文件,这是难以接受的。 悲观锁: 悲观锁的好处在于可以减少并发,但是当并发量非常大的时候,由于锁消耗资源、锁定时间过长等原因,很容易导致系统性能下降,资源消耗严重。因此一般我们可以在并发量不是很大,并且出现并发情况导致的异常用户和系统都很难以接受的情况下,会选择悲观锁进行。
  1. 乐观锁总是假设最好的情况,就是每次去拿数据的时候都认为别人不会修改,但在更新的时候会判断别人有没有去更新这个数据,可以通过版本号机制和CAS算法实现、乐观锁适用于多读的应用类型,这样可以提高吞吐量,例如redis。
  2. 悲观锁总是假设最坏的情况,就是认为每次去拿数据的时候都会认为别人会修改数据,所以每次拿数据的时候都会上锁,常使用在传统关系型数据库中。
  3. mysql数据库的共享锁和排他锁都是悲观锁的实现。

10 设计模式了解么

  • 解题思路 得分点 单例模式、工厂模式
  • 标准回答 创建型包括:单例模式、工厂方法模式、抽象工厂模式、建造者模式和原型模式; 结构型包括:代理模式、装饰模式、适配器模式、组合模式、桥梁模式、外观模式和享元模式; 行为型包括:模板方法模式、命令模式、责任链模式、策略模式、迭代器模式、中介者模式、观察者模式、备忘录模式、访问者模式、状态模式和解释器模式。 面试中不要求23种设计模式全部了解,但至少应掌握单例模式和工厂模式。
  • 加分回答 可以说出知道的框架所用到的设计模式或底层设计模式,例如Spring中的单例模式、工厂模式,AQS的模板模式等等
  1. 常用的设计模式有单例模式、工厂模式、代理模式、适配器模式、装饰器模式、模板方法模式等等。
  2. 像sping中的定义的bean默认为单例模式,spring中的BeanFactory用来创建对象的实例,他是工厂模式的体现。
  3. AOP面向切面编程时代理模式的体现,它的底层就是基于动态代理实现的。适配器模式在springMVC中有体现,它的处理器适配器会根据处理器规则适配相应的处理器执行,模板方法模式用来解决代码重复的问题等

11 说说你对AOP的理解

  • 解题思路 得分点 AOP概念、AOP作用、AOP的实现方式
  • 标准回答
    • AOP是一种编程思想,是通过预编译方式和运行期动态代理的方式实现不修改源代码的情况下给程序动态统一添加功能的技术。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。
    • AOP技术利用一种称为“横切”的技术,剖解开封装对象的内部,将影响多个类的公共行为封装到一个可重用的模块中,并将其命名为切面。所谓的切面,简单来说就是与业务无关,却为业务模块所共同调用的逻辑,将其封装起来便于减少系统的重复代码,降低模块的耦合度,有利用未来的可操作性和可维护性。 利用AOP可以对业务逻辑各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率。
    • AOP可以有多种实现方式,而Spring AOP支持如下两种实现方式。
      • JDK动态代理:这是Java提供的动态代理技术,可以在运行时创建接口的代理实例。Spring AOP默认采用这种方式,在接口的代理实例中织入代码。
      • CGLib动态代理:采用底层的字节码技术,在运行时创建子类代理的实例。当目标对象不存在接口时,Spring AOP就会采用这种方式,在子类实例中织入代码。
  • 加分回答 在应用场景方面,Spring AOP为IoC的使用提供了更多的便利,一方面,应用可以直接使用AOP的功能,设计应用的横切关注点,把跨越应用程序多个模块的功能抽象出来,并通过简单的AOP的使用,灵活地编制到模块中,比如可以通过AOP实现应用程序中的日志功能。另一方面,在Spring内部,例如事务处理之类的一些支持模块也是通过Spring AOP来实现的。
  • AOP不能增强的类:
    1. Spring AOP只能对IoC容器中的Bean进行增强,对于不受容器管理的对象不能增强。
    2. 由于CGLib采用动态创建子类的方式生成代理对象,所以不能对final修饰的类进行代理。

AOP(aop: aspect oriented programming) 有切面,连接点,通知,切点。是spring两大核心之一,是一种编程思想,是对OOP的一种补充。它可以对业务逻辑的各个部分进行隔离,降低耦合,提高代码的可重用性。它的底层是通过动态代理实现的。它的应用场景有事务、日志管理等。(在方法上开一个切口,可以在这个类执行之前执行自定义的方法,在不修改源代码的前提下,对方法进行增强)

  1. aop是基于动态代理的技术,在程序运行的时候,动态的修改class字节码生成新的class文件进行运行的技术,可以对某个类的某个方法、多个类的多个方法,通过配置切点表达式进行匹配,然后改造被匹配到的类的某一些方法,然后运行;
  2. 动态代理已知两种
    1. cjlib的基于子类的动态代理(可以在运行期间创建子类的动态实例),不能对final修饰的类进行代理
    2. jdk自带的基于接口的动态代理(可以在运行时创建接口的代理实例),SringAOP只能对IOC容器中的Bean进行增强,对于不受容器管理的对象不能增强
  3. 常见的aop使用场景,日志统一管理事务管理器。

2023.04.09更新

12 说说Redis的持久化策略

  • 解题思路 得分点 RDB、AOF
  1. reids的持久化方式有两种:RDB和AOF
    1. RDB持久化:将当前进程中的数据已生成快照的方式保存到硬盘中,是redis默认的持久化机制。(指定间隔内将数据集快照写进磁盘中的dump.rdb)
      1. 优点:持久化时生成的文件体积小,恢复数据快
      2. 缺点:每次运行都需要执行fork操作,RDB持久化策略,没有做到实时的持久化,有时可能会丢失最后一步的数据。
    2. RDB持久化:以独立日志的方式记录每次写入的命令,重启时执行AOF中的命令即可恢复数据。(Append only file以日志形式记录每个写操作,只允许追加不允许改写)
      1. 优点:AOF持久化的安全性更高,保证了数据持久化的实时性。
      2. 缺点:文件要大很多,恢复速度慢。
  2. RDB-AOF持久化:这种方式是基于AOF持久化方式构建出来的。兼具RDB和AOF的优势。
  3. 优先级AOF>RDB
  4. Redis默认是用RDB文件存储

13 请你讲讲单例模式、请你手写一下单例模式

  • 解题思路 得分点 饿汉式单例模式、懒汉式单例模式、线程安全的懒汉式单例模式

  • 标准回答 单例模式(Singleton Pattern)是最简单的创建型设计模式。它会确保一个类只有一个实例存在。单例模式最重要的特点就是构造函数私有,从而避免外界直接使用构造函数直接实例化该类的对象。 单例模式在Java种通常有两种表现形式:

    • 饿汉式:类加载时就进行对象实例化

    • 懒汉式:第一次引用类时才进行对象实例化 饿汉式单例模式: 在类被加载时就会初始化静态变量instance,这时候类的私有构造函数就会被调用,创建唯一的实例。

      public class Singleton{ 
      	private static Singleton instance = new Singleton(); // 构造方法私有,确保外界不能直接实例化 
          private Singleton(){ } //通过公有的静态方法获取对象实例 
          
          public static Singleton getInstance(){ 
              return instance; 
          } 
      } 
      

      懒汉式单例模式: 类在加载时不会初始化静态变量instance,而是在第一次被调用时将自己初始化

      public class Singleton { 
          private static Singleton instance = null; // 私有构造方法,确保外界不能直接实例化。 
          private Singleton() {} // 通过公有的静态方法获取对象实例 
          public static Singleton getInstace() { 
              if (instance == null) { 
                  instance = new Singleton(); 
              } return instance; 
          } 
      } 
      
    • 但这时有一个问题,如果线程A和B同时调用此方法,会出现执行if (instance == null)语句时都为真的情况,那么线程AB都会创建一个对象,那内存中就会出现两个对象,这违反了单例模式的定义。为解决这一问题,可以使用synchronized关键字对静态方法 getInstance()进行同步,线程安全的的懒汉式单例模式代码如下:

      public class Singleton { 
          private static Singleton instance = null; // 私有构造方法,确保外界不能直接实例化。 
          private Singleton() {} // 通过公有的静态方法获取对象实例 
          synchronized public static Singleton getInstace() { 
              if (instance == null) { 
                  instance = new Singleton(); 
              } return instance; 
          } 
      }
      
    • 饿汉式单例类在资源利用效率上不如懒汉式单例类,但从速度和反应时间来看,饿汉式单例类要优于懒汉式单例类。

  • 加分回答 单例模式的优点:

    • 在一个对象需要频繁的销毁、创建,而销毁、创建性能又无法优化时,单例模式的优势尤其明显
    • 在一个对象的产生需要比较多资源时,如读取配置、产生其他依赖对象时,则可以通过在启用时直接产生一个单例对象,然后用永久驻留内存的方式来解决
    • 单例模式可以避免对资源的多重占用,因为只有一个实例,避免了对一个共享资源的并发操作
    • 单例模式可以在系统设置全局的访问点,优化和共享资源访问 单例模式的缺点:
      • 单例模式无法创建子类,扩展困难,若要扩展,除了修改代码基本上没有第二种途径可以实现
      • 单例模式对测试不利。在并行开发环境中,如果采用单例模式的类没有完成,是不能进行测试的
      • 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是要用单例模式取决于环境

单例模式分为饿汉模式,懒汉模式,饿汉模式是实例提前创建好放到内存中等待使用,懒汉模式是在第一次对象调用方法执行时创建实例,线程安全的。单例模式是指一个对象只能实例化一次,节省内存,但是会降低性能,优点是节省资源,降低创建,销毁性能消耗。缺点是不能扩展,不能创建子类,对测试不利,并行开发环境中单例类没有完成不能测试

单利模式 public class SingleTon { private static single= null; public Single(){ } public syncronized static Single getSingle(){ if( single = null){ single = new single(); } } }

14 虚拟内存和物理内存的区别

  • 标准回答
    • 物理内存 以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于 CPU 的地址线条数。比如在 32 位平台下,寻址的范围是 2^32 也就是 4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给 4G 物理内存,就可能会出现很多问题:
      • 因为物理内存是有限的,当有多个进程要执行的时候,都要给 4G 内存,很显然内存不够,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作效率很低 - 由于指令都是直接访问物理内存的,那么任何进程都可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是不安全的
    • 虚拟内存 由于物理内存有很多问题,所以出现了虚拟内存。虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

理内存:计算机中真实拥有的内存。物理内存是有限的,容易产生内存不足问题。虚拟内存是一种抽象的逻辑概念,拥有连续的内存地址。

2023.04.13更新

15 说说你对IoC的理解

  • 解题思路 得分点 控制反转与依赖注入含义
  • 标准回答 IoC是控制反转的意思,是一种面向对象编程的设计思想。在不采用这种思想的情况下,我们需要自己维护对象与对象之间的依赖关系,很容易造成对象之间的耦合度过高,在一个大型的项目中这十分的不利于代码的维护。IoC则可以解决这种问题,它可以帮我们维护对象与对象之间的依赖关系,并且降低对象之间的耦合度。 说到IoC就不得不说DI,DI是依赖注入的意思,它是IoC实现的实现方式。由于IoC这个词汇比较抽象而DI比较直观,所以很多时候我们就用DI来代替它,在很多时候我们简单地将IoC和DI划等号,这是一种习惯。实现依赖注入的关键是IoC容器,它的本质就是一个工厂。
  • 加分回答 IoC是Java EE企业应用开发中的就偶组件之间复杂关系的利器。 在以Spring为代表的轻量级Java EE开发风行之前,实际开发中是使用更多的是EJB为代表的开发模式。在EJB开发模式中,开发人员需要编写EJB组件,这种组件需要满足EJB规范才能在EJB容器中运行,从而完成获取事务,生命周期管理等基本服务,Spring提供的服务和EJB并没有什么区别,只是在具体怎样获取服务的方式上两者的设计有很大不同:Spring IoC提供了一个基本的JavaBean容器,通过IoC模式管理依赖关系,并通过依赖注入和AOP切面增强了为JavaBean这样的POJO对象服务于事务管理、生命周期管理等基本功能;而对于EJB,一个简单的EJB组件需要编写远程/本地接口、Home接口和Bean的实体类,而且EJB运行不能脱离EJB容器,查找其他EJB组件也需要通过诸如JNDI的方式,这就造成了对EJB容器和技术规范的依赖。也就是说Spring把EJB组件还原成了POJO对象或者JavaBean对象,以此降低了用用开发对于传统J2EE技术规范的依赖。 在应用开发中开发人员设计组件时往往需要引用和调用其他组件的服务,这种依赖关系如果固化在组件设计中,会造成依赖关系的僵化和维护难度的增加,这个时候使用IoC把资源获取的方向反转,让IoC容器主动管理这些依赖关系,将这些依赖关系注入到组件中,这就会让这些依赖关系的适配和管理更加灵活。

Ioc是spring的基本特性之一,意思是控制反转,将原本由人完成的类的实例化等操作交给容器去完成,可以降低对象之间的耦合度,实现方式为di:依赖注入

  1. IoC:控制反转,一种面向对象的编程思想。是Spring框架种的一个核心理论思想,主张将对象的创建、属性赋值、生命周期等操作的控制交由spring容器进行管理,降低代码之间的耦合性。
  2. 控制:对象的创建的控制权限;
  3. 反转:将对象的控制权限交给spring。
  4. 之前我们创建对象时用new,现在直接从spring容器中取,维护对象之间的依赖关系,降低对象之间的耦合度。
  5. 实现方式为DI,依赖注入,有三种注入方式:构造器、setter、接口注入
    • 接口注入已经被淘汰了
    • 属性setter注入方式,Spring通过无参构造或无参静态工厂方法实例化Bean对象后,再通过调用该Bean的setter方法实现注入。
    • 构造器注入,Spring直接通过调用有参的构造方法实现依赖注入,每一个参数就是一个依赖。
  6. @AutoWired是Spring的注解只能在Spring中使用,默认采用类型匹配的方式,可以通过配合@Qualifier实现名称匹配。@Resource是JDK自带的注解,Java标准,适用于大部分框架,默认是名称匹配,匹配不到了进行类型匹配,还匹配不到就抛出异常

16 请你说说内存管理

  • 解题思路 得分点 段页式内存管理方式
  • 准回答 Linux 操作系统是采用段页式内存管理方式: 页式存储管理能有效地提高内存利用率(解决内存碎片),而分段存储管理能反映程序的逻辑结构并有利于段的共享。将这两种存储管理方法结合起来,就形成了段页式存储管理方式。 段页式存储管理方式即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。在段页式系统中,为了实现从逻辑地址到物理地址的转换,系统中需要同时配置段表和页表,利用段表和页表进行从用户地址空间到物理内存空间的映射。 系统为每一个进程建立一张段表,每个分段有一张页表。段表表项中至少包括段号、页表长度和页表始址,页表表项中至少包括页号和块号。在进行地址转换时,首先通过段表查到页表始址,然后通过页表找到页帧号,最终形成物理地址。
  1. 内存管理有:页式存储,段式存储,段页式存储。
    1. 页式存储,提高内存的利用率,有效减少内存碎片
    2. 段式存储,更好的反应程序的逻辑结构,有利于段之间的共享 (反映程序的逻辑结构并有利于段的共享)
    3. 段页式存储,结合前面两种存储方式,将程序划分为若干段 linux就是采用这种方式(段页式存储管理先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。并利用段表和页表进行从用户地址空间到物理内存空间的映射。)

17 IO多路复用(select、poll、epoll)

  • 解题思路 得分点 概念、select、poll、epoll
  • 标准回答
    1. I/O 多路复用是一种使得程序能同时监听多个文件描述符的技术,从而提高程序的性能。I/O 多路复用能够在单个线程中,通过监视多个 I/O 流的状态来同时管理多个 I/O 流,一旦检测到某个文件描述符上我们关心的事件发生(就绪),能够通知程序进行相应的处理(读写操作)。
    2. Linux 下实现 I/O 复用的系统调用主要有 select、poll 和 epoll。
      1. select select 的主旨思想:
        1. 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中,这个文件描述符的列表数据类型为 fd_set,它是一个整型数组,总共是 1024 个比特位,每一个比特位代表一个文件描述符的状态。比如当需要 select 检测时,这一位为 0 就表示不检测对应的文件描述符的事件,为 1 表示检测对应的文件描述符的事件。
        2. 调用 select() 系统调用,监听该列表中的文件描述符的事件,这个函数是阻塞的,直到这些描述符中的一个或者多个进行 I/O 操作时,该函数才返回,并修改文件描述符的列表中对应的值,0 表示没有检测到该事件,1 表示检测到该事件。函数对文件描述符的检测的操作是由内核完成的。
        3. select() 返回时,会告诉进程有多少描述符要进行 I/O 操作,接下来遍历文件描述符的列表进行 I/O 操作。 select 的缺点: 1. 每次调用select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大; 2. 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大; 3. select 支持的文件描述符数量太小了,默认是 1024(由 fd_set 决定); 4. 文件描述符集合不能重用,因为内核每次检测到事件都会修改,所以每次都需要重置; 5. 每次 select 返回后,只能知道有几个 fd 发生了事件,但是具体哪几个还需要遍历文件描述符集合进一步判断。
      2. poll poll 的原理和 select 类似,poll 支持的文件描述符没有限制。
      3. epoll epoll 是一种更加高效的 IO 复用技术,epoll 的使用步骤及原理如下:
        1. 调用 epoll_create() 会在内核中创建一个 eventpoll 结构体数据,称之为 epoll 对象,在这个结构体中有 2 个比较重要的数据成员,一个是需要检测的文件描述符的信息 struct_root rbr(红黑树),还有一个是就绪列表struct list_head rdlist,存放检测到数据发送改变的文件描述符信息(双向链表);
        2. 调用 epoll_ctrl() 可以向 epoll 对象中添加、删除、修改要监听的文件描述符及事件; - 调用 epoll_wt() 可以让内核去检测就绪的事件,并将就绪的事件放到就绪列表中并返回,通过返回的事件数组做进一步的事件处理。
      4. epoll 的两种工作模式:
        1. LT 模式(水平触发) LT(Level - Triggered)是缺省的工作方式,并且同时支持 Block 和 Nonblock Socket。在这种做法中,内核检测到一个文件描述符就绪了,然后可以对这个就绪的 fd 进行 IO 操作,如果不作任何操作,内核还是会继续通知
        2. 。ET 模式(边沿触发) ET(Edge - Triggered)是高速工作方式,只支持 Nonblock socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 检测到。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 进行 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。 ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
  1. IO多路复用是指单个线程或进程能够处理多个IO请求。
  2. select调用主要统计有多少个文件描述符需要进行IO操作。缺点:内存开销大,支持的的文件描述符的个数有限。
  3. poll和select调用差别不大,主要是底层数据结构变成了链表,支持的文件描述符的个数无上限。epoll调用是更加高效的方式,底层数据结构使用了红黑树和链表,避免了大量的内存分配和轮询

2023.02.19

18 线程和协程的区别

  • 标准回答
    1. 线程是操作系统的资源,线程的创建、切换、停止等都非常消耗资源,而创建协程不需要调用操作系统的功能,编程语言自身就能完成,所以协程也被称为用户态线程,协程比线程轻量很多;
    2. 线程在多核环境下是能做到真正意义上的并行,而协程是为并发而产生的;
    3. 一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行;
    4. 线程进程都是同步机制,而协程则是异步;
    5. 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力;
    6. 操作系统对于线程开辟数量限制在千的级别,而协程可以达到上万的级别
  1. 线程是创建、切换、停止都是要调用操作系统来实现的,协程则可以通过编程语言实现
  2. 进程、线程是同步的,协程是异步的
  3. 线程是抢占式,协程是非抢占式,需要用户主动释放cpu
  4. 线程在多核状态能实现并行,而协程是为了用来支持并发的

19 MySQL的事务隔离级别

  • 解题思路 得分点 未提交读、已提交读、可重复读、可串行化 标

  • 准回答 SQL 标准定义了四种隔离级别,这四种隔离级别分别是:

    1. 读未提交(READ UNCOMMITTED);
    2. 读提交 (READ COMMITTED);
    3. 可重复读 (REPEATABLE READ);
    4. 串行化 (SERIALIZABLE)。
  • 事务隔离是为了解决脏读、不可重复读、幻读问题,下表展示了 4 种隔离级别对这三个问题的解决程度:

  • 隔离级别脏读不可重复读幻读
    READ UNCOMMITTED可能可能可能
    READ COMMITTED不可能可能可能
    REPEATABLE READ不可能不可能可能
    SERIALIZABLE不可能不可能不可能
  • 上述4种隔离级别MySQL都支持,并且InnoDB存储引擎默认的支持隔离级别是REPEATABLE READ,但是与标准SQL不同的是,InnoDB存储引擎在REPEATABLE READ事务隔离级别下,使用Next-Key Lock的锁算法,因此避免了幻读的产生。

    所以,InnoDB存储引擎在默认的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别;

  • 加分回答

    1. READ UNCOMMITTED: 它是性能最好、也最野蛮的方式,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。
    2. SERIALIZABLE: 读的时候加共享锁,其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
    3. REPEATABLE READ & READ COMMITTED: 为了解决不可重复读,MySQL 采用了 MVVC (多版本并发控制) 的方式。 我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,记为 row trx_id,而这个字段就是使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。
  1. 读未提交:读取了没有提交的一个数据,容易会产生脏读,幻读以及不可重复读
  2. 读已提交:读取了已提交的数据,避免了脏读的产生,但是仍出现不可重复读和幻读
  3. 可重复读:会产生幻读。MySQLinnodb中最高的隔离级别就是可重复读。可用MV CC并发版本控制来避免可重复读
  4. 串行化:最高的隔离级别,他要求的就是按照顺序一个一个来

事务就是说程序中一系列操作要么全都成功,要么全都失败,不能出现一个事务里,一些成功一些失败的情况。

  • 读未提交(read uncommited):一个事务可以读取到其他事务修改了但是还没有提交的数据,所以在该隔离级别下是不加任何共享锁或者排他锁的,相应的它的执行速度最快,但是会导致脏读,不可重复读和幻读的情况发生。
  • 串型化(seriazeable):当一个事务读取某个数据时,他会对整张表加共享锁,修改时,整张表加排他的,所以它的性能时最低的,但是脏读,幻读,不可重复读的问题不会出现。
  • 读已提交(readcommitted):指一个事务只能读到其他事务修改提交后的数据,若数据只是修改了还未提交,那么此时读取的数据便不是当前读而是有MVCC中readview提供的数据快照(老版本数据),可以避免脏读,不可避免不可重复读和幻读。
  • 可重复读(repeatable):是指一个事务连续两次读取到的数据都是相同的,无论这两次读取的过程中是否有其他事修改了该数据。他避免了脏读,不可重复读。不能避免幻读,同时它也是mysql默认的隔离级别。
  • 在MVCC中每一个事务都会有一个readview,在readcommited隔离级别中:一个事务每快照读一次都会重新生成一次readview。所以他无法避免不可重复度,而repeatable中,仅在事务开启时生成一次readview,后续的读取操作都会复用这个readview所以每次读取同一个数据时读取的值都是一样的,这就是它实现避免不可重复读的原理。

20 请说说你对反射的了解

  • j得分点 反射概念,通过反射机制可以实现什么
  • 标准回答 Java程序中,许多对象在运行时都会有编译时异常和运行时异常两种,例如多态情况下Car c = new Audi(); 这行代码运行时会生成一个c变量,在编译时该变量的类型是Car,运行时该变量类型为Audi;另外还有更极端的情况,例如程序在运行时接收到了外部传入的一个对象,这个对象的编译时类型是Object,但程序又需要调用这个对象运行时类型的方法,这种情况下,有两种解决方法:第一种做法是假设在编译时和运行时都完全知道类型的具体信息,在这种情况下,可以先使用instanceof运算符进行判断,再利用强制类型转换将其转换成其运行时类型的变量。第二种做法是编译时根本无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类的真实信息,这就必须使用反射。
  • 具体来说,通过反射机制,我们可以实现如下的操作:
    • 程序运行时,可以通过反射获得任意一个类的Class对象,并通过这个对象查看这个类的信息;
    • 程序运行时,可以通过反射创建任意一个类的实例,并访问该实例的成员;
    • 程序运行时,可以通过反射机制生成一个类的动态代理类或动态代理对象。
  • 加分回答 Java的反射机制在实际项目中应用广泛,常见的应用场景有:
    • 使用JDBC时,如果要创建数据库的连接,则需要先通过反射机制加载数据库的驱动程序;
    • 多数框架都支持注解/XML配置,从配置中解析出来的类是字符串,需要利用反射机制实例化;
    • 面向切面编程(AOP)的实现方案,是在程序运行时创建目标对象的代理类,这必须由反射机制来实现。
  1. 反射就是在程序运行期间动态的获取对象的属性和方法的功能叫做反射。它能够在程序运行期间,对于任意一个类,都能知道它所有的方法和属性,对于任意一个对象,都能知道他的属性和方法。
  2. 获取Class对象的三种方式:getClass(); xx.class; Class.forName("xxx");
  3. 反射的优缺点:
    1. 优点:运行期间能够动态的获取类,提高代码的灵活性。
    2. 缺点:性能比直接的Java代码要慢很多。
  4. 应用场景:spring的xml配置模式,以及动态代理模式都用到了反射。

21 如何利用Redis实现一个分布式锁?

  • 得分点 为什么要实现分布式锁、实现分布式锁的方式
  • 标准回答 在分布式的环境下,会发生多个server并发修改同一个资源的情况,这种情况下,由于多个server是多个不同的JRE环境,而Java自带的锁局限于当前JRE,所以Java自带的锁机制在这个场景下是无效的,那么就需要我们自己来实现一个分布式锁。 采用Redis实现分布式锁,我们可以在Redis中存一份代表锁的数据,数据格式通常使用字符串即可。 首先加锁的逻辑可以通过setnx key value来实现,但如果客户端忘记解锁,那么这种情况就很有可能造成死锁,但如果直接给锁增加过期时间即新增expire key seconds又会发生其他问题,即这两个命令并不是原子性的,那么如果第二步失败,依然无法避免死锁问题。考虑到如上问题,我们最终可以通过set...nx...命令,将加锁、过期命令编排到一起,把他们变成原子操作,这样就可以避免死锁。写法为set key value nx ex seconds 。 解锁就是将代表锁的那份数据删除,但不能用简单的del key,因为会出现一些问题。比如此时有进程A,如果进程A在任务没有执行完毕时,锁被到期释放了。这种情况下进程A在任务完成后依然会尝试释放锁,因为它的代码逻辑规定它在任务结束后释放锁,但是它的锁早已经被释放过了,那这种情况它释放的就可能是其他线程的锁。为解决这种情况,我们可以在加锁时为key赋一个随机值,来充当进程的标识,进程要记住这个标识。当进程解锁的时候进行判断,是自己持有的锁才能释放,否则不能释放。另外判断,释放这两步需要保持原子性,否则如果第二步失败,就会造成死锁。而获取和删除命令不是原子的,这就需要采用Lua脚本,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的。
  • 综上所述,优化后的实现分布式锁命令如下:
    • 加锁 set key random-value nx ex seconds
    • 解锁 if redis.call(“get”,KEYS[1]) == ARGV[1] then return redis.call(“del”,KEYS[1]) else return 0 end
  • 加分回答 上述的分布式锁实现方式是建立在单节点之上的,它可能存在一些问题,比如有一种情况,进程A在主节点加锁成功,但主节点宕机了,那么从节点就会晋升为主节点。那如果此时另一个进程B在新的主节点上加锁成功而原主节点重启了,成为了从节点,系统中就会出现两把锁,这违背了锁的唯一性原则。 总之,就是在单个主节点的架构上实现分布式锁,是无法保证高可用的。若要保证分布式锁的高可用,则可以采用多个节点的实现方案。这种方案有很多,而Redis的官方给出的建议是采用RedLock算法的实现方案。该算法基于多个Redis节点,它的基本逻辑如下:
    • 这些节点相互独立,不存在主从复制或者集群协调机制;
    • 加锁:以相同的KEY向N个实例加锁,只要超过一半节点成功,则认定加锁成功;
    • 解锁:向所有的实例发送DEL命令,进行解锁; 我们可以自己实现该算法,也可以直接使用Redisson框架。

最简单redis分布式锁的实现方式:加锁:setnx(key,1),解锁:del(key),问题:如果客户忘记解锁,将会出现死锁。第二种分布式锁的实现方式:setnx(key,1)+expire(key,30),解锁:del(key).问题:,由于setnx和expire的非原子性,当第二步挂掉,仍然会出现死锁。第三种方式:加锁:将setnx和expire变成原子性操作,set(key,1,30,NX),解锁:del(key)。同时考虑到线程A还在执行,但是锁已经到期,当线程A执行结束时去释放锁时,可能就会释放别的线程锁,所以在解锁时要先判断一下value值,看是不是该锁,如果是,再进行删除。

方案一:SETNX + EXPIRE

方案二:SETNX + value值是(系统时间 + 过期时间)

方案三:使用Lua脚本(包含SETNX + EXPIRE两条指令)

方案四:SET的扩展命令(SET EX PX NX)

方案五:SET EX PX NX + 校验唯一随机值,再释放锁

方案六:开源框架:Redisson

方案七:多机实现的分布式锁Redlock

2020.04.21

22 请你说说ArrayList和LinkedList的区别

  • 得分点 数据结构、访问效率
  • 标准回答
    1. ArrayList的实现是基于数组,LinkedList的实现是基于双向链表。
    2. 对于随机访问ArrayList要优于LinkedList,ArrayList可以根据下标以O(1)时间复杂度对元素进行随机访问,而LinkedList的每一个元素都依靠地址指针和它后一个元素连接在一起,查找某个元素的时间复杂度是O(N)。
    3. 对于插入和删除操作,LinkedList要优于ArrayList,因为当元素被添加到LinkedList任意位置的时候,不需要像ArrayList那样重新计算大小或者是更新索引。
    4. LinkedList比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

ArrayList底层是数组实现的,数组是一组连续的内存单元,读取快(使用索引),插入删除慢(需要重新计算大小或是更新索引) --底层是一个双向链表和双端队列特点,查询慢,增删快

LinkedList底层基于双向链表,读取慢,插入删除快;链表的每一个节点保存了数据值,和指向前一个节点的指针和指向后一个节点的指针,LinkedList比Array更占内存 --底层是一个双向链表和双端队列特点,查询慢,增删快

共同点;都是线程不安全的.

23 请你说说聚簇索引和非聚簇索引

  • 得分点 索引即数据、二次查询
  • 标准回答 两者主要区别是数据和索引是否分离。
    • 聚簇索引是将数据与索引存储到一起,找到索引也就找到了数据;
    • 而非聚簇索引是将数据和索引存储分离开,索引树的叶子节点存储了数据行的地址。
    • 在InnoDB中,一个表有且仅有一个聚簇索引(因为原始数据只留一份,而数据和聚簇索引在一起),并且该索引是建立在主键上的,即使没有指定主键,也会特殊处理生成一个聚簇索引;其他索引都是辅助索引,使用辅助索引访问索引外的其他字段时都需要进行二次查找。 而在MyISAM中,所有索引都是非聚簇索引,叶子节点存储着数据的地址,对于主键索引和普通索引在存储上没有区别。
  • 加分回答 在InnoDB存储引擎中,可以将B+树索引分为聚簇索引和辅助索引(非聚簇索引)。无论是何种索引,每个页的大小都为16KB,且不能更改。 聚簇索引是根据主键创建的一棵B+树,聚簇索引的叶子节点存放了表中的所有记录。辅助索引是根据索引键创建的一棵B+树,与聚簇索引不同的是,其叶子节点仅存放索引键值,以及该索引键值指向的主键。也就是说,如果通过辅助索引来查找数据,那么当找到辅助索引的叶子节点后,很有可能还需要根据主键值查找聚簇索引来得到数据,这种查找方式又被称为书签查找。因为辅助索引不包含行记录的所有数据,这就意味着每页可以存放更多的键值,因此其高度一般都要小于聚簇索引。

两者主要的区别是数据和索引的分离,聚簇不分离,非聚簇分离

它们两个的最大区别就是索引和数据是否存放在一起。

  • 聚簇索引:索引和数据存放在一起,叶子节点保留数据行。
  • 非聚簇索引:索引和数据分开存放,叶子节点存放的是指向数据行的地址。

24 数据库为什么不用红黑树而用B+树?

  • 得分点 磁盘IO
  • 标准回答 首先,红黑树是一种近似平衡二叉树(不完全平衡),结点非黑即红的树,它的树高最高不会超过 2*log(n),因此查找的时间复杂度为 O(log(n)),无论是增删改查,它的性能都十分稳定; 但是,红黑树本质还是二叉树,在数据量非常大时,需要访问+判断的节点数还是会比较多,同时数据是存在磁盘上的,访问需要进行磁盘IO,导致效率较低; 而B+树是多叉的,可以有效减少磁盘IO次数;同时B+树增加了叶子结点间的连接,能保证范围查询时找到起点和终点后快速取出需要的数据。
  • 加分回答 红黑树做索引底层数据结构的缺陷 试想一下,以红黑树作为底层数据结构在面对在些表数据动辄数百万数千万的场景时,创建的索引它的树高得有多高? 索引从根节点开始查找,而如果我们需要查找的数据在底层的叶子节点上,那么树的高度是多少,就要进行多少次查找,数据存在磁盘上,访问需要进行磁盘IO,这会导致效率过低; 那么红黑树作为索引数据结构的弊端即是:树的高度过高导致查询效率变慢。

索引的数据结构会被存储在磁盘中,每次查询都需要到磁盘中访问,对于红黑树,树的高度可能会非常的高,会进行很多次的磁盘IO,效率会非常低,B+树的高度一般为2-4,也就是说在最坏的条件下,也最多进行2到4次磁盘IO,这在实际中性能时非常不错的

红黑树本质上还是二叉树,一个结点最多只能拥有两个子结点,而B+树则是多叉的,这会使得相同的数据存储,二叉树的高度会大于B+树;而数据是存储在磁盘上的,树的高度越高,磁盘IO次数越多,开销越大,B+树可以有效的减少这一开销;且B+树的叶子节点是通过链表的方式进行相连的,能在找到起点和终点后快速取出需要的数据

25 请你说说Redis的数据类型

  • 得分点 string、hash、list、set、zset
  • 标准回答 Redis主要提供了5种数据结构:字符串(String)、哈希(Hash)、列表(List)、集合(set)、有序集合(zset)。
    1. String是一组字节。在 Redis 数据库中,字符串是二进制安全的。这意味着它们具有已知长度,并且不受任何特殊终止字符的影响。可以在一个字符串中存储最多 2 兆字节的内容。 Redis 列表定义为字符串列表,按插入顺序排序。可以将元素添加到 Redis 列表的头部或尾部。列表的最大长度为 232 – 1 个元素(超过 40 亿个元素)。
    2. 哈希是键值对的集合。在 Redis 中,哈希是字符串字段和字符串值之间的映射。因此,它们适合表示对象。每个哈希可以存储多达 232– 1 个字段-值对。
    3. 集合(set)是 Redis 数据库中的无序字符串集合。在 Redis 中,在redis sorted sets里面当items内容大于64的时候同时使用了和skiplist两种设计实现。这也会为了排序和查找性能做的优化。关于时间复杂度:添加和删除都需要修改skiplist,所以复杂度为O(log(n))。 但是如果仅仅是查找元素的话可以直接使用hash,其复杂度为O(1) ,其他的range操作复杂度一般为O(log(n)),当然如果是小于64的时候,因为是采用了ziplist的设计,其时间复杂度为O(n)集合中的最大成员数为 232-1 个元素(超过 40 亿个元素)。
    4. Redis 有序集合类似于 Redis 集合,也是一组非重复的字符串集合。但是,排序集的每个成员都与一个分数相关联,该分数用于获取从最小到最高分数的有序排序集。虽然成员是独特的,但可以重复分数。
  • 加分回答 Redis还提供了Bitmap、HyperLogLog、Geo类型,但这些类型都是基于上述核心数据类型实现的。5.0版本中,Redis新增加了Streams数据类型,它是一个功能强大的、支持多播的、可持久化的消息队列。

Redis主要提供了5种数据结构:字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(zset)。

  • 五种基本数据类型:
    1. String:String是Redis中最基本的数据类型,可以存储任何数据,包括二进制数据、序列化的数据、JSON化的对象甚至是图片。
    2. List:List是字符串列表,按照插入的顺序排序,元素可以重复,你可以添加一个元素到哦列表的头部或者尾部,底层是一个链表结构。
    3. Set:Set是一个无序不重复的集合。
    4. Hash:Hash是String类型的filed和value的集合,适合用于存储对象。
    5. Zset:Zset和set一样也是String类型元素的集合,且不允许有重复的元素,但不同的是Zset的每个元素都会关联一个分数,分数可以重复,Redis通过分数来为集合汇总的成员进行从小到大的排序。
  • 四种特殊数据类型
    1. bitmap
    2. hyperloglog
    3. geo
    4. stream

26 请你讲讲工厂模式,手写实现工厂模式

  • 得分点 简单工厂、工厂方法、抽象工厂
  • 标准回答 工厂模式(Factory Method Pattern)也叫虚拟构造函数模式或多态性工厂模式,其用意是定义一个创建产品对象的工厂接口,将实际创建性工作推迟到子类中。
    • 工厂模式可以分为简单工厂、工厂方法和抽象工厂模式 简单工厂模式严格来讲并不算是一种设计模式,更多的时候是一种编程习惯。简单工厂的实现思路是,定义一个工厂类,根据传入的参数不同返回不同的实例,被创建的实例具有共同的父类或接口。简单工厂适用于需要创建的对象较少或客户端不关心对象的创建过程的情况。
    • 示例 略
  • 工厂模式:不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。 1
    1. 简单工厂:提供一个统一的工厂类来创造对象,应用场景:需要创建的对象较少,客户端不关心对象的创建过程;
    2. 工厂方法:针对不同的对象提供不同的工厂,应用场景:客户端不需要知道它所创建的对象的类。客户端可以通过子类来指定创建对应的对象。
    3. 抽象工厂模式:可以创建一组对象。

工厂模式:不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。

  • 简单工厂:提供一个统一的工厂类来创造对象,应用场景:需要创建的对象较少,客户端不关心对象的创建过程;
  • 工厂方法:针对不同的对象提供不同的工厂,应用场景:客户端不需要知道它所创建的对象的类。客户端可以通过子类来指定创建对应的对象。
  • 抽象工厂模式:可以创建一组对象。

2023.4.26

27 你知道哪些线程安全的集合?

  • 得分点 Collections、java.util.concurrent (JUC)
  • 标准回答 java.util包下的集合类中,大部分都是非线程安全的,但也有少数的线程安全的集合类,例如Vector、Hashtable,它们都是非常古老的API。虽然它们是线程安全的,但是性能很差,已经不推荐使用了。对于这个包下非线程安全的集合,可以利用Collections工具类,该工具类提供的synchronizedXxx()方法,可以将这些集合类包装成线程安全的集合类。 从JDK 1.5开始,并发包下新增了大量高效的并发的容器,这些容器按照实现机制可以分为三类。
    • 第一类是以降低锁粒度来提高并发性能的容器,它们的类名以Concurrent开头,如ConcurrentHashMap。
    • 第二类是采用写时复制技术实现的并发容器,它们的类名以CopyOnWrite开头,如CopyOnWriteArrayList。
    • 第三类是采用Lock实现的阻塞队列,内部创建两个Condition分别用于生产者和消费者的等待,这些类都实现了BlockingQueue接口,如ArrayBlockingQueue。
  • 加分回答 Collections还提供了如下三类方法来返回一个不可变的集合,这三类方法的参数是原有的集合对象,返回值是该集合的“只读”版本。通过Collections提供的三类方法,可以生成“只读”的Collection或Map。 emptyXxx():返回一个空的不可变的集合对象 singletonXxx():返回一个只包含指定对象的不可变的集合对象 unmodifiableXxx():返回指定集合对象的不可变视图

java.uti包中的集合类大部分都是非线程安全的,例如:ArrayList/LinkedList/HashMap等等,但也有少部分是线程安全的,像是Vector和Hashtable,它们属于很古老的API了,是基于Synchronized实现的,性能很差,在实际的开发中不常用。

一般可以使用collections工具类中的syncheronizedXxx()方法将非线程安全的集合包装成线程安全的类。

在java5之后可以使用concurrent包提供的大量的支持并发访问的集合类,例如ConcurrentHashMap/CopyOnWriteArrayList等

28 请你说说ConcurrentHashMap

  • 得分点 数组+链表+红黑树、锁的粒度
  • 标准回答 在JDK8中,ConcurrentHashMap的底层数据结构与HashMap一样,也是采用“数组+链表+红黑树”的形式。同时,它又采用锁定头节点的方式降低了锁粒度,以较低的性能代价实现了线程安全。底层数据结构的逻辑可以参考HashMap的实现,下面我重点介绍它的线程安全的实现机制。
    1. 初始化数组或头节点时,ConcurrentHashMap并没有加锁,而是CAS的方式进行原子替换(原子操作,基于Unsafe类的原子操作API)。
    2. 插入数据时会进行加锁处理,但锁定的不是整个数组,而是槽中的头节点。所以,ConcurrentHashMap中锁的粒度是槽,而不是整个数组,并发的性能很好。
    3. 扩容时会进行加锁处理,锁定的仍然是头节点。并且,支持多个线程同时对数组扩容,提高并发能力。每个线程需先以CAS操作抢任务,争抢一段连续槽位的数据转移权。抢到任务后,该线程会锁定槽内的头节点,然后将链表或树中的数据迁移到新的数组里。
    4. 查找数据时并不会加锁,所以性能很好。另外,在扩容的过程中,依然可以支持查找操作。如果某个槽还未进行迁移,则直接可以从旧数组里找到数据。如果某个槽已经迁移完毕,但是整个扩容还没结束,则扩容线程会创建一个转发节点存入旧数组,届时查找线程根据转发节点的提示,从新数组中找到目标数据。
  • 加分回答 ConcurrentHashMap实现线程安全的难点在于多线程并发扩容,即当一个线程在插入数据时,若发现数组正在扩容,那么它就会立即参与扩容操作,完成扩容后再插入数据到新数组。在扩容的时候,多个线程共同分担数据迁移任务,每个线程负责的迁移数量是 (数组长度 >>> 3) / CPU核心数。 也就是说,为线程分配的迁移任务,是充分考虑了硬件的处理能力的。多个线程依据硬件的处理能力,平均分摊一部分槽的迁移工作。另外,如果计算出来的迁移数量小于16,则强制将其改为16,这是考虑到目前服务器领域主流的CPU运行速度,每次处理的任务过少,对于CPU的算力也是一种浪费。

ConcurrentHashMap是一个线程安全的集合,它的底层是数组+链表/红黑树构成的。

在1.7的时候采用segment数组+hashEntry的方式实现的,lock加在Segment的上面,在size计算的时候,首先是不加锁的,最多计算三次,前后两次的结果是相同的话那么结果就是准确的,如果不一样的话,那么就加锁,重新计算。

在1.8的时候废弃了这种算法,采用Synchronized+CAS+Node来保证并发安全的进行,使用一个volatile类型的变量baseCount来记录元素的个数,集合每次增加或者删除的时候,basecount就会通过addCount()来对baseCunt产生相应的变化,最后得到元素的个数。 初始值为16,每次扩容都是之前的二倍,不支持null值和null为key


  1. ConcurrentHashMap的底层数据结构与HashMap一样,也是采用“数组+链表+红黑树
  2. 采用锁定头节点的方式降低了锁粒度,以较低的性能代价实现了线程安全。
  3. 实现机制:
    1. 初始化数组或头节点时,ConcurrentHashMap并没有加锁,而是CAS的方式进行原子替换
    2. 插入数据时会进行加锁处理,但锁定的不是整个数组,而是槽中的头节点。所以,ConcurrentHashMap中锁的粒度是槽,而不是整个数组,并发的性能很好。
    3. 扩容时会进行加锁处理,锁定的仍然是头节点。并且,支持多个线程同时对数组扩容,提高并发能力。
    4. 在扩容的过程中,依然可以支持查找操作。

2023.04.30

29 说说缓存穿透、击穿、雪崩的区别

  • 得分点 三种问题的发生原因以及解决方式
  • 标准回答
    1. 缓存穿透:是指客户端查询了根本不存在的数据,使得这个请求直达存储层,导致其负载过大甚至造成宕机。这种情况可能是由于业务层误将缓存和库中的数据删除造成的,当然也不排除有人恶意攻击,专门访问库中不存在的数据导致缓存穿透。 我们可以通过缓存空对象的方式和布隆过滤器两种方式来解决这一问题。缓存空对象是指当存储层未命中后,仍然将空值存入缓存层 ,当客户端再次访问数据时,缓存层直接返回空值。还可以将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值。
    2. 缓存击穿:当一份访问量非常大的热点数据缓存失效的瞬间,大量的请求直达存储层,导致服务崩溃。 缓存击穿可以通过热点数据不设置过期时间来解决,这样就不会出现上述的问题,这是“物理”上的永不过期。或者为每个数据设置逻辑过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存。除了永不过期的方式,我们也可以通过加互斥锁的方式来解决缓存击穿,即对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。
    3. 缓存雪崩:是指当某一时刻缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。 缓存雪崩的解决方式有三种;第一种是在设置过期时间时,附加一个随机数,避免大量的key同时过期。第二种是启用降级和熔断措施,即发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回。第三种是构建高可用的Redis服务,也就是采用哨兵或集群模式,部署多个Redis实例,这样即使个别节点宕机,依然可以保持服务的整体可用。
  1. 缓存穿透:客户端访问不存在的数据,使得请求直达存储层,导致负载过大,直至宕机。原因可能是业务层误删了缓存和库中的数据,或是有人恶意访问不存在的数据。
    • 解决方式:
      1. 存储层未命中后,返回空值存入缓存层,客户端再次访问时,缓存层直接返回空值。
      2. 将数据存入布隆过滤器,访问缓存之前经过滤器拦截,若请求的数据不存在则直接返回空值。
  2. 缓存击穿:一份热点数据,它的访问量非常大,在它缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。
    • 解决方案:
      1. 永不过期:对热点数据不设置过期时间。
      2. 加互斥锁,当一个线程访问该数据时,另一个线程只能等待,这个线程访问之后,缓存中的数据将被重建,届时其他线程就可以从缓存中取值。
  3. 缓存雪崩:大量数据同时过期、或是redis节点故障导致服务不可用,缓存层无法提供服务,所有的请求直达存储层,造成数据库宕机。
    • 解决方案:
      1. 避免数据同时过期,设置随机过期时间。
      2. 启用降级和熔断措施。
      3. 设置热点数据永不过期。
      4. 采用redis集群,一个宕机,另外的还能用

30 Redis如何与数据库保持双写一致性

  • 得分点 四种同步策略及其可能出现的问题,重试机制
  • 标准回答 保证缓存和数据库的双写一致性,共有四种同步策略,即先更新缓存再更新数据库、先更新数据库再更新缓存、先删除缓存再更新数据库、先更新数据库再删除缓存。 先更新缓存的优点是每次数据变化时都能及时地更新缓存,这样不容易出现查询未命中的情况,但这种操作的消耗很大,如果数据需要经过复杂的计算再写入缓存的话,频繁的更新缓存会影响到服务器的性能。如果是写入数据比较频繁的场景,可能会导致频繁的更新缓存却没有业务来读取该数据。 删除缓存的优点是操作简单,无论更新的操作复杂与否,都是直接删除缓存中的数据。这种做法的缺点则是,当删除了缓存之后,下一次查询容易出现未命中的情况,那么这时就需要再次读取数据库。 那么对比而言,删除缓存无疑是更好的选择。 那么我们再来看一下先操作数据库和后操作数据库的区别;先删除缓存再操作数据库的话,如果第二步骤失败可能导致缓存和数据库得到相同的旧数据。先操作数据库但删除缓存失败的话则会导致缓存和数据库得到的结果不一致。出现上述问题的时候,我们一般采用重试机制解决,而为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行。当我们采用重试机制之后由于存在并发,先删除缓存依然可能存在缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致的情况。
  • 所以我们得到结论:先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况,则可以采用重试机制解决问题。

保证双写一致性共有四种方案:

  1. 先更新缓存,在更新数据库,该方式的优点是能够保证数据访问是都会命中redis,降低数据库压力。缺点是每次修改都会操作缓存降低了服务器的性能。
  2. 先更新数据库,再更新缓存:缺点是可能导致数据库和redis中的数据不一致(数据库更新成功,redis更新失败)。
  3. 先删除redis,再更新数据库。缺点:可能导致数据库和redis中的数据都是旧数据(删除redis后,再更新数据时失败了)
  4. 先更新数据库再删除redis。缺点:可能导致数据库和redis的数据不一致(redis删除失败)。出错时使用重试机制异步重新处理。

31 说说你了解的线程同步方式

  • 得分点 synchronized、Lock
  • 标准回答 Java主要通过加锁的方式实现线程同步,而锁有两类,分别是synchronized和Lock。 synchronized可以加在三个不同的位置,对应三种不同的使用方式,这三种方式的区别是锁对象不同: 1. 加在普通方法上,则锁是当前的实例(this)。 2. 加在静态方法上,则锁是当前类的Class对象。 3. 加在代码块上,则需要在关键字后面的小括号里,显式指定一个对象作为锁对象。 不同的锁对象,意味着不同的锁粒度,所以我们不应该无脑地将它加在方法前了事,尽管通常这可以解决问题。而是应该根据要锁定的范围,准确的选择锁对象,从而准确地确定锁的粒度,降低锁带来的性能开销。 synchronized是比较早期的API,在设计之初没有考虑到超时机制、非阻塞形式,以及多个条件变量。若想通过升级的方式让synchronized支持这些相对复杂的功能,则需要大改它的语法结构,不利于兼容旧代码。因此,JDK的开发团队在1.5引入了Lock接口,并通过Lock支持了上述的功能。Lock支持的功能包括:支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量(阻塞队列)。 加分回答 synchronized采用“CAS+Mark Word”实现,为了性能的考虑,并通过锁升级机制降低锁的开销。在并发环境中,synchronized会随着多线程竞争的加剧,按照如下步骤逐步升级:无锁、偏向锁、轻量级锁、重量级锁。 Lock则采用“CAS+volatile”实现,其实现的核心是AQS。AQS是线程同步器,是一个线程同步的基础框架,它基于模板方法模式。在具体的Lock实例中,锁的实现是通过继承AQS来实现的,并且可以根据锁的使用场景,派生出公平锁、不公平锁、读锁、写锁等具体的实现。
  1. Java通过加锁实现线程同步,锁有两类:synchronized和Lock。
  2. synchronized加在三个不同的位置,对应三种不同的使用方式,这三种方式的区别是锁对象不同:
    1. 加在普通方法上,则锁是当前的实例(this)。
    2. 加在静态方法上,锁是当前类的Class对象。
    3. 加在代码块上,则需要在关键字后面的小括号里,显式指定一个对象作为锁对象。
  3. Lock支持的功能包括:支持响应中断、支持超时机制、支持以非阻塞的方式获取锁、支持多个条件变量(阻塞队列)

2023.05.05

32 请你说说innodb和myisam的区别?

  • 得分点 事务、锁、读写性能、存储结构
  • 标准回答 InnoDB是具有事务、回滚和崩溃修复能力的事务安全型引擎,它可以实现行级锁来保证高性能的大量数据中的并发操作;MyISAM是具有默认支持全文索引、压缩功能及较高查询性能的非事务性引擎。具体来说,可以在以下角度上形成对比: 事务:InnoDB支持事务;MyISAM不支持。 数据锁:InnoDB支持行级锁;MyISAM只支持表级锁。 读写性能:InnoDB增删改性能更优;MyISAM查询性能更优。 全文索引:InnoDB不支持(但可通过插件等方式支持);MyISAM默认支持。 外键:InnoDB支持外键;MyISAM不支持。 存储结构:InnoDB在磁盘存储为一个文件;MyISAM在磁盘上存储成三个文件(表定义、数据、索引)。 存储空间:InnoDB需要更多的内存和存储;MyISAM支持支持三种不同的存储格式:静态表(默认)、动态表、压缩表。 移植:InnoDB在数据量小时可通过拷贝数据文件、备份 binlog、mysqldump工具移植,数据量大时比较麻烦;可单独对某个表通过拷贝表文件移植。 崩溃恢复:InnoDB有崩溃恢复机制;MyISAM没有。 默认推荐:InnoDB是MySQL5.5之后的默认引擎。 加分回答 InnoDB中行级锁是怎么实现的? InnoDB行级锁是通过给索引上的索引项加锁来实现的。只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。 当表中锁定其中的某几行时,不同的事务可以使用不同的索引锁定不同的行。另外,不论使用主键索引、唯一索引还是普通索引,InnoDB都会使用行锁来对数据加锁。

InnoDB支持事务,默认的锁是行锁。 MyISAM不支持事务,默认的锁是表锁。 读写性能:InnoDB增删改性能更优;MyISAM查询性能更优。

  1. innodb支持事务,默认支持外键约束,myisam不支持;
  2. Innodb支持行级锁(通过在索引上加锁实现);myisam不支持,支持表级锁;
  3. Innodb的增删改性能更优;Myisam的查询性能更优;
  4. Innodb不支持全文索引,myisam默认支持;
    • innodb使用聚簇索引和非聚簇索引,而myisam只使用非聚簇索引,innodb不支持全文索引(可通过插件使其支持),myisam支持全文索引。
  5. 前者增删改性能更优,后者查询性能更优
  6. innodb最少需要一个文件便可以存储表信息。myisam至少需要三个文件(分别存储表结构,数据,索引)
  7. Mysql5.5之后默认引擎是InnoDB, 行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,innoDB才使用行锁,否则,使用表锁

33 String、StringBuffer、Stringbuilder有什么区别

  • 得分点 字符串是否可变,StringBuffer、StringBuilder线程安全问题
  • 标准回答 Java中提供了String,StringBuffer两个类来封装字符串,并且提供了一系列方法来操作字符串对象。 String是一个不可变类,也就是说,一个String对象创建之后,直到这个对象销毁为止,对象中的字符序列都不能被改变。 StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer对象被创建之后,我们可以通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()、等方法来改变这个字符串对象的字符序列。当通过StringBuffer得到期待中字符序列的字符串时,就可以通过toString()方法将其转换为String对象。 StringBuilder类是JDK1.5中新增的类,他也代表了字符串对象。和StringBuffer类相比,它们有共同的父类AbstractStringBuilder,二者无论是构造器还是方法都基本相同,不同的一点是,StringBuilder没有考虑线程安全问题,也正因如此,StringBuilder比StringBuffer性能略高。因此,如果是在单线程下操作大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先使用StringBuilder类。
  • StringBuilder和StringBuffer非常类似,均代表可变的字符序列,而且方法也一样
  • String:不可变字符序列,效率低,但是复用率高。
    • String类型对象创建出来后归常量池管,可以随时从常量池调用同一个String对象。StringBuffer和StringBuider在创建对象后一般要转化成String对象才调用。
  • StringBuffer:可变字符序列、效率较高(增删)、线程安全
    • String是final修饰不可被继承不可变,即时修改也是新建一个变量,String通过不变的特性实现了线程之间的可见性,一定条件下可以保证线程安全。StringBuffer和StringBuilder对象是可变的。StringBuffer实现了Synchronize封装使得它线程安全,而StringBuilder是线程不安全的。所以从运行速度来说,StringBuilder>StringBuffer>String
  • StringBuilder:可变字符序列、效率最高、线程不安全

2023.05.18

34 请你说说HashMap底层原理

  • 得分点 数据结构、put()流程、扩容机制
  • 标准回答
    • 数据结构 在JDK8中,HashMap底层是采用“数组+链表+红黑树”来实现的。 HashMap是基于哈希算法来确定元素的位置(槽)的,当我们向集合中存入数据时,它会计算传入的Key的哈希值,并利用哈希值取余来确定槽的位置。如果元素发生碰撞,也就是这个槽已经存在其他的元素了,则HashMap会通过链表将这些元素组织起来。如果碰撞进一步加剧,某个链表的长度达到了8,则HashMap会创建红黑树来代替这个链表,从而提高对这个槽中数据的查找的速度。 HashMap中,数组的默认初始容量为16,这个容量会以2的指数进行扩容。具体来说,当数组中的元素达到一定比例的时候HashMap就会扩容,这个比例叫做负载因子,默认为0.75。自动扩容机制,是为了保证HashMap初始时不必占据太大的内存,而在使用期间又可以实时保证有足够大的空间。采用2的指数进行扩容,是为了利用位运算,提高扩容运算的效率。
    • put()流程 put()方法的执行过程中,主要包含四个步骤: 1. 判断数组,若发现数组为空,则进行首次扩容。 2. 判断头节点,若发现头节点为空,则新建链表节点,存入数组。 3. 判断头节点,若发现头节点非空,则将元素插入槽内。 4. 插入元素后,判断元素的个数,若发现超过阈值则再次扩容。 其中,第3步又可以细分为如下三个小步骤: 1. 若元素的key与头节点一致,则直接覆盖头节点。 2. 若元素为树型节点,则将元素追加到树中。 3. 若元素为链表节点,则将元素追加到链表中。追加后,需要判断链表长度以决定是否转为红黑树。若链表长度达到8、数组容量未达到64,则扩容。若链表长度达到8、数组容量达到64,则转为红黑树。
    • 扩容机制 向HashMap中添加数据时,有三个条件会触发它的扩容行为: 1. 如果数组为空,则进行首次扩容。 2. 将元素接入链表后,如果链表长度达到8,并且数组长度小于64,则扩容。 3. 添加后,如果数组中元素超过阈值,即比例超出限制(默认为0.75),则扩容。 并且,每次扩容时都是将容量翻倍,即创建一个2倍大的新数组,然后再将旧数组中的数组迁移到新数组里。由于HashMap中数组的容量为2^N,所以可以用位移运算计算新容量,效率很高。
  • 加分回答 HashMap是非线程安全的,在多线程环境下,多个线程同时触发HashMap的改变时,有可能会发生冲突。所以,在多线程环境下不建议使用HashMap,可以考虑使用Collections将HashMap转为线程安全的HashMap,更为推荐的方式则是使用ConcurrentHashMap。
  1. 数据结构:在1.8之前,HashMap的底层是数组加链表,JDK8后,HashMap底层是采用“数组+链表+红黑树”来实现的;
    • HashMap是基于哈希算法来确定元素的位置,当我们向集合中存入数据时,它会计算传入的Key的哈希值,并利用哈希值取余来确定位置。若碰撞进一步加剧会创建红黑树来代替这个链表,从而提高对数据的查找速度
  2. 它的put流程是:基于哈希算法来确定元素位置,当我们向集合存入数据时,他会计算传入的key的哈希值,并利用哈希值取绝对值再根据集合长度取余来确定元素的位置,如果这个位置已经存在其他元素了,就会发生哈希碰撞,则hashmap就会通过链表将这些元素组织起来,如果链表的长度达到8时,就会转化为红黑树,从而提高查询速度。
    1. 判断数组,若发现数组为空,则进行首次扩容。
    2. 判断头节点,若发现头节点为空,则新建链表节点,存入数组。
    3. 判断头节点,若发现头节点非空,则将元素插入槽内。
    4. 插入元素后,判断元素的个数,若发现超过阈值则再次扩容。
  3. 扩容机制:HashMap中数组的默认初始容量为16,当达到默认负载因子0.75时,会以2的指数倍进行扩容。 Hashmap时非线程安全的,在多线程环境下回产生循环死链,因此在多线程环境下建议使用ConcurrentHashMap。
    • 触发扩容行为三个条件:
      1. 如果数组为空,则进行首次扩容。
      2. 将元素接入链表后,如果链表长度达到8,并且数组长度小于64,则扩容。
      3. 添加后,如果数组中元素超过阈值,即比例超出限制(默认为0.75),则扩容。 并且,每次扩容时都是将容量翻倍,即创建一个2倍大的新数组,然后再将旧数组中的数组迁移到新数组里。

35 说说你了解的JVM内存模型

  • 得分点 类加载子系统、执行引擎、运行时数据区
  • 标准回答 JVM由三部分组成:类加载子系统、执行引擎、运行时数据区。
    1. 类加载子系统,可以根据指定的全限定名来载入类或接口。
    2. 执行引擎,负责执行那些包含在被载入类的方法中的指令。
    3. 当程序运行时,JVM需要内存来存储许多内容,例如:字节码、对象、参数、返回值、局部变量、运算的中间结果,等等,JVM会把这些东西都存储到运行时数据区中,以便于管理。而运行时数据区又可以分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。
  • 加分回答 运行时数据区是开发者重点要关注的部分,因为程序的运行与它密不可分,很多错误的排查也需要基于对运行时数据区的理解。在运行时数据区所包含的几块内存空间中,方法区和堆是线程之间共享的内存区域,而虚拟机栈、本地方法栈、程序计数器则是线程私有的区域,就是说每个线程都有自己的这个区域。
  1. 类加载子系统:根据全限定名来载入类;
  2. 执行引擎:执行那些被载入类的方法的指令;
  3. 运行时数据区:在程序运行时,存储程序的内容,例如:字节码、对象、参数、返回值等。运行时数据区又分为方法区、堆、栈、程序计数器

36 说说JVM的垃圾回收机制

  • 得分点 新生代收集、老年代收集、混合收集、整堆收集
  • 标准回答 当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则。而分代收集理论,建立在如下三个分代假说之上,即弱分代假说、强分代假说、跨代引用假说。
    • 依据分代假说理论,垃圾回收可以分为如下几类: 1. 新生代收集:目标为新生代的垃圾收集。 2. 老年代收集:目标为老年代的垃圾收集,目前只有CMS收集器会有这种行为。 3. 混合收集:目标为整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。 4. 整堆收集:目标为整个堆和方法区的垃圾收集。 加分回答 HotSpot虚拟机内置了很多垃圾收集器,其中针对新生代的垃圾收集器有Serial、ParNew、Parallel Scavenge,针对老年代的垃圾收集器有CMS、Serial Old、Parallel Old。此外,HotSpot还内置了面向整堆的G1收集器。
    • 在上述收集器中,常见的组合方式有: 1. Serial + Serial Old,是客户端模式下常用的收集器。 2. ParNew + CMS,是服务端模式下常用的收集器。 3. Parallel Scavenge + Parallel Old,适用于后台运算而不需要太多交互的分析任务。

JVM的垃圾回收机制是遵循分代收集理论进行设计的,主要分为四种收集方式:1.新生代收集,目标为新生代的垃圾收集。2.老年代收集:目标为老年代的垃圾收集,目前只有CMS收集器会有这种行为。3.混合收集:目标为整个新生代及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。4.整堆收集:目标为整个方法区和堆的垃圾收集。

常见的垃圾回收算法包括:标记清除算法:缺点:内存碎片化,优点:速度快。标记复制算法:缺点占用内存大,优点:内存连续。标记整理算法:优点:内存连续,缺点:整理效率低。

待梳理完成题目

34 说说JVM的垃圾回收机制
35 说说类加载机制
36 epoll原理
37 请你说一下抽象类和接口的区别
38 请你说说==与equals()的区别
39 说说synchronize的用法及原理
40 说说你对AQS的理解
41 Java哪些地方使用了CAS
42 说说JVM的垃圾回收算法
43 请你说说Redis数据类型中的zset,它和set有…
44 说说static修饰符的用法
45 说说线程的状态
46 说说你对ThreadLocal的理解
47 说说Spring Boot常用的注解
48 说说Bean的生命周期
49 synchronized和Lock有什么区别
50 说说volatile的用法及原理
说说Redis的单线程架构
如何实现Redis高可用
请你说一下final关键字
请你说说重载和重写的区别,构造方法能不能…
请说说你对Java集合的了解
请你说说I0多路复用
请你说说索引怎么实现的B+树,为什么选这…
请你讲一下Java 8的新特性
请你说说泛型、泛型擦除
说说你了解的线程通信方式
请你说说JUC
请你说说HashMap和Hashtable的区别
HashMap是线程安全的吗?如果不是该如…
请你说说Java的四种引用方式
请你讲下G1垃圾回收器
请你说说内存溢出
请你说说内存泄漏
请你说说数据库引警有哪些,各自有什么区别
简单介绍Spring
介绍一下MyBatis的缓存机制
请你说说String类,以及new和
请你说说hashCode0和equals0的区别,为…
说说线程的创建方式
说说你对ArrayList的理解
请你说说BIO、NIO、O
说说你对Spring Boot的理解以及它和Spri…
说说Spring Boot的自动装配
说说@Autowired和@Resource注解的区别
说说Redis的主从同步机制
说说Redis的缓存淘汰策略
说说垃圾收集器
请你说说Java的特点和优点为什么要选择Ja…
介绍一下包装类的自动拆装箱与自动装箱
说说wt0和sleep0的区别
说说你对线程池的理解
简单说下你对JVM的了解
说说Java运行时数据区
请你讲下CMS垃圾回收器
说说JVM的双亲委派模型
请你说说数据库索的底层数据结构
说说Spring Boot的启动流程
介绍一下SpringMVC的执行流程
在MyBatis中$和#有什么区别
请你说说Java基本数据类型和引用类型
请你说说Java的异常处理机制
说说你对面向对象的理解
请介绍一下访问修饰符
说说Java中常用的锁及原理
请你说说List与Set的区别

请你讲一下Java NIO
说说GC的可达性分析
说说类的实例化过程
请你讲讲B树和B+树
MySQL主从同步是如何实现的?
请你介绍一下数据库的ACID
请你说说数据库的索引是什么结构,为什么不
请你说说InnoDB的MVCC
说说Soring Boot的起步依赖
说说Spring事务管理
说说Bean的作用域,以及默认的作用域
说说BeanFactory和FactoryBean的区别
说说你对Redis的了解

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yinying293

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值