LOADING

加载过慢请开启缓存 浏览器默认开启

2024中通后端一面(一)

自我介绍

项目介绍

Spring boot怎么启动的

  1. 构造一个SpringApplication对象,完成初始化工作。

    1. 创建并初始化ApplicationInitializers。设置到initializers属性中。
    2. 创建并初始化ApplicationListeners。设置到listeners属性中。
  2. 调用run方法,启动Spring boot应用。

    1. 创建并初始化计时器StopWatch,用来记录应用启动时间。
    2. 创建并初始化监听器SpringApplicationRunListeners,用来监听应用启动过程中的事件。
    3. 创建并初始化ApplicationArguments,获取run方法传递的args参数。
    4. 创建并初始化环境配置ConfigurableEnvironment,用来获取应用的配置信息,封装main方法参数,写入到Environment中。并发布ApplicationEnvironmentPreparedEvent事件。
    5. 创建应用程序上下文ApplicationContext,根据配置信息决定创建哪种类型的上下文,比如AnnotationConfigApplicationContext,AnnotationConfigServletWebServerApplicationContext,GenericApplicationContext,GenericWebApplicationContext等。

@Transactional失效的原因

@Transactional注解:声明式事务管理注解,用于表示需要进行事务管理的方法或类,保证方法或类中的操作在同一个事务中进行,实现对应方法的原子性。失效的原因:

  1. 事务方法必须是public的,如果是private的,事务不会生效。
  2. @Transactional注解属性propagation设置错误,导致事务不会生效。如配置TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
  3. Transactional注解属性rollbackFor设置错误,导致事务不会生效。如配置Exception.class:只有抛出Exception异常时,事务才会回滚。配置RuntimeException.class:只有抛出RuntimeException异常时,事务才会回滚。
  4. 同一个类中方法调用,事务不会生效。因为@Transactional注解是通过AOP实现的,AOP是基于动态代理实现的,同一个类中方法调用,是不会触发动态代理的,所以事务不会生效。
  5. 抛出的异常被catch住,事务不会生效。因为@Transactional注解是通过AOP实现的,AOP是基于动态代理实现的,抛出异常时,会触发动态代理,从而触发事务回滚,但是如果异常被catch住了,就不会触发动态代理,所以事务不会生效。
  6. 使用的数据库不支持事务,事务不会生效。如MyISAM不支持事务,InnoDB支持事务。

MySQL联合索引

最左匹配原则:联合索引的最左边的索引列被用于查询,如果最左边的索引列没有被用于查询,则无法使用联合索引。如:联合索引为(a,b,c),则查询条件为a=1 and b=2 and c=3时,可以使用联合索引,查询条件为a=1 and c=3时,无法使用联合索引。

hashmap死循环问题,如何定位元素的

通过jps+jstack命令定位死循环问题,jps命令用于查看java进程,jstack命令用于查看java进程的线程信息。通过jps命令查看java进程的进程号,然后通过jstack命令查看java进程的线程信息,找到死循环的线程,然后定位死循环的代码。

ConcurrentHashMap的实现原理,1.7和1.8的区别,为何1.7要用分段锁,1.8为何要用CAS

实现原理:本质上是HashMap,只是保证线程安全的hashMap,数组+链表+红黑树
1.7版本中保证线程安全的方式是分段锁,1.8版本中保证线程安全的方式是CAS+Synchronized,1.8版本中使用CAS+Synchronized保证线程安全的效率比1.7版本中使用分段锁保证线程安全的效率要高。
1.7中使用分段锁的原因是:因为1.7中使用的是数组+链表的数据结构,当多个线程同时对一个链表进行操作时,会导致链表的数据结构被破坏,从而导致数据错误。所以1.7中使用分段锁,将数组分成多个段,每个段都有一个锁,当多个线程同时对一个链表进行操作时,只需要对链表所在的段加锁,就可以保证线程安全。但是如果HashEntry中数据过多,遍历时间复杂度为O(n),效率比较低。
1.8使用CSA+Sync的原因是:因为1.8中使用的是数组+链表+红黑树的数据结构,主要还是由于在1.8中sync锁的优化,sync锁的开销大大降低,同时1.8中使用红黑树,遍历时间复杂度为O(logn),效率比较高。

Redis如何保证原子性的

单线程

Redis分布式锁的实现

通过setnx命令实现。其中setnx命令是Redis中的原子操作,可以保证在多个客户端的并发操作中,只有一个客户端能够成功地向Redis中设置键值对。通过setnx命令实现分布式锁的步骤如下:

  1. 获取锁,通过setnx命令向Redis中设置键值对,如果设置成功,则获取锁成功,如果设置失败,则获取锁失败。
  2. 释放锁:通过del命令删除Redis中的键值对。
  3. 设置锁的过期时间:通过expire命令设置Redis中的键值对的过期时间,防止获取锁的客户端在释放锁之前宕机,导致锁无法释放。
  4. 防止误删除其他客户端的锁设置唯一标识ID

Spring AOP

AOP是基于动态代理实现的,动态代理有两种实现方式:JDK动态代理和CGLIB动态代理。JDK动态代理是基于接口实现的,CGLIB动态代理是基于继承实现的。Spring AOP默认使用JDK动态代理,如果目标类没有实现接口,则使用CGLIB动态代理。Spring AOP的实现原理如下:

  1. 创建代理对象:通过ProxyFactoryBean创建代理对象,ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean接口是Spring中的一个工厂类,用于创建对象。ProxyFactoryBean创建代理对象的过程如下:
// 1. 创建代理对象
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
// 2. 设置目标对象
proxyFactoryBean.setTarget(target);
// 3. 设置代理对象
proxyFactoryBean.setProxyTargetClass(true);
// 4. 设置代理对象的拦截器
proxyFactoryBean.setInterceptorNames("myInterceptor");
// 5. 获取代理对象
MyService myService = (MyService) proxyFactoryBean.getObject();

AQS和CLH队列锁的底层实现

AQS:全称为AbstractQueuedSynchronizer,是一个用于构建锁和同步器的框架,底层数据结构就是双项链表

AQS核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

CLH队列是一个虚拟的双向队列也就是不存在队列实例,仅存在结点之间的关联关系,AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

算法打印树状目录结构

import os
def generate_tree(path, level=0):
    tree=" "
    folder_name = os.path.basename(path)
    if level == 0:
        tree += folder_name + "\n"
    else:
        tree += f"{' '*level}|--{folder_name}/\n"
    items = os.listdir(path)
    dirs = [item for item in items if os.path.isdir(os.path.join(path, item))]
    files = [item for item in items if os.path.isfile(os.path.join(path, item))]
    for item in sorted(dirs) + sorted(files):
        if os.path.isdir(os.path.join(path, item)):
            tree += generate_tree(os.path.join(path, item), level+1)
        else:
            tree += f"{' '*level}|--{item}\n"
    return tree

folder_path = os.getcwd()
tree = generate_tree(folder_path)

with open("tree.txt", "w", encoding="utf-8") as f:
    f.write(tree)

算法:最长回文串

#include<bits/stdc++.h>
using namespace std;
class Solution{
    public:
        string longestPalindrome(string s){
            int n = s.size();
            vector<vector<int>> dp(n, vector<int>(n));
            string ans;
            for(int l = 0; l < n; ++l){
                for(int i = 0; i + l < n; ++i){
                    int j = i + l;
                    if(l == 0){
                        dp[i][j] = 1;
                    }else if(l == 1){
                        dp[i][j] = (s[i] == s[j]);
                    }else{
                        dp[i][j] = (s[i] == s[j] && dp[i+1][j-1]);
                    }
                    if(dp[i][j] && l + 1 > ans.size()){
                        ans = s.substr(i, l + 1);
                    }
                }
            }
            return ans;
        }
};
int main(){
    string s;
    cin >> s;
    Solution solution;
    string ans = solution.longestPalindrome(s);
    cout << ans << endl;
    return 0;
}

布隆过滤器的实现原理

布隆过滤器是一个bit向量或者说是一个bit数组,初始化的时候全部都被置为0,然后添加数字的时候,使用k个hash函数计算,然后将元素得到的hash映射到K个位置上,将对应的K个位置置为1,查询的时候,使用k个hash函数计算,然后将元素得到的hash映射到K个位置上,如果K个位置上有一个位置为0,则说明该元素一定不存在,如果K个位置上都为1,则说明该元素可能存在。这是一个概率模型,可能存在误判,但是不会漏判。存储元素越多,误判率越高。

任务调度策略

  1. FCFS先来先服务调度算法:按照任务到达的先后顺序进行调度,不考虑任务的执行时间,不考虑任务的优先级。
  2. SJF短作业优先调度算法:按照任务的执行时间进行调度,执行时间越短的任务优先级越高。
  3. HRN高响应比优先调度算法:按照任务的响应比进行调度,响应比越高的任务优先级越高。响应比=(等待时间+执行时间)/执行时间。
  4. 时间片轮转调度算法:按照任务到达的先后顺序进行调度,每个任务执行一个时间片,然后切换到下一个任务,直到所有任务执行完毕。

集群环境下的任务调度框架

ElaticJob, Quartz,XXL-JOB

rpc调用过程中,注册中心的作用

注册中心:存放和调用服务,实现服务和注册中心,服务与服务之间的相互通信,服务的注册和发现。注册中心的作用是将服务的地址和端口注册到注册中心,供其他服务调用。常见的组成中心有:Zookeeper,Eureka,Consul,Nacos。

服务提供者: 需要为某个功能对外提供服务时我们可以暴露服务给使用者调用,服务提供者需要将自己的服务注册到注册中心,供其他服务调用。

服务消费者: 需要调用某个功能时,可以从注册中心获取服务提供者的地址和端口,然后调用服务提供者的服务。

容器:用于加载和运行服务提供者和服务消费者。

讲一讲乐观锁

乐观锁是一种并发控制机制,用于解决并发情况下的数据一致性问题,就是读不加锁,写加锁。乐观锁的实现方式有两种:版本号机制,时间戳方式和CAS机制。

版本号方式:在数据表中增加一个版本号字段,每次更新数据时将版本号加1,同时将当前版本号作为更新条件,如果当前版本号和更新条件中的版本号一致,则更新成功,否则更新失败。

时间戳方式:在数据表中增加一个时间戳字段,每次更新数据时将时间戳更新为当前时间戳,同时将当前时间戳作为更新条件,如果当前时间戳和更新条件中的时间戳一致,则更新成功,否则更新失败。

CAS机制:CAS全称为Compare And Swap,即比较并交换,是一种无锁算法,CAS机制是通过CPU的原子指令实现的,CAS操作包含三个操作数:内存地址V,旧的预期值A,新的预期值B。CAS操作的执行过程如下:
当执行CAS操作是,只有当V的值等于A时,才会将V的值更新为B,否则不会更新V的值。CAS操作是原子操作,可以保证在多线程的并发情况下,只有一个线程能够成功地将V的值更新为B,其他线程都会失败。

讲讲公平锁与非公平锁

公平锁:多个线程按照申请锁的顺序去获取锁,线程会直接进入队列去排队,先进先出获得锁。
非公平锁:多个线程获取锁的顺序是随机的,线程不会直接进入队列去排队,而是直接去尝试获取锁,如果获取失败,再进入队列排队。
公平锁的优缺点:

  1. 优点:所有线程都能够获得锁,不会出现饥饿现象。
  2. 缺点:吞吐量下降,因为每个线程都需要排队,所以会导致线程阻塞,从而降低吞吐量。cpu唤醒阻塞线程的开销比较大,会导致cpu资源浪费。
    非公平锁的优缺点:
  3. 减少CPU唤醒阻塞线程的开销,提高吞吐量。
  4. 缺点:可能会导致某些线程一直获取不到锁,出现饥饿现象。

实现方式:ReentrantLock中的构造方法可以传入一个boolean类型的参数,true表示公平锁,false表示非公平锁。synchronized关键字是非公平锁。

分布式锁的实现方式,各自的优缺点

  1. 基于Zookeeper实现:在Zookeeper创建一个持久节点ParentLock,当客户端想要获取锁时,在ParentLock节点下创建临时顺序节点。然后客户端再去获取临时节点是否靠前的一个,如果是则获取锁,另一个客户端先创建节点,然后获取临时节点靠前,如果不靠前的,并且不是最小的序号,此时向前面的节点注册Watcher,用于监听前一个节点的锁是否存在。任务完成后删除临时节点,由于节点都是相互监听的,当前一个节点消失,下一个节点被置顶。如果机器宕机了,会自动删除临时节点。
  2. 基于Redis实现:通过setnx命令实现。其中setnx命令是Redis中的原子操作,可以保证在多个客户端的并发操作中,只有一个客户端能够成功地向Redis中设置键值对。
  3. 基于数据库的排它锁实现,通过数据库的排它锁实现,当一个客户端获取锁时,会将锁的信息写入数据库,当另一个客户端获取锁时,会查询数据库中是否存在锁的信息,如果存在,则获取锁失败,如果不存在,则获取锁成功。当任务完成后,删除锁的信息。

优缺点:
Zookeeper

优点:封装好的框架,容易实现,有等待锁的队列,提升抢锁的概率
缺点:添加和删除节点的性能比较差

Redis

优点:性能比较好
缺点:实现复杂,需要考虑超时,原子性,误删等情况,没有等待锁的队列,需要在客户端自旋等锁,抢锁的概率比较低

数据库

优点:容易理解
缺点:复杂度较高,需要设计表,策略等如果获取锁失败,需要不断的链接数据库,查库操作,数据库的强依赖性,导致数据库不可用时,业务系统也会不可用

redis集群如何实现分布式锁

保证分布式锁的有效性和安全性的要求如下:
互斥:任何时刻只能有一个client获取锁
释放死锁:即使锁定资源的服务崩溃或分区,仍然能够释放锁
容错性:只要多数redis节点在使用,client就可以获取和释放锁
Redlock+Redisson
如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点,恰好这时master节点发生故障,一个slave节点就会升级为master节点,线程二就可以获取同个key的锁,但线程一也已经拿到锁了,锁的安全性就没了

Redlock的实现步骤

  1. 获取当前时间,单位是毫秒
  2. 按顺序向N个Redis节点请求加锁,客户端设置网络连接和响应超时时间,并且超时时间要小于锁的失效时间,如果超时,跳过该master节点,尽快去尝试下一个master节点
  3. 客户端使用当前时间减去开始获取锁的时间,得到获取锁的时间,当且仅当从大多数(N/2+1)的master节点都取到锁,并且总的获取锁的时间小于锁的失效时间时,锁才算获取成功
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所消耗的时间
  5. 如果获取锁失败了,客户端向所有master节点发送解锁请求

Spring循环依赖

Spring循环依赖是指两个或者多个bean相互依赖,形成一个闭环,导致bean无法被实例化。Spring循环依赖的解决方案就是使用三级缓存,

TCP可靠性保障

  1. 应用层:通过应用层的重传机制保证可靠性,如HTTP协议中的重传机制。
  2. 校验和:通过校验和机制保证可靠性,校验和是通过将数据包中的数据进行累加,然后将累加的结果存储在校验和字段中,接收方在接收到数据包后,将数据包中的数据进行累加,然后将累加的结果与校验和字段中的值进行比较,如果两个值相等,则说明数据包没有损坏,否则说明数据包损坏了。
  3. 序列号/确认应答:通过序列号/确认应答机制保证可靠性,发送方在发送数据包时,会给数据包设置一个序列号,接收方在接收到数据包后,会给发送方发送一个确认应答,确认应答中包含接收到的数据包的序列号,发送方在接收到确认应答后,会将序列号小于等于确认应答中序列号的数据包从发送缓冲区中删除,从而保证数据包的可靠性。
  4. 连接方式:点对点的连接外加三次握手机制的可靠连接
  5. 流量控制:通过滑动窗口机制保证可靠性,滑动窗口机制是指接收方在接收到数据包后,会给发送方发送一个窗口大小,发送方在接收到窗口大小后,会根据窗口大小来发送数据包,从而保证接收方不会因为接收数据包太多而导致缓冲区溢出。防止丢包
  6. 拥塞控制机制:通过拥塞控制机制保证可靠性,拥塞控制机制是指当网络拥塞时,会导致数据包丢失,所以需要通过拥塞控制机制来控制发送方的发送速率,从而保证数据包的可靠性。防止丢包

粘包和拆包

粘包:发送方发送的数据包太大,接收方接收到的数据包太大,导致接收方将多个数据包当成一个数据包来处理,从而导致粘包。
拆包:发送方发送的数据包太大,接收方接收到的数据包太小,导致接收方将一个数据包拆分成多个数据包来处理,从而导致拆包。

处理方式:

  1. 消息定长:发送方发送的数据包的长度固定,接收方接收到的数据包的长度也是固定的,从而避免粘包和拆包。
  2. 在包围增加特殊字符进行分割,如\r\n
  3. 自定义协议方式

为何UDP不会发生粘包和拆包

因为UDP是面向无连接的,面向消息的,不会使用块的合并算法,其消息通信时有消息边界的,所以不会发生粘包和拆包。

跳表如何实现增删改查

理解调表的数据结构也就明白了

拼接字符串的方法

  1. 使用StringBuilder的append方法拼接字符串
  2. 使用+拼接字符串
  3. 使用String的format方法拼接字符串
  4. 使用String的concat方法拼接字符串
  5. 使用STring的join方法拼接字符串

区别:
append方法:由于是StringBuild方法,所以是线程不安全的,但是效率比较高。不支持指定初始化容量和增量大小,所以在拼接字符串时,如果字符串长度超过了StringBuilder的容量,则会导致StringBuilder扩容,从而影响效率。
+方法:书写简单,易于理解和维护,但是效率低,因为每次使用+方法拼接字符串时,都会创建一个StringBuilder对象,然后调用append方法拼接字符串,最后调用toString方法将StringBuilder对象转换为String对象。频繁使用这种方法拼接字符串,会导致大量的临时对象,导致内存空间的浪费。
format方法:
concat方法:每次调用concat方法拼接字符串时,都会创建一个新的String对象,然后将原来的String对象和新创建的String对象拼接起来,最后返回新创建的String对象。频繁使用这种方法拼接字符串,会导致大量的临时对象,导致内存空间的浪费。且每次调用都只能拼接两个字符串,不支持拼接多个字符串。
join方法:线程安全,实例可变,简单好用,但是在大量拼接字符串时,会导致过多的内存分配和垃圾回收,从而影响效率。

StringBuild和StringBuffer的区别

StringBuild是线程不安全的,StringBuffer是线程安全的。StringBuild的效率比StringBuffer的效率高。其中StringBuffer通过使用synchronized关键字来保证线程安全,所以效率比较低。

String类的常用方法

  1. charAt(int index):返回指定索引处的char值。
  2. length():返回字符串的长度。
  3. indexOf(String str):返回指定子字符串在此字符串中第一次出现处的索引。
  4. substring(int beginIndex, int endIndex):返回一个新字符串,它是此字符串的一个子字符串。左闭右开
  5. replace(char oldChar, char newChar):返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。
  6. contains(CharSequence s):当且仅当此字符串包含指定的 char 值序列时,返回 true。判断字符串是否包含某个字符串。
  7. equals(Object anObject):将此字符串与指定的对象比较。判断字符串是否相等。
  8. isEmpty():当且仅当 length() 为 0 时返回 true。判断字符串是否为空。
  9. toLowerCase():使用默认语言环境的规则将此 String 中的所有字符都转换为小写。

翻转字符串用什么方法

使用StringBuilder的reverse方法翻转字符串

LinkedList和ArrayList的区别

底层实现上Arraylist是动态数组,LinkedList是双向链表,所以遍历速度上ArrayList比LinkedList快,但是插入和删除速度上LinkedList比ArrayList快。内存消耗上ArrayList比LinkedList少,因为LinkedList需要存储指针,而ArrayList不需要存储指针。此外,LinkedList还实现了Deque接口,所以LinkedList还可以当做队列和栈来使用。

&和&&的区别

&是位运算符,&&是逻辑运算符。&是按位与运算符,&&是逻辑与运算符。&是按位与运算符,即将两个数转换为二进制数,然后按位进行与运算,最后将结果转换为十进制数。&&是逻辑与运算符,即当两个数都为true时,结果为true,否则结果为false。

1000个数据,查找一个数据,怎么做

  1. 顺序查找:从第一个数据开始,依次查找,直到找到为止。
  2. hash查找:将数据存储到hash表中,然后通过hash函数计算数据的hash值,然后根据hash值查找数据。
  3. 如果有序就二分查找,如果无序就hash查找

error和exception的区别

error是指虚拟机无法解决的严重问题,如系统崩溃,虚拟机错误,内存溢出等,一般不编写针对性的代码进行处理。
exception是指程序可以处理的异常,如空指针异常,数组越界异常,类型转换异常等,一般需要编写针对性的代码进行处理。

数据库的隔离级别,默认级别

四种:读未提交,读已提交,可重复读,串行化
默认级别:可重复读

mysql的锁有哪些

  1. 全局锁:整个数据库在这个状态下就处于只读状态,对于整个库的备份或者整个库的恢复都需要用到全局锁。全局锁的命令是Flush tables with read lock,执行这个命令后,只能做查询操作,不能做更新操作,如果做更新操作,会一直等待直到锁被释放。释放锁的命令是unlock tables,执行这个命令后,之前被锁住的线程才能继续执行。
    使用场景:全库的逻辑备份,而InnoDB存储引擎则不需要这样进行全局备份,因为InnoDB有自己的备份方式,即热备份,可以在不影响其他线程的情况下进行备份。
  2. 表级锁:表级锁中的读锁时共享锁,写锁时独占锁。元数据锁是一种特殊的表级锁,是对于表结构的锁,当对一个表做增删改查操作时,需要对表结构做修改,此时会对表结构加锁,防止其他线程对表结构做修改。AUTO-INC锁是一种特殊的表级锁,是对于自增长字段的锁,当对一个表做增删改查操作时,需要对自增长字段做修改,此时会对自增长字段加锁,防止其他线程对自增长字段做修改。
  3. 行级锁:分为三大类,记录锁,间隙锁,以及两种的结合

索引失效的情况

  1. 索引列上使用了函数,导致索引失效
  2. 对索引使用左或者左右模糊匹配
  3. 对索引列进行表达式计算
  4. 对索引隐式类别转换:如用整数查询字符串类型的字段,会导致索引失效,但是用字符串查询整数类型的字段,不会导致索引失效,因为mysql在遇到字符串和数字比较的时候,会自动将字符串转化为数字。
  5. 联合索引非最左匹配
  6. Where子句中的OR条件,如果OR条件中有一个条件列没有索引,那么即使其他条件列有索引,也不会使用索引。

乐观锁和悲观锁

悲观锁:每次读写数据时都会加锁,悲观锁的实现方式有两种:共享锁和排他锁。共享锁是指多个线程对同一个数据进行读操作,此时会加共享锁,其他线程可以对该数据进行读操作,但是不能对该数据进行写操作。排他锁是指一个线程对同一个数据进行写操作,此时会加排他锁,其他线程不能对该数据进行读操作和写操作。本质上就是sync关键字和ReentrantLock类的实现原理。

乐观锁:拿数据时不加锁,但是在更新的时候会判断一下在此期间这个数据有没有被更新,可以使用版本号和CAS算法实现。

MVCC中,不同时间戳的操作如何合并在一起的

synchronized和ReentrantLock的原理

synchroized是JVM层面的锁,ReentrantLock是JDK层面的锁。synchronized是通过monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit指令是插入到方法结束处和异常处,JVM通过这两个指令来实现synchronized关键字。ReentrantLock是通过AQS实现的,AQS是一个用于构建锁和同步器的框架,底层数据结构是双向链表。

ThreadLocal使用场景和原理

ThreadLocal:线程本地本地变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构,是一个线程内部的存储结构,该结构只能由当前线程读写,其他线程无法访问,从而实现了线程隔离的效果。ThreadLocal的使用场景如下:

  1. 用于保存线程安全的对象,如SimpleDateFormat,Random等。
  2. 用于保留每个线程独立信息,如在Java Web中,使用ThreadLocal保存用户信息Session实例,可以在任何地方使用,而不用传递参数,避免了参数传递的麻烦。

原理:ThreadLocal是通过ThreadLocalMap实现的,ThreadLocalMap是ThreadLocal的内部类,是一个以ThreadLocal对象为键,任意对象为值的存储结构,是一个线程内部的存储结构,该结构只能由当前线程读写,其他线程无法访问,从而实现了线程隔离的效果。ThreadLocal并不是用来解决共享资源的多线程访问的,因为每个线程中的资源都是副本,不会共享,因此ThreadLocal适合作为线程上下文变量,简化线程内的传参。

但是这个会导致内存泄漏:由于底层实现的Map中,key是弱引用,但是value是强引用,垃圾时会自动回收key,而value的回收取决于Thread对象的生命周期,因此当大量的value无法得到释放就会导致内存泄漏。解决方法:每次使用完ThreadLocal,都调用它的remove方法,清除数据。

ArrayList

底层实现是动态数组,它的容量能动态增长,查询快,增删慢,线程不安全,效率高
其他的list:如Vetor,linkedlist
vector的底层实现是数组,线程安全,效率低
linkedlist的底层实现是双向链表,查询慢,增删快,线程不安全,效率高

ArrayList线程安全版本的容器

CopyOnWriteArrayList是ArrayList线程安全版本的容器。CopyOnWriteArrayList是线程安全的,因为它使用一种称为写时复制的方法来实现并发性。当一个线程试图修改CopyOnWriteArrayList时,它会创建一个新的ArrayList,并将更改复制到新列表中。原始列表保持不变,因此其他线程可以继续访问它而不受影响。当所有线程都完成修改时,新的列表将替换旧列表。

CopyOnWriteArrayList的性能比其他线程安全的列表实现要低,因为它需要在每次修改时创建一个新的列表。但是,它在大多数情况下仍然是有效的,因为它能够在高并发环境中提供可预测的性能。

HashMap的原理

底层实现是数组+链表+红黑树,查询快,增删慢,线程不安全,效率高

ConcurrentHashMap的原理

基于HashMap进行的改进,在JDK1.7之前使用的是分段锁,JDK1.8之后使用的是CAS+Synchronized来保证线程安全,效率高

Spring用了哪些设计模式

  1. 单例模式:保证一个类只有一个实例,并提供一个全局访问点。
  2. 工厂模式:Sprinmg中的BeanFactory就是工厂模式的体现,BeanFactory使用了工厂模式来管理Bean的创建。
  3. 代理模式:Spring中的AOP就是使用了代理模式,Spring中的AOP使用了动态代理,动态代理又分为JDK动态代理和CGLIB动态代理。
  4. 观察者模式:Spring中的事件驱动模型就是观察者模式的体现,Spring中的事件驱动模型使用了观察者模式来实现。
  5. 适配器模式:Spring框架中的MVC框架中的HandlerAdapter就是适配器模式的体现,HandlerAdapter使用了适配器模式来实现。
  6. 模板方法模式:Spring中的JdbcTemplate就是模板方法模式的体现,JdbcTemplate使用了模板方法模式来实现。
  7. 策略模式:Spring中的依赖注入机制就是基于策略模式实现的,不同的策略可以实现不同的依赖注入方式
  8. 享元模式:Spring中的Bean是单例的,这就是享元模式的体现,享元模式可以减少对象的创建,从而减少内存的使用。

Bean的生命周期

在Spring中,Bean的生命周期可以分为以下几个阶段:

  1. 实例化:Spring容器根据配置文件或注解等方式,创建Bean的实例。
  2. 属性赋值:Spring容器将Bean的属性值通过构造函数、setter方法等方式注入到Bean实例中。
  3. 初始化前回调方法:在Bean实例化和属性赋值完成之后,Spring容器会调用Bean的初始化前回调方法,例如InitializingBean接口的afterPropertiesSet()方法或者自定义的init()方法。
  4. 初始化后回调方法:在Bean的初始化前回调方法执行完之后,Spring容器会调用Bean的初始化后回调方法,例如BeanPostProcessor接口的postProcessBeforeInitialization()方法和postProcessAfterInitialization()方法。
  5. 使用:Bean实例化和初始化完成后,可以被其他Bean或者应用程序使用。
  6. 销毁前回调方法:当Bean不再被需要时,Spring容器会调用Bean的销毁前回调方法,例如DisposableBean接口的destroy()方法或者自定义的destroy()方法。
  7. 销毁:在Bean的销毁前回调方法执行完之后,Spring容器会销毁Bean实例。
    需要注意的是,Bean的生命周期可以通过配置文件中的init-method和destroy-method属性来自定义初始化前和销毁前的回调方法。同时,BeanPostProcessor接口的实现类可以对Bean的属性进行修改和增强。

Bean是单例还是多例,如何修改

默认是单例的,可以通过配置文件中的scope属性来修改Bean的作用域,scope属性有以下几个值:

  1. singleton:单例,一个Spring容器中只有一个Bean实例,默认值。
  2. prototype:多例,每次从Spring容器中获取Bean时,都会创建一个新的Bean实例。

慢查询如何分析

  1. 开启慢查询日志:在my.cnf配置文件中添加slow_query_log=1,设置慢查询日志的阈值slow_query_log_file=/var/log/mysql/mysql-slow.log,设置慢查询日志的存储路径long_query_time=2,设置慢查询日志的阈值,单位为秒。
  2. 查看慢查询日志:使用tail -f /var/log/mysql/mysql-slow.log命令查看慢查询日志。
  3. 第三方工具可以帮助分析慢查询语句,比如MySQLTuner、Percona Toolkit等。
  4. 使用命令行分析慢查询语句,比如explain命令、show profile命令等。
  5. 使用图像化工具分析慢查询语句,比如MySQL Workbench、Navicat等。

优化方式:优化索引,优化SQL语句,优化数据库结构,优化服务器配置,优化服务器参数等。

Explain分析后,type的执行效率等级,什么等级是比较合适的

NULL:无需访问表或者索引,比如获取一个索引列的最大值或最小值。
system/const:当查询最多匹配一行时,常出现于where条件是=的情况。system是const的一种特殊情况,既表本身只有一行数据的情况。
eq_ref:多表关联查询时,根据唯一非空索引进行查询的情况。
ref:多表查询时,根据非唯一非空索引进行查询的情况。
range:在一个索引上进行范围查找。
index:遍历索引树查询,通常发生在查询结果只包含索引字段时。
ALL:全表扫描,没有任何索引可以使用时。这是最差的情况,应该避免。

设计一个秒杀场景

抢购手机

手撕代码:三个线程交替打印1-100

public class PrintNumber {
    private static int count = 1;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            while (count <= 100) {
                synchronized (lock) {
                    if (count % 3 == 1) {
                        System.out.println(Thread.currentThread().getName() + " " + count++);
                    }
                }
            }
        }, "线程1").start();

        new Thread(() -> {
            while (count <= 100) {
                synchronized (lock) {
                    if (count % 3 == 2) {
                        System.out.println(Thread.currentThread().getName() + " " + count++);
                    }
                }
            }
        }, "线程2").start();

        new Thread(() -> {
            while (count <= 100) {
                synchronized (lock) {
                    if (count % 3 == 0) {
                        System.out.println(Thread.currentThread().getName() + " " + count++);
                    }
                }
            }
        }, "线程3").start();
    }
}

Java中go在什么场景下使用

go是Java中的关键字,用于跳转到指定的标签处,一般用于多层循环中,可以跳出多层循环。

NIO

NIO是New IO的缩写,是一种同步非阻塞的IO模型,在Java NIO中,主要有三个核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)。
NIO中,应用程序会不断发起read调用,等待数据从内核空间拷贝到用户空间的这段时间,线程依旧是阻塞的,直到在内核把数据拷贝到用户空间的这段时间里,线程依旧是阻塞的,直到内核把数据拷贝的用户空间。
多路复用模型:多个通道可以注册到同一个选择器上,选择器负责监听这些通道上的IO事件,当某个通道上的IO事件发生时,选择器就会获取到该通道上的IO事件,然后将该事件分发给某个线程去处理。多路复用模型可以减少线程的数量,从而减少线程上下文切换的开销,提高系统的并发能力。

select、poll、epoll的区别

select本质上通过设置或检查存放fd标志位的数据结构进行下一步处理,基于轮询机制,每次都要遍历所有的fd,效率较低,同时支持的fd数量有限制,一般为1024个。

poll和select的原理类似,不同的是描述符fd的存放方式不同,poll使用的是pollfd结构,而不是select的fd_set结构,同时支持的fd数量没有限制。因为它使用的是链表维护的fd,其他的都差不多和select() 函数一样,poll() 函数返回后,需要轮询pollfd来获取就绪的描述符,根据描述符的状态进行处理,但是poll 没有最大文件描述符数量的限制。poll 和select 同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

epoll使用一个epfd管理多个socket描述符,也不限制socket描述符的个数,将用户空间的socket 描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。当epoll记录的socket产生就绪的时候,epoll会通过callback的方式来激活这个fd,这样子在epoll_wait便可以收到通知,告知应用层哪个socket 就绪了,这种通知的方式是可以直接得到那个socket 就绪的,因此相比于select 和poll ,它不需要遍历socket 列表,时间复杂度是O(1),不会因为记录的socket 增多而导致开销变大。

epoll 的操作模式
epoll 对socket 描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT 模式是默认模式,LT 模式与ET 模式的区别如下:

LT 模式:即水平出发模式,当epoll_wait 检测到socket描述符处于就绪时就通知应用程序,应用程序可以不立即处理它。下次调用epoll_wait 时,还会再次产生通知。

ET 模式:即边缘触发模式,当epoll_wait 检测到socket 描述符处于就绪时就通知应用程序,应用程序必须立即处理它。如果不处理,下次调用epoll_wait 时,不会再次产生通知。

ET 模式在很大程度上减少了epoll 事件被重复触发的次数,因此效率要比LT 模式高。epoll 工作在ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

springboot和spring的区别

spring是一个轻量化的开源开发框架,主要用于管理Java应用程序中的组件和对象,并提供各种服务,如事务管理,安全控制,面向切面编程和远程控制等。
springboot是基于spring框架开发的用于开发Web应用程序的框架,它可以简化Spring应用程序的初始化过程,提供了一种快速构建应用程序的方式。内置servlet容器,不需要在服务端部署tomcat,只需要将项目打成jar包,使用java -jar命令就可以启动项目。

AOP的实现方式

  1. 基于动态代理的实现方式:基于JDK动态代理和CGLIB动态代理实现的。代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类
  2. 基于静态代理实现:代理类在编译阶段生成,在编译阶段将通知织入Java字节码中,AspectJ就是基于静态代理实现的。但是代理对象需要和目标对象实现一样的接口,并且实现接口的方法,代码冗余,且维护成本高。

JDK动态代理,不实现接口就不能做代理吗?为什么

是的,因为如果选择JDK动态代理目标类,代理类根据目标类实现的接口动态生成,不需要自己编写,生成的动态代理类和目标类都实现相同的接口,如果目标类没有实现接口,那么代理类就无法生成。没有实现接口时要使用CGLIB动态代理。

Bean的生命周期

  1. 实例化:Spring容器根据配置文件或注解等方式,创建Bean的实例。
  2. 属性赋值:Spring容器将Bean的属性值通过构造函数、setter方法等方式注入到Bean实例中。
  3. 初始化前回调方法:在Bean实例化和属性赋值完成之后,Spring容器会调用Bean的初始化前回调方法,例如InitializingBean接口的afterPropertiesSet()方法或者自定义的init()方法。
  4. 初始化后回调方法:在Bean的初始化前回调方法执行完之后,Spring容器会调用Bean的初始化后回调方法,例如BeanPostProcessor接口的postProcessBeforeInitialization()方法和postProcessAfterInitialization()方法。
  5. 使用:Bean实例化和初始化完成后,可以被其他Bean或者应用程序使用。
  6. 销毁前回调方法:当Bean不再被需要时,Spring容器会调用Bean的销毁前回调方法,例如DisposableBean接口的destroy()方法或者自定义的destroy()方法。
  7. 销毁:在Bean的销毁前回调方法执行完之后,Spring容器会销毁Bean实例。
    需要注意的是,Bean的生命周期可以通过配置文件中的init-method和destroy-method属性来自定义初始化前和销毁前的回调方法。同时,BeanPostProcessor接口的实现类可以对Bean的属性进行修改和增强。

Spring循环依赖

A依赖B,B依赖A,这种情况下,Spring会抛出BeanCurrentlyInCreationException异常
spring进行扫描->反射后封装成beanDefinition对象->放入beanDefinitionMap->遍历map->验证(是否单例、是否延迟加载、是否抽象)->推断构造方法->准备开始进行实例->去单例池中查,没有->去二级缓存中找,没有提前暴露->生成一个objectFactory对象暴露到二级缓存中->属性注入,发现依赖Y->此时Y开始它的生命周期直到属性注入,发现依赖X->X又走一遍生命周期,当走到去二级缓存中找的时候找到了->往Y中注入X的objectFactory对象->完成循环依赖。
1、为什么要使用X的objectFacory对象而不是直接使用X对象?
利于拓展,程序员可以通过beanPostProcess接口操作objectFactory对象生成自己想要的对象
2、是不是只能支持单例(scope=singleton)而不支持原型(scope=prototype)?
是。因为单例是spring在启动时进行bean加载放入单例池中,在依赖的bean开始生命周期后,可以直接从二级缓存中取到它所依赖的bean的objectFactory对象从而结束循环依赖。而原型只有在用到时才会走生命周期流程,但是原型不存在一个已经实例化好的bean,所以会无限的创建->依赖->创建->依赖->…。
3、循环依赖是不是只支持非构造方法?
是。类似死锁问题
三级缓存:
1、单例池:存放已经实例化好的单例bean,singletonObjects
2、二级缓存:存放bean的objectFactory对象,用于循环依赖,earlySingletonObjects
3、三级缓存:存放bean的beanDefinition对象,用于存放bean的原始信息,用于解决循环依赖,singletonFactories

MyBatis传参方式

  1. 单个参数:基本类型、包装类型、String类型、Date类型、数组、集合、Map、实体类,使用#{}获取参数值,使用${}获取参数名称。
  2. 多个参数:使用@Param注解指定参数名称,使用#{}获取参数值,使用${}获取参数名称。
  3. 使用Map传参:使用#{}获取参数值,使用${}获取参数名称。
  4. 使用实体类传参:使用#{}获取参数值,使用${}获取参数名称。
    其中#{}和${}的区别是:#{}是预编译处理,${}是字符串替换。

手写一个单例模式

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

设计原则

  1. 单一职责原则:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
  2. 开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
  3. 里氏代换原则:所有引用基类对象的地方能够透明地使用其子类的对象。
  4. 迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用。
  5. 接口隔离原则:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
  6. 依赖倒转原则:抽象不应该依赖于细节,细节应该依赖于抽象。

Java为什么不支持多继承

  1. 多继承会引起命名冲突,如果多个父类中有相同的方法,那么子类不知道要继承哪个父类的方法。
  2. 多继承会带来歧义,如果多个父类中有相同的属性,那么子类不知道要继承哪个父类的属性。

MYSQL表中的有5亿的数据,分页查询如何实现

MySQL的索引原理

索引是一种数据结构,用来存储引擎用于快速查找记录的一种数据结构,需要额外开辟空间和数据维护其工作
索引的基本过程是:把创建索引列的内容和对应的行号记录到一个索引文件中,当查询时,先到索引文件中查找对应的行号,然后再到数据文件中查找对应的行号,最后返回查询结果。

Redis可以用来做什么

  1. 缓存:将热点数据存储到Redis中,可以提高数据的访问速度,减少数据库的压力。
  2. 分布式锁:使用Redis的setnx命令可以实现分布式锁。
  3. 简易的消息队列:Redis提供了发布和订阅机制,可以将消息发送到一个频道,然后订阅该频道的客户端就可以接收到消息。
  4. 数据存储:Redis支持多种数据结构,可以将数据存储到Redis中,比如将用户信息存储到Redis中。
  5. 计数器,排行榜等

BitMap和布隆过滤器的区别

BitMap是一种数据结构,用来存储0和1,可以用来判断某个元素是否存在,但是不能判断某个元素是否不存在,因为某个元素不存在,可能对应的位为0,也可能对应的位为1,因此BitMap不适合用来判断某个元素是否存在。布隆过滤器是建立在bitmap的基础之上的,通过对同一个元素使用多个hash函数,将多个hash函数得到的值对应的位设置为1,这样可以降低误判率。

HashMap处理哈希冲突的方式

  1. 开放地址法:当发生哈希冲突时,使用某种探测技术在散列表中形成一个探测序列,直到找到空的散列地址,将其插入其中。探测技术包括线性探测、二次探测、双重散列等。
  2. 拉链法:将散列到同一个散列地址的所有元素都保存在一个链表中,当发生哈希冲突时,只需要将元素插入到对应的链表中即可。
  3. 再哈希法:当发生哈希冲突时,使用另一个哈希函数计算出一个新的散列地址,直到找到空的散列地址,将其插入其中。
  4. 建立公共溢出区:当发生哈希冲突时,将冲突的元素都保存在公共溢出区中,当需要查找元素时,先在散列表中查找,如果没有找到,则到公共溢出区中查找。

在JDK1.8中hashmap通过链表+红黑树的方式来处理哈希冲突,当链表长度大于8时,链表会转换为红黑树,当红黑树节点个数小于6时,红黑树会转换为链表。

并发情况下使用Map,如何保证线程安全

  1. 使用ConcurrentHashMap,ConcurrentHashMap是线程安全的,因为它使用了分段锁,每个线程只能锁住一个分段,因此多个线程可以同时访问不同的分段,从而提高了并发性能。
  2. 使用syncronizedMap,syncronizedMap是线程安全的,因为它使用了synchronized关键字,synchronized关键字可以保证同一时刻只有一个线程可以访问同步代码块,从而保证线程安全。
  3. 使用HashTable,HashTable是线程安全的,因为它使用了synchronized关键字,synchronized关键字可以保证同一时刻只有一个线程可以访问同步代码块,从而保证线程安全。

ConcurrentHashMap的原理

在JDK1.7之前使用的是分段锁,JDK1.8之后使用的是CAS+Synchronized来保证线程安全,效率高

在JDK1.7中,ConcurrentHashMap使用了分段锁,ConcurrentHashMap中包含多个Segment数组,Segment继承了ReentrantLock,每个Segment维护了一个HashEntry数组,HashEntry是一个链表结构,每个Segment维护了一个链表结构的HashEntry数组,当一个线程占用了某个Segment时,其他线程可以访问其他Segment,从而提高了并发性能。

分段锁是可重入锁,因为Segment继承了ReentrantLock,因此可以重入,当一个线程获取了某个Segment时,可以再次获取该Segment,而不会发生死锁。

在JDK1.8中,ConcurrentHashMap使用了CAS+Synchronized来保证线程安全,ConcurrentHashMap中包含多个Node数组,Node是一个链表结构,每个Node维护了一个链表结构的Node数组,当一个线程占用了某个Node时,其他线程可以访问其他Node,从而提高了并发性能。

如何理解可重入锁

可重入锁是指同一个线程可以多次获取同一把锁,比如一个线程获取了某个锁,当再次获取该锁时,不会发生死锁,而是可以获取该锁,这就是可重入锁。

什么是公平锁和非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。

非公平锁为何吞吐量大

非公平锁的吞吐量比公平锁大的原因有以下几点:

  1. 非公平锁允许在获取锁的时候插队,即使有其他线程在等待获取锁,当前线程也有机会直接获取锁。这样可以减少线程的竞争和等待时间,提高吞吐量。

  2. 非公平锁在尝试获取锁时不会先检查是否有其他线程在等待,而是直接尝试获取锁。这样可以节省了检查等待队列的时间,提高了吞吐量。

  3. 非公平锁在某些情况下可能会导致等待线程长时间等待,因为如果有插队的线程一直获取锁,那么等待线程的等待时间可能会延长。但是在高并发的情况下,非公平锁通过减少线程的竞争和等待时间,可以更快地处理更多的请求,从而提高整体的吞吐量。

需要注意的是,非公平锁虽然可以提高吞吐量,但是对于线程的公平性可能会有所降低,因为插队的线程可能会一直获取锁,导致其他线程一直处于等待状态。因此,在选择锁的时候需要根据具体的场景和需求进行权衡。

JVM内存结构

JVM(Java虚拟机)内存结构分为以下几个部分:

  1. 方法区(Method Area):用于存储类的信息、常量、静态变量等。在JDK8之前,方法区是永久代(Permanent Generation)的一部分,而在JDK8之后,方法区被移除,取而代之的是元空间(Metaspace)。

  2. 堆(Heap):用于存储对象实例。堆是JVM中最大的一块内存区域,被所有线程共享。堆被细分为新生代(Young Generation)和老年代(Old Generation)。新生代又被细分为Eden空间、From Survivor空间和To Survivor空间。

  3. 虚拟机栈(VM Stack):每个线程在运行时都会创建一个栈,用于存储方法的局部变量、操作数栈等。每个方法在执行时都会创建一个栈帧,栈帧中存储了方法的局部变量表、操作数栈、动态链接、方法返回地址等信息。

  4. 本地方法栈(Native Method Stack):与虚拟机栈类似,但是用于执行本地方法(非Java代码)。

  5. 程序计数器(Program Counter Register):用于记录当前线程执行的字节码指令的地址。

除了上述内存区域外,JVM还会使用一些其他的内存区域,如直接内存(Direct Memory)等。直接内存并不是JVM的一部分,但是它可以被JVM所管理。直接内存是在堆外分配的内存,通常用于高效地进行I/O操作。

JVM为什么把堆区进一步划分为新生代和老年代

新生代和老年代的划分是为了更好地回收内存,因为新生代中的对象生命周期较短,而老年代中的对象生命周期较长,因此可以根据不同的垃圾回收算法对不同的内存区域进行回收。

手撕代码:两个线程交替打印奇偶数

public class PrintNumber {
    private static int count = 1;
    private static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            while (count <= 100) {
                synchronized (lock) {
                    if (count % 2 == 1) {
                        System.out.println(Thread.currentThread().getName() + " " + count++);
                    }
                }
            }
        }, "线程1").start();

        new Thread(() -> {
            while (count <= 100) {
                synchronized (lock) {
                    if (count % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + " " + count++);
                    }
                }
            }
        }, "线程2").start();
    }
}

redis为何使用跳表而不是B+树

Redis使用跳表而不是B+树的原因有以下几点:

  1. 实现简单:跳表的实现相对简单,代码量较少,易于理解和维护。相比之下,B+树的实现较为复杂,需要考虑节点的分裂和合并等操作。

  2. 内存效率高:跳表的节点结构相对紧凑,不需要额外的指针来连接节点,因此在存储数据时占用的内存空间相对较小。而B+树的节点需要存储指向子节点的指针,会占用更多的内存空间。

  3. 查询效率高:跳表的查询时间复杂度为O(log n),与B+树相当。而在实际应用中,跳表的查询性能通常与B+树相当甚至更好,因为跳表的节点结构更加紧凑,可以减少缓存未命中时的读取开销。

  4. 简化事务处理:Redis中的事务处理是基于日志的,使用跳表可以简化事务处理的实现。跳表的节点结构使得在插入、删除和更新操作时,可以通过简单的指针修改来实现事务的原子性。

综上所述,Redis选择使用跳表而不是B+树,是为了实现简单、内存效率高、查询效率高和简化事务处理等方面的考虑。

本文作者:GWB
当前时间:2023-11-09 11:11:09
版权声明:本文由gwb原创,本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 国际许可协议。
转载请注明出处!