【每日面经】快手面经

ConcurrentHashMap和HashMap的区别?使用场景?

  • 线程安全性
    • concurrentHashMap是线程安全的,HashMap不是线程安全的
  • 锁机制
    • ConcurrentHashMap采用的是分段锁(Sagment)机制,降低所得粒度提高了并发性能

CurrrentHashMap的底层实现

底层数据结构:

  • 在jdk1.7中底层采用的是分段数组+链表来实现的
  • jdk1.8中底层采用的和HashMap结构一样,数组+链表/红黑树

CurrentHashMap如何保证线程安全:

  • jdk1.7首先计算对应key的哈希值,然后找到对应的分段数组,紧接着,去获取ReentrantLock锁,然后通过hash值定位hashEntrry数组下标。这样缺点就是每次都只有一个线程执行,导致性能过低
  •  jdk1.8中采用的是和hashmap一致的底层结构,但是是通过cas+synchronized来保证并发的情况的线程安全问题。其中CAS控制数组节点的添加,而synchronized控制的是链表和红黑树的节点添加,在操作的时候会将首节点用锁锁住,这样就可以保证线程安全了

线程编程的时候如何保证线程安全?

线程安全问题主要就是资源贡献,导致的多个线程出现竞争。首先解决方案毋庸置疑的就是使用锁机制来保证线程安全,其次就是线程本地存储通过ThreadLocal这个类来实现每个线程都有独立的一个数据副本,这样就看可以避免线程安全问题,还有就是使用一些线程安全的集合,和原子性的对象来保证线程的安全

分布式场景下如何保证线程安全?

分布式场景之下不能通过我们所说的synchronized和reentrantLock来实现的锁机制保证线程安全。因为之前我们所说的锁他都是在本地jvm实现的锁,一个JVM只可以持有一把锁,但是分布式的场景之下一个请求可能是多个服务发过来的(例如:8001端口,8002端口....都发来了请求),这样的话每一个服务都是有自己jvm的,就会出现多个服务都持有锁的情况,这样普通的锁是不能保证线程安全的问题的

 所以在分布式的环境下,可以去尝试使用redis实现的分布式锁。具体就是通过setnx来对具体的键进行加锁。

CAS会出现什么问题?ABA如何解决?

可以看一下我之前的文章

线程池的使用场景?解决了什么问题?为什么用线程池?

线程池主要是在创建线程的场景之下进行创建线程的,对比其他的创建线程的方式有很多优点:

  • 资源的利用率:避免了频繁的创建和销毁线程带来的系统开销
  • 对线程的约束:可以控制线程的数量、存活时间以及通过线程工厂进行一下属性的配置(名称、优先级和是否为守护线程)
  • 响应性能提升:使用线程池可以更快地响应新的任务请求。因为线程已经创建并处于就绪状态,新任务可以立即被分配给空闲的线程执行

线程池的使用场景包括:

  • 短时间内需要处理大量任务的场景

    • 例如 Web 服务器处理大量的并发请求
  • 执行周期性任务

    • 如定时备份数据、定时发送邮件等

线程池解决的问题:

  • 资源管理优化

    • 避免频繁地创建和销毁线程,减少系统资源的消耗。创建和销毁线程是相对耗时和消耗资源的操作,如果每个任务都创建新线程,可能导致系统性能下降
  • 控制并发度

    • 可以限制同时运行的线程数量,防止过多线程并发执行导致系统资源耗尽或出现性能瓶颈
  • 提高响应性

    • 任务可以快速放入线程池等待执行,无需等待线程创建,从而提高系统的响应速度

线程池的拒绝策略?

线程池的拒绝策略主要是为了在任务队列已经满的时候,并且当前线程数量大于最大线程数的时候,会执行拒绝策略。

AbortPolicy:默认的拒绝策略,直接抛出异常

CallRunsPolicy:将任务给调用者来执行

DiscardPolicy:直接丢弃任务

DiscardOldestPolicy:丢弃任务队列中最早的任务,将当前任务加入进去

JVM内存模型简单描述一下?模型简单描述一下?

程序计数器:每一个线程都具有程序计数器,作用是在并发的环境下,线程是交替执行的,线程A可能执行一半,开始执行线程B,当再次执行线程A的时候会按照上次执行的位置来执行,这个就是程序计数器的作用,指向当前线程执行的行号

虚拟机栈:每一个线程在创建的时候都会创建一个虚拟机栈,用于存储局部变量和方法的栈帧。每一次调用一个方法的时候,都会存储一个方法的栈帧。当方法执行完成之后自动将栈帧释放

本地方法栈:本地方法栈和虚拟机栈类似,记录本地方法调用的时候产生的栈帧

:用于存储对象实例数组,并且内部定义了新生代老年代,1.8之前还有一个永久代,在1.8之后移到本地内存中改名为元空间

本地内存:内部右元空间直接内存元空间用于存储对象的结构,并且内部有运行时常量池,运行时常量池会根据常量池(存储类的一些信息)中的信息,将其的符号引用替换为直接引用。直接内存主要是用于执行NIO(如文件传输、网络数据传输)的操作

JVM双亲委派机制描述一下?

双亲委派机制是类加载器的一个加载方式。类加载器主要是分为几种:

  • 启动类加载器:主要是加载Java_Home/jre/lib目录下的类,例如Java本身提供的一些类
  • 扩展类加载器:主要是加载Java_Home/jre/lib/ext目录下的类,例如我们引入的jar包下的类
  • 应用类加载器:主要是加载自己classPath下的类,例如我们自己定义的类
  • 自定义类加载器:自己定义类加载的规则

而双亲委派模型指的就是一个类加载器在收到加载的请求的时候,优先从父类的类加载器进行加载,如果父类具有父类那就继续向上找父类,如果父类可以加载那么直接返回,如果加载不了,才会委托给下一级进行加载

双亲委派模型的作用就在于防止类的重复加载防止核心API被修改

JVM调优有哪些参数?

对于JVM调参主要就是调整年轻代、老年代、元空间的内存空间大小以及垃圾回收器的类型。

  • 堆空间大小
  • 虚拟机栈的设置
  • 年轻代eden区和两个Survivor的大小比例
  • 年轻代晋升老年代的阈值
  • 垃圾回收器的类型

如果GC时间比较长,一般怎么排查?

  • 查看 GC 日志

    • 启用详细的 GC 日志记录,通过分析日志中的时间、回收的区域、回收前后的内存使用情况等信息,了解 GC 的具体行为
  • 监控工具

    • 使用 JConsole、VisualVM 等工具来实时监控 JVM 的运行状态,包括内存使用、线程情况、GC 活动等
  • 垃圾回收器选择

    • 确认当前使用的垃圾回收器是否适合应用的特点和负载,如果不合适,可以尝试切换其他类型的垃圾回收器

数据库索引一般是用什么数据结构?和其它数据结构有什么区别?

数据库索引在Innodb引擎下是使用的B+数,B+数是一个多叉的一个树,它的非叶子节点只存储索引信息,叶子节点存储数据,并且叶子节点是通过链表进行关联的。B+树有如下的优点:

  1. 叉数多,路径更短
  2. 磁盘读写代价低,非叶子节点只是存储指针,叶子节点存储数据,并且查询效率稳定
  3. 方便进行扫库和区间查询,叶子节点是一个双向链表

和B树区别:

  1. 在B树中,非叶子节点和叶子节点都会存放数据,而B+树的 所有的数据都会出现在叶子节点,在查询的时候,B+树查找效率更加稳定
  2. 在进行范围查询的时候,B+树效率更高,因为B+树都在叶子节点存 储,并且叶子节点是一个双向链表

数据库事务是什么?为什么要用事务?

事务就是一组操作的集合,这些操作要么全部执行,要么全部不执行。要知道事务的作用就必须要提及一下事务的特点ACID(原子性、一致性、隔离性、持久性)拿转账来举例子:

  • 原子性:A给B转账200,A扣除200,B增加200,这个过程要门都成功,要么都失败
  • 一致性:在转账的过程中,A如果少200,那么B一定增加200
  • 隔离性:A给B转账,不会受到C的影响
  • 持久性:事务提交之后,这个数据该百年是持久生效的

就是因为这些特征所以要使用事务

事务的隔离级别有了解吗?

由于并发事务会造成一系列的问题:脏读、不可重复读、幻读

  • 脏读:事务A在修改数据的时候还没有提交被事务B读取了这个数据,之后事务A进行了回滚,导致事务B读取到脏数据
  • 不可重复读:事务A在读取一个数据的时候,事务B对这个数据进行了修改,导致事务A再次读取数据的时候发现两次读取数据的结果不一致
  • 幻读:事务A在读取一些数据的时候发现一些数据不存在,这个时候事务B插入了一些数据,事务A再次查询发现查询到了一些不存在的数据

可以通过不同的隔离级别来解决这个问题:读未提交(解决不了任何问题),读已提交(解决脏读)、可重复读(解决不可重复读、脏读)、串行化(解决所有问题)

在innodb默认的是使用的可重复读的隔离级别。不同的隔离级别是通过MVCC机制和锁来实现的。其中MVCC机制指的是维护了不同版本的数据,根据内部的规则和当前事务id来决定,当前事务读取的是哪个版本的数据。它的内部实现主要是有几个部分:

  • 隐藏字段:如主键、事务id、回滚指针(用于指向上一个版本的数据)
  • undo log版本链:undo log主要是用来记录回滚日志的,存储老版本的数据,在内部会将老版本的数据根据回滚指针进行关联,形成一个链表
  • ReadView读视图:readView是用来决定选择哪个版本数据的关键,在内部定义了一些规则和当前的一些事务id来判断读取哪一个版本数据。不同的隔离级别生成的快照读是不一样的,最终的访问结果也是不一样的。在RC的隔离级别之下,每一次执行快照读都会生成一个ReadView,让如果是RR的隔离级别,那么只有在第一次生成ReadView,之后进行复用

MySQL和Java里面有哪些锁机制?

Mysql中的锁:

  • 共享锁:在事务进行读取数据的时候,其他事务不能进行数据的修改
  • 排他锁:在事务进行修改数据的时候,其他事务不能进行读取数据

Java中的锁:

  • 乐观锁:会假设不会发生冲突,在更新数据的时候会根据新值和旧值的比对或者版本号的比对,来判断是否有冲突,没有则更新,否则重试。例如cas
  • 悲观锁:总是假设最坏的情况,每次操作数据时都先获取锁。例如 synchronized 关键字就是一种悲观锁
  • 自旋锁:当一个线程获取锁失败时,它会不断尝试获取,而不是进入阻塞状态
  • 读写锁:分为读锁和写锁,读锁可以被多个线程同时持有,而写锁是排他的。ReentrantReadWriteLock 类实现了读写锁
  • 公平锁:公平锁按照请求锁的顺序来分配锁
  • 非公平锁:非公平锁不会按照请求锁的顺序来分配锁

对于数据库容量有限,如何存储用户的数据?有什么优化方式?

主要是可以进行分库分表,其次就是通过redis来进行存储一定的数据

分库分表是怎么做的?

分库分表主要是由垂直分库、水平分库、垂直分表、水平分表

  • 垂直分库: 根据业务的不同讲不同的表拆分到不同的数据库中
  • 水平分库:将数据按照一定的规则分布到多个数据库中
  • 垂直分表:将一个表中的不常用的或者字段比较大的拆分到另一张表中
  • 水平分表:将一张表的数据按照规则拆分到的多张表中

其中所说的规则用以几种举例:

  • 哈希取模

    • 例如,根据用户 ID 对分表数量进行取模运算:hash(userId) % tableCount ,结果决定数据存储在哪个分表中
    • 优点:数据分布相对均匀
    • 缺点:当分表数量发生变化时,数据迁移成本较高
  • 范围划分

    • 按照某个字段的值的范围,如用户年龄 0 - 20 岁存储在表 1,21 - 40 岁存储在表 2 等
    • 优点:易于理解和扩展
    • 缺点:可能导致数据分布不均匀
  • 时间范围

    • 对于与时间相关的数据,如订单数据,可以按照创建时间的月份、季度或年份进行分表
    • 例如,2023 年的订单存储在表 1,2024 年的订单存储在表 2

为啥要使用Redis,Redis解决了什么问题?

  1. redis可以作为缓存来减轻数据库的压力
  2. redis基于内存具有很高的读写性能
  3. redis在分布式系统中,可以用分布式锁保证线程安全
  4. redis也可以通过list左插入右读取来实现一个简易的消息队列

Redis如何进行持久化?

AOF和RDB两种持久化的方式。

  • AOF指的是追加文件的形式。当每次执行插入修改redis数据的时候,都会将对应的redis语句存储在AOF文件中,当要恢复数据的时候,通过执行AOF中的命令来进行数据的恢复
    • 优点:数据不易丢失
    • 缺点:执行速度过慢,当指令过多的时候会出现文件过大
  • RDB指的是快照文件的形式。它会按照一定的频率,以二进制文件的形式来将数据进行一个同步,但需要数据恢复,可以直接通过RDB文件进行恢复
    • 优点:速度快,文件大小合适
    • 缺点:容易丢失数据

如何解决AOF文件过大的问题?

可以执行BGREWRITEAOF命令来实现AOF文件的重写,这样对于一些冗余的命令会进行一定的删除合并

  • 10
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

lose_rose777

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

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

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

打赏作者

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

抵扣说明:

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

余额充值