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+树有如下的优点:
- 叉数多,路径更短
- 磁盘读写代价低,非叶子节点只是存储指针,叶子节点存储数据,并且查询效率稳定
- 方便进行扫库和区间查询,叶子节点是一个双向链表
和B树区别:
- 在B树中,非叶子节点和叶子节点都会存放数据,而B+树的 所有的数据都会出现在叶子节点,在查询的时候,B+树查找效率更加稳定
- 在进行范围查询的时候,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
,结果决定数据存储在哪个分表中 - 优点:数据分布相对均匀
- 缺点:当分表数量发生变化时,数据迁移成本较高
- 例如,根据用户 ID 对分表数量进行取模运算:
-
范围划分
- 按照某个字段的值的范围,如用户年龄 0 - 20 岁存储在表 1,21 - 40 岁存储在表 2 等
- 优点:易于理解和扩展
- 缺点:可能导致数据分布不均匀
-
时间范围
- 对于与时间相关的数据,如订单数据,可以按照创建时间的月份、季度或年份进行分表
- 例如,2023 年的订单存储在表 1,2024 年的订单存储在表 2
为啥要使用Redis,Redis解决了什么问题?
- redis可以作为缓存来减轻数据库的压力
- redis基于内存具有很高的读写性能
- redis在分布式系统中,可以用分布式锁保证线程安全
- redis也可以通过list左插入右读取来实现一个简易的消息队列
Redis如何进行持久化?
AOF和RDB两种持久化的方式。
- AOF指的是追加文件的形式。当每次执行插入修改redis数据的时候,都会将对应的redis语句存储在AOF文件中,当要恢复数据的时候,通过执行AOF中的命令来进行数据的恢复
- 优点:数据不易丢失
- 缺点:执行速度过慢,当指令过多的时候会出现文件过大
- RDB指的是快照文件的形式。它会按照一定的频率,以二进制文件的形式来将数据进行一个同步,但需要数据恢复,可以直接通过RDB文件进行恢复
- 优点:速度快,文件大小合适
- 缺点:容易丢失数据
如何解决AOF文件过大的问题?
可以执行BGREWRITEAOF命令来实现AOF文件的重写,这样对于一些冗余的命令会进行一定的删除合并