Java面经

1、 类加载机制和类加载器的双亲委派机制

\quad 虚拟机将描述类的class二进制字节码文件加载到内存,并对数据进行校验、转换解析和初始化,最终成为可以被JVM使用的Java类型,这一整套流程就是类加载机制。细节如下:

  • 加载:通过全限定类名获取定义此类的二进制字节流,并将此字节流所代表的静态存储结构转化为方法区运行时的数据结构,最后在内存中生成一个代表这个类的Class对象,这样可以在后面通过该对象获得该类的各种数据,例如获得该类的Methods,Fields等等
  • 验证:主要是确保字节码文件中包含的信息符合当前虚拟机要求,比如版本号、文件格式、是否符合Java语言规范等等
  • 准备:设置类自己的变量(static修饰)分配初始值和内存,这里不包括实例变量,实例变量在初始化的时候随对象分配在堆中
  • 解析:JVM将常量池中符号引用替换为直接引用的过程
  • 初始化:在完成上述步骤后,执行类构造器<clinit>()方法,执行所有类变量的赋值操作和静态语句块的执行,最后执行构造函数

**注意:**上述流程中加载和(验证,准备,解析)同步进行,之后才是初始化
类加载器的双亲委派模型:

  • 类加载器完成“通过一个全限定类名来获取描述该类二进制字节流”的功能,相当于完成类的“加载”过程的第一步

  • Java有三种类加载器:启动类加载器、扩展类加载器和应用程序类加载器。如果需要的话可以自定义类加载器。启动类加载器加载javahome\lib目录下的类库,扩展类加载器加载javahome\lib\ext下的类库,应用程序加载器加载用户类路径classpath下的类库,默认使用这个类加载器。

  • 应用程序类加载器父类是扩展类加载器,扩展类加载器父类是启动类加载器,启动类加载器没有父类

  • 双亲委派模式是:如果一个类加载器收到了类加载的请求,它不会立马加载此类,而是将这个请求委派给父类加载器去完成,因此所有的加载请求都会传送到顶层的启动类加载器中。当父类加载器无法完成这个加载请求时子类才会自己加载
    在这里插入图片描述

  • 如果不这样做,会产生很多问题:例如自己创建一个java.lang.Object类,放在classpath下,直接被应用程序加载器加载;而启动类加载器又会加载java最顶级的父类Object,这两个类全限定类名完全一样,就会造成系统中出现多个java.lang.Object类,造成混乱。使用双亲委派机制,该类的加载会发现可以在顶层类加载器完成,也就不会被应用程序加载器加载了,因此java.lang.Object类在程序的各类加载器环境中都是同一个类。当然,你自己写的java.lang.Object类永远无法被执行

2、spring aop 的原理, jdk、cglib实现的区别

\quad AOP实现一些日志功能,对于指定包下所有方法都会执行相应逻辑。

@Component
@Aspect
public class LogAdvice {
    @Before("execution(* com.serivce.impl.*.*(..))")
    public void before(){
        System.out.println("------------方法执行前before()-----------");
    }

    @After("execution(* com.serivce.impl.*.*(..))")
    public void after(){
        System.out.println("------------方法执行后after()------------");
    }

    @AfterReturning("execution(* com.serivce.impl.*.*(..))")
    public void afterReturning(){
        System.out.println("------------方法执行后afterReturning()---");
    }

    @Around("execution(* com.serivce.impl.*.*(..))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable{
        System.out.println("------------环绕前-----------------------");
        System.out.println("签名(拿到方法名):" + joinPoint.getSignature());
        Object proceed = joinPoint.proceed();
        System.out.println("------------环绕后-----------------------");
        System.out.println(proceed);
    }

    @AfterThrowing("execution(* com.serivce.impl.*.*(..))")
    public void afterThrow(){
        System.out.println("------------有异常发生-------------------");
    }
}

\quad AOP和注解实现计时器Timer,当在一个方法上面加上@Timer时就能统计该方法运行时间:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Timer {
}
@Aspect
@Component
public class LogTimeInterceptor {
    private long startTime;

    @Pointcut("@annotation(com.aop.Timer)")
    public void point() {

    }

    @Around("point()")
    public Object doAround(ProceedingJoinPoint pjp) {
        long startTime = System.currentTimeMillis();
        Object obj = null;
        try {
            obj = pjp.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        long endTime = System.currentTimeMillis();

        MethodSignature signature = (MethodSignature) pjp.getSignature();
        String methodName = signature.getDeclaringTypeName() + "." + signature.getName();
        System.out.println(methodName + "cost time: " + (endTime - startTime) + "ms");
        return obj;
    }
}

\quad AOP(Aspect-Oriented Programming:面向切面编程) 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
\quad Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。
\quad 使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。
\quad 动态代理分为两类:一类是基于接口的动态代理,一类是基于类的动态代理。
注意:jdk创建对象的速度远大于cglib,这是由于cglib创建对象时需要操作字节码(使用了asm框架直接改变原类字节码)。spring默认使用jdk动态代理,如果类没有接口,则使用cglib。
\quad 这里给出一个cglib实现动态代理的场景,一个类没有接口,因此不能使用jdk proxy实现,我们实现一个万能代理类CglibProxy,这个类传入所需要代理对象的Class对象即可,如下:
\quad 被代理对象:

public class LiuDeHua {
    public void sing(){
        System.out.println("刘德华在唱歌");
    }
    public void dance(){
        System.out.println("刘德华在跳舞");
    }
}

\quad 这是万能代理:

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibProxy implements MethodInterceptor {
    public Object CreateProxy(Class<?> clazz){
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("谈出场费");
        Object res = methodProxy.invokeSuper(o, objects);
        System.out.println("收钱");
        return res;
    }
}

\quad 测试代码:

import org.junit.Test;

public class Main {
    @Test
    public void test(){
        CglibProxy proxy = new CglibProxy();
        LiuDeHua liuDeHua = (LiuDeHua) proxy.CreateProxy(LiuDeHua.class);
        liuDeHua.sing();
        liuDeHua.dance();
    }
}
谈出场费
刘德华在唱歌
收钱
谈出场费
刘德华在跳舞
收钱

3、ioc的原理

\quad 控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)。
\quad spring通过配置文件扫描我们在配置文件中写好的包下面所有class文件,对class文件中上面有@Bean,@Component,@Serive…等的类创建类对象,放入beanFactory。接着扫描各个类的成员变量,如果成员变量上有@Autowired注解,则在beanFactory中找出该成员变量对应类型的对象注入进去。简易版代码如下:首先定义两个注解@Bean和@Autowired,主程序ApplicationContext通过扫描当前classpath下面所有的class文件,并按照是否有@Bean创建对象放入beanFactory中。然后完成依赖注入,代码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {

}
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
package reflect;

import Annotation.Autowired;
import Annotation.Bean;

import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Properties;

public class ApplicationContext<T> {
    private HashMap<Class, Object> beanFactory = new HashMap<>();
    private String filePath;

    public T getBean(Class clazz){
        return (T)beanFactory.get(clazz);
    }

    // 初始化,获得所有的类
    public void initContext() throws Exception{
        InputStream resource = ApplicationContext.class.getClassLoader().getResourceAsStream("config/bean.properties");
        Properties properties = new Properties();
        properties.load(resource);
        for (Object key : properties.keySet()) {
            beanFactory.put(Class.forName(key.toString()),
                    Class.forName(properties.getProperty(key.toString())).newInstance());
        }
    }

    public void initContextByAnnotation() throws Exception{
        // 获得当前包路径
        filePath = ApplicationContext.class.getClassLoader().getResource("").getFile();
        // 扫描所有的class文件,也叫扫包,将包中用注解@Bean修饰的扫进去
        scan(new File(filePath));
        // 给类中标注了@Autowired的字段赋值
        assembleObject();
    }
    private void assembleObject() throws Exception{
        for (Class key : beanFactory.keySet()) {
            Object obj = beanFactory.get(key);  // 取出容器中类的键值对
            // 利用反射取出对象的所有field字段
            Field[] declaredFields = obj.getClass().getDeclaredFields();
            for (Field field : declaredFields) {
                // 没有被@Autowired修饰则不自动注入
                if(field.getAnnotation(Autowired.class) == null) continue;
                field.setAccessible(true);
                field.set(obj, beanFactory.get(field.getType()));
            }
        }
    }
    private void scan(File root) throws Exception{
        if(root.isDirectory()){
            File[] childFiles = root.listFiles();
            if(childFiles == null || childFiles.length == 0){
                return;
            }
            for(File child: childFiles){
                if(child.isDirectory()){
                    scan(child);
                }else{
                    // 取出文件的绝对路径
                    String absolutePath = child.getAbsolutePath();
                    // 不是class文件则忽略
                    if(!absolutePath.endsWith(".class")) continue;
                    // 取出相对路径
                    String path = absolutePath.substring(filePath.length() - 1);
                    // 根据相对路径得到全类名
                    path = path.replaceAll("\\\\", ".").replace(".class", "");
                    // 根据全类名反射得到类对象
                    Class<?> aClass = Class.forName(path);
                    // 如果该类没有被注解@Bean修饰则不放入容器
                    if(aClass.getAnnotation(Bean.class) == null) continue;
                    // 接口类不放入
                    if(aClass.isInterface()) continue;
                    // 用该类创建对象
                    Object instance = aClass.newInstance();
                    // 如果该实体类实现的是某个接口,则放入该接口的键值对
                    if(aClass.getInterfaces().length > 0){
                        beanFactory.put(aClass.getInterfaces()[0], instance);
                    }else{
                        beanFactory.put(aClass, instance);
                    }
                }
            }
        }
    }
}

4、Spring 循环依赖问题

\quad 首先需要明确:对于循环依赖的场景,只有单例对象的属性注入是可以成功的。构造器注入和prototype类型的属性注入都会初始化Bean失败。

5、Spring Bean生命周期

\quad Spring Bean的生命周期从创建Spring容器开始,直到最终Spring容器销毁Bean。对于一个类被包装成一个Bean对象,首先是会运行这个类的构造函数,接着根据配置信息或者注解给该类属性注入值,接下来如果该类有初始化方法则会调用初始化方法,然后才是该类的方法,最后才是该类的销毁。
\quad 总结一下:Spring Bean的生命周期可以分为四步:实例化、属性赋值、初始化和销毁。部分源码如下:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
		throws BeanCreationException {

	// Instantiate the bean.
	BeanWrapper instanceWrapper = null;
	if (mbd.isSingleton()) {
		instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
	}
	if (instanceWrapper == null) {
		// 1.创建Bean实例
		instanceWrapper = createBeanInstance(beanName, mbd, args);
	}
	final Object bean = instanceWrapper.getWrappedInstance();
	Class<?> beanType = instanceWrapper.getWrappedClass();
	if (beanType != NullBean.class) {
		mbd.resolvedTargetType = beanType;
	}

	// Allow post-processors to modify the merged bean definition.
	synchronized (mbd.postProcessingLock) {
		if (!mbd.postProcessed) {
			try {
				applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
			}
			catch (Throwable ex) {
				throw new BeanCreationException(mbd.getResourceDescription(), beanName,
						"Post-processing of merged bean definition failed", ex);
			}
			mbd.postProcessed = true;
		}
	}
	// Initialize the bean instance.
	Object exposedObject = bean;
	try {
		// 2.属性赋值
		populateBean(beanName, mbd, instanceWrapper);
		// 3.初始化
		exposedObject = initializeBean(beanName, exposedObject, mbd);
	}
}

需要注意的是,如果我们实现了Spring为我们保留的接口BeanPostProcesser,里面的postProcessBeforeInitialization会在初始化前执行,postProcessAfterInitialization会在初始化后进行。
\quad 在执行方法之前,会先看看是否使用动态代理进行了方法增强。

6、redis常用场景

使用场景:作为缓存
缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis内部是支持事务的,在使用时候能有效保证数据的一致性。 作为缓存使用时,一般有两种方式保存数据:

  • a)、读取前,先去读Redis,如果没有数据,读取数据库,将数据拉入Redis。
  • b)、插入数据时,同时写入Redis。

方案一会有缓存穿透问题:数据库中没有需要命中的数据,导致redis一直没有数据而一直命中数据库。
\quad Redis是一个开源的key—value型数据库,支持string、list、set、zset和hash类型数据。对这些数据的操作都是原子性的,redis为了保证效率会定期持久化数据。 Redis 是单线程,多路复用方式提高处理效率。

持久化

  • RDB 快照持久化
    • 优点:压缩二进制适合备份,加载恢复数据比AOF快
    • 缺点:没法做到实时持久化,频繁操作消耗资源
  • AOF 追加持久化
    • 通过追加命令可以实时持久化
    • 加载恢复数据慢

缓存穿透

  • key对应的数据在数据源并不存在,每次针对key的请求从缓存中获取不到,请求都会到数据库,从而压垮数据库。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
  • 解决方案:布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不会存在的key会被这个bitmap拦截掉。
  • 布隆过滤器:一种数据结构,由一个二进制向量组成bit [n],初始时这个向量元素全为0。当添加一个数据key的时候,我们通过多个hash函数计算出这个key的hash值,需要注意这个hash值取值范围为[0,二进制向量长度-1]。假设hash(1)=1,则将第2个格子由0变为1。如下图:
    在这里插入图片描述
  • 对于一个新的key,我们如何判断其是否存在于这个过滤器中呢?我们利用上面定义的几个哈希函数,分别计算出hash(key)的值,然后看其对应的地方是否都是1,如果存在一个不是1的情况,则该数据一定不在过滤器中。反之都是1,也不一定在,但是这样已经可以删选出大量一定不在过滤器中的情况
  • 布隆过滤器可以判断某个数据一定不存在,但是无法判断一定存在。
  • 优点:优点很明显,二进制组成的数组,占用内存极少,并且插入和查询速度都足够快。
  • 缺点:随着数据的增加,误判率会增加;还有无法判断数据一定存在;另外还有一个重要缺点,无法删除数据。

缓存击穿

  • key对应的数据存在,但在redis中已过期,此时如果有大量并发请求过来,这些请求发现缓存过期,就会从数据库中把数据从数据库加载进缓存

缓存雪崩

  • 当缓存服务器重启或者大量缓存集中在一个时间段失效,此时会给数据库带来很大压力,导致整个系统出现问题
  • 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

redis锁

  • INCR: key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中。
  • SETNX:如果 key 不存在,将 key 设置为 value;如果 key 已存在,则 SETNX 不做任何动作
1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求设置key的值,设置成功
5、 客户端B执行代码完成,删除锁

$redis->setNX($key, $value);
$redis->expire($key, $ttl);
  • SET:上面两种方法都有一个问题,会发现,都需要设置 key 过期。那么为什么要设置key过期呢?如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新。于是乎我们需要给锁加一个过期时间以防不测。
import redis
from pymongo import MongoClient

r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
tweet_db = MongoClient('mongodb://root:FzDwXxCl.121.$Root$@121.48.165.123:30011')['tweet_stream']['test_3_22_dbscan_clusters_AllEvents']
cursor = tweet_db.find()

cnt = 0
for event in cursor:
    cnt += 1
    key = "event" + str(cnt)
    val = str(event['describe'])
    r.set(key, val)
r.set('time', 's', ex=5000)
print(r.dbsize())  # 查看当前redis有多少个键值对
print(r.keys("*"))  # 打印当前redis所有的键
print(r.ttl("time"))  # 查看event1的过期时间(s)

r.save()  # 将数据采用RDB方式持久化到硬盘,保存文件名是dump.rdb
r.bgrewriteaof()  # 将数据采用AOF的方式持久化到硬盘,保存文件名是appendonly.aof
print(r.incr('$event1'))  # incr加锁
r.setnx('$event0', '$test')  # setnx加锁
r.set('$event0', '$test', ex=10)  # set加锁

7、分布式id,如何生成,使用redis自增序列号有什么风险?

雪花算法

\quad 核心思想是:分布式ID固定是一个long型的数字,一个long型占8个字节,也就是64个bit,原始snowflake算法中对于bit的分配如下图:
在这里插入图片描述

  • 第一个bit位是标识部分,在java中由于long的最高位是符号位,正数是0,负数是1,一般生成的ID为正数,所以固定为0
  • 时间戳部分占41bit,这个是毫秒级的时间,一般实现上不会存储当前的时间戳,而是时间戳的差值(当前时间-固定的开始时间),这样可以使产生的ID从更小值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
  • 工作机器id占10bit,这里比较灵活,比如,可以使用前5位作为数据中心机房标识,后5位作为单机房机器标识,可以部署1024个节点
  • 序列号部分占12bit,支持同一毫秒内同一个节点可以生成4096个ID

使用redis生成分布时id

\quad 使用Redis来生成分布式ID,其实和利用Mysql自增ID类似,可以利用Redis中的incr命令来实现原子性的自增与返回,比如:

127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增加1,并返回
(integer) 2
127.0.0.1:6379> incr seq_id // 增加1,并返回
(integer) 3
  • 使用redis的效率是非常高的,但是要考虑持久化的问题。Redis支持RDB和AOF两种持久化的方式。
  • RDB持久化相当于定时打一个快照进行持久化,如果打完快照后,连续自增了几次,还没来得及做下一次快照持久化,这个时候Redis挂掉了,重启Redis后会出现ID重复。
  • AOF持久化相当于对每条写命令进行持久化,如果Redis挂掉了,不会出现ID重复的现象,但是会由于incr命令过得,导致重启恢复数据时间过长。

8、mysql 慢查询 怎样优化,索引加哪些列,什么工具分析慢查询

基本概念

在这里插入图片描述

  • 建索引需要保证唯一,使用语句ALTER TABLEt_userADD INDEX index_name (column)
  • 主键一定是唯一性索引,唯一性索引并不一定就是主键
  • 数据表主键就相当于是书页码,索引相当于书的目录,有了目录我们可以很快的知道这本书的基本内容和结构,数据索引也一样,它可以加快数据表的查询速度
  • 数据表中只允许有一个主键,但是可以有多个索引

索引越多越好吗

  • 数据变更需要维护索引,因此更多的索引意味着更多的维护成本
  • 更多的索引意味着也需要更多的空间

慢sql分析

  • 通过SHOW STATUS LIKE '%slow_queries%';查看慢查询记录,如果值为1,则有慢查询。SHOW VARIABLES LIKE '%query%';可以看到当前慢查询设定的时间阈值为10s,也就是当一条sql运行时间超过10s后就会被记录下来。会将文件保存在C:\Program Files\MySQL\MySQL Server 8.0\data\mysql\slow_log.csv文件中
  • 在sql语句前加explain,可以分析本次查询是否走索引,如果没有走索引则修改sql或者尽量让sql走索引

9、事务的隔离级别

基本概念

  • 事务:一个事务是一个完整的业务逻辑单元,不能再分,要么全部执行成功,要么全部失败。
  • 事务四大特性(ACID)
    • 原子性:事务是最小的业务逻辑单元
    • 一致性:一个事务必须保证多条sql语句同时成功或失败
    • 隔离性:两个事务之间具有隔离性(这里就引发出隔离级别)
    • 持久性:最终数据必须持久化到硬盘中,一个事务才算是最终完成

高并发情况下各个事务出现的各种情况

  • 脏读:一个事务读取到了缓存中另一个事务未提交的数据。说明:当事务B对data进行了修改但是未提交事务,此时事务A对data进行读取,并使用事务B修改的数据做业务处理。
  • 不可重复读:读取数据的同时进行修改,一个事务范围内两个相同的查询却返回了不同数据。说明:当事务A、B同时对data进行访问,A读取,B修改。事务A第一次读取data后B提交,如果A再次读取data,则读到的数据就与第一次不同,这种情况称为不可重复读
  • 幻读:一个事务在读取数据时,另一个事务插入了数据,导致上个事务第二次读取数据时,数据不一致。说明:data 表有一条数据,事务A对data进行读取, 事务B对data进行数据新增 ,此时事务A读取只有一条数据,而最后实际data是有两条数据,就好象发生了幻觉一样情况成为幻读;

事务的四个隔离级别

在这里插入图片描述

  • 1、未提交读:可读取未提交的操作数据,最低的隔离级别,这种情况会出现脏读
  • 2、读已提交:一个事务等待另一个事务提交完后才能读取,解决了脏读问题,但会出现不可重复读
  • 3、可重复读:读取事务开启的时候不能对数据进行修改,解决了不可重复读问题,但存在幻读问题
  • 4、序列化:是最高的事务隔离级别,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用
  • 可以根据需求设置数据库的事务的级别。“读未提交”一般没用,“读已提交”解决脏读但存在不可重复读,“可重复读”解决了脏读和不可重复读,但会出现幻读。串行化读,都可以解决,但是需要注意的是事务级别越高性能越低。
  • MySQL默认第三隔离级别,Oracle默认第二隔离级别

mysql如何解决幻读

在这里插入图片描述
\quad 可以使用next-key锁,该锁包含记录锁和间隙锁。原理:将当前数据行与上一条数据和下一条数据之间的间隙锁定,保证此范围内读取的数据是一致的。
在这里插入图片描述

10、tcp三次握手,拥塞处理

五层模型

应用层:HTTP(超文本传输协议),HTTPS,FTP,DNS(域名解析),SMTP(电子邮件)
传输层:TCP、UDP
网络层:IP(Internet协议)、ICMP(Internet控制协议)
数据链路层
物理层

DNS解析过程

三次握手和四次断开

\quad SYN:同步序列编号(Synchronize Sequence Numbers)。是TCP/IP建立连接时使用的握手信号。在客户机和服务器之间建立正常的TCP网络连接时,客户机首先发出一个SYN消息,服务器使用SYN+ACK应答表示接收到了这个消息,最后客户机再以ACK消息响应。这样在客户机和服务器之间才能建立起可靠的TCP连接,数据才可以在客户机和服务器之间传递。
在这里插入图片描述
\quad 两次握手为啥不行?假设客户端发送syn给服务器的时候发生阻塞,服务器过了很久才收到,然后发送ack信息给客户端。但此时客户端已经断开连接,服务器就会一直等待。
\quad 因为连接的数据传输是双向的,因此断开也需要双向。首先A发送关闭连接请求给B,告诉B我不会给你发信息了,然后B发ACK信息确认。但此时如果B发数据给A,A还是会接受,处于半连接状态。接着B发送关闭连接请求给A,告诉A我不会给你发信息了,A发送ACK信息表示确定。此时TCP连接正式断开。

TCP和UDP头部字段

在这里插入图片描述
32位序号:即编号,初始值:第一个数据在第一次交互时由系统随机生成。序号值如何变化?第一个为随机值,第二个就是发送的数据在整个字节流中的偏移量 + 第一次生成的值数据值也是从小到达排列:可以保证数据不乱序,举个例子:
在这里插入图片描述

在这里插入图片描述

TCP流量控制

\quad 滑动窗口机制。流量控制就是让发送方的发送速率不要太快,让接收方来得及接受。利用滑动窗口机制可以很方便的在TCP连接上实现对发送方的流量控制。
在这里插入图片描述
\quad 如上图如所示,发送方和接收方维护一个窗口,只有在窗口里面的数据才允许发送方发送到接收端。滑动窗口的大小由接收端缓存大小决定。在上图中,窗口大小为6,1,2,3已被发送并被确认,则窗口向右滑动3格;4,5,6被发送出去,但未收到ACK,此时还可发送7,8,9,但不能发送窗口以外的数据;等到4,5,6被发送出去后窗口继续向右移动

TCP拥塞控制

\quad 流量控制是点到点通信量的控制,拥塞控制是一个全局的过程,是防止数据量过大使得整个网络无法正常工作。假设有1个网络,其链路传输速率为1Mb/s,有1000台主机连接在上面。假设其中500台向另外500台以1Mb/s的速度发送数据,那么即使任意两个主机间来得及接受,无需流量控制,但整个网络无法承受,需要进行拥塞控制。
\quad 拥塞控制流程是:先试探整个网络负荷情况,先发送少量数据进行试探,再逐渐增加,拥塞窗口呈指数倍增长。增长到阈值时进入拥塞避免状态,窗口线性增加。若网络出现拥塞,则把阈值设置为当前窗口的一半,并将窗口大小设置为1重新开始下一轮试探。如下图:
在这里插入图片描述
快速重传:个别数据在网络中丢失,但此时网络并未发生拥塞,如果发送方迟迟收不到确认,就会超时,认为网络中发生了拥塞,影响效率。快速重传算法可以让发送方尽早知道发生了报文段的丢失,好重新发送。原理如下:假设发送的数据包M1,M2成功发送,接收方则返回ACK1,ACK2对这两个包确认。此时发送方发送M3,M3丢失,发送方又发送M4,M5,M6,但此时接受方不能发送ACK4,ACK5,而是发送ACK3,这样发送方会受到三次重复的ACK3,知道M3丢失,立即重传。快速重传策略可以提高网络吞吐量约20%

TCP和UDP区别

TCPUDP
是否连接面向连接无需连接
传输可靠性可靠不可靠
应用场合传输大量数据少量数据
速度
开销首部20字节8个字节
有无流量控制,数据检验等可靠功能
使用场景文件传输,远程登录QQ聊天、在线视频、广播通信

11、知道CAP吗

\quad 一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

  • 一致性:all nodes see the same data at the same time”,即所有节点在同一时间的数据完全一致。一致性是因为多个数据拷贝下并发读写才有的问题,因此理解时一定要注意结合考虑多个数据拷贝下并发读写的场景。
  • 可用性指“Reads and writes always succeed”,即服务在正常响应时间内一直可用。
  • 分区容错性指“the system continues to operate despite arbitrary message loss or failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务。

12、了解buffer,cache吗

  • buffer是缓冲,往往是顺序访问,例如两个主机之间进行数据传输就有缓冲区
  • cache是缓存,往往是随机访问,主要是为了加快运算速度。比如CPU的三级缓存,就是为了加快资源的命中

13、垃圾回收

判断对象是否是垃圾

  • 引用计数法
  • 可达性分析(JVM)

垃圾回收算法

  • 标记清除算法:内存碎片
  • 标记整理算法:效率不高,无碎片
  • 复制算法:双倍内存
  • 分代回收算法:JVM使用
    • 新生代使用复制算法,原因在于新生代对象死亡率高,复制算法在此时效率高
    • 老年代使用标记整理/清除算法,原因有二:老年代对象存活率高;复制算法需要额外空间,老年代没有额外空间作担保

14、字节码修改

\quad 可以使用ASM框架完成,CGLib的底层都是使用ASM框架完成的。

15、去除有序链表里重复的节点

class Solution {
public:
    ListNode* deleteDuplicates(ListNode* head) {
        ListNode *dummy = new ListNode(INT_MIN);
        dummy->next = head;
        for(auto p = dummy, q = dummy->next; q; ){
            while(q != NULL && p->val == q->val) q = q->next;
            p->next = q;
            p = p->next;
        }
        return dummy->next;
    }
};

16、一个是判断链表有无环

class Solution {
public:
    bool hasCycle(ListNode *head) {
        auto fast = head, last = head;
        while(fast && fast->next && fast->next->next && last->next){
            fast = fast->next->next;
            last = last->next;
            if(fast == last) return true;
        }
        return false;
    }
};

17、B+树对比B树的好处

B树是M叉树,所有叶子节点在同一层。M叉树意味着一个节点最多有M-1个关键字,一旦达到M个就要分裂。需要注意的是:B树每个节点都存有索引和数据,因此B树最好的情况下查询时间复杂度为O(1)。对于5阶B树,我们进行插入操作,如下:
在这里插入图片描述
\quad B+树就是在B树基础上,进行了一些改进:

  • 非叶子节点只存储索引不存数据,只在叶子节点存数据
  • 新建一条链,将叶子节点按照相邻从小到大顺序连接起来
    在这里插入图片描述
    B+树相对于B树来讲优点:
  • 单一节点存储元素更多,数据都在叶子节点上,而B树有部分数据在内部节点上,这样IO次数更少
  • 查询性能稳定,B树不稳定
  • 所有的叶子节点形成了一个有序链表,更加便于范围查找

18、乐观锁,CAS在哪用到,与悲观锁的区别

悲观锁

  • 假设每次去拿数据都认为别人会修改,所以在每次拿数据时都会上锁,让共享资源每次只给一个线程使用,Java中synchronizedReentrantLock等独占锁就是悲观锁的实现

乐观锁

  • 假设每次去拿数据的时候都认为别人不会修改,故不上锁。但是在更新的时候会判断一下此期间别人有没有去更新这个数据,这个的实现可以是版本号机制+CAS算法。乐观锁适合多读少写的场景。
  • 版本号机制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功
  • CAS算法:compare and swap,著名的无锁算法,即不使用锁的情况下实现多线程之间的同步。CAS算法涉及到三个操作数:需要读写的内存值V,进行比较的值A和拟写入的值B,当且仅当V=A相等时才更新,一般情况下是进行自旋操作,即不断地重试。
    • ABA问题:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。
    • 循环时间长开销大

19、sychronized原理,为什么慢,和ReentrantLock的区别

介绍一下ES

存放文档,本质就是字典树,创建倒排索引,记录每个字符属于属于哪些文档集合。

AB抛硬币,A先抛,谁先抛到正面谁赢,问两者赢的概率

\quad A赢有以下情况:正 1 2 \frac{1}{2} 21、反反正 1 2 3 \frac{1}{2^3} 231、反反反反正…因此A赢概率为 S = 首 项 1 − 公 比 = 1 2 1 − 1 4 = 2 3 S=\frac{首项}{1-公比}=\frac{\frac{1}{2}}{1-\frac{1}{4}}=\frac{2}{3} S=1=14121=32

MySQL innodb索引 聚簇/非聚簇索引 联合索引

  • 聚簇索引:将数据存储和索引放到了一块,索引结构的叶子节点保存了数据
  • 非聚簇索引:将数据和索引分开,索引结构的叶子节点指向了数据对应的位置。非聚簇索引都是辅助索引,辅助索引访问数据总是需要二次查找
  • 联合索引:当我们的where查询存在多个条件查询的时候,我们需要对查询的列创建组合索引。这样可以减少开销,假如对col1、col2、col3创建组合索引,相当于创建了(col1)、(col1,col2)、(col1,col2,col3)3个索引
    • 最左匹配原则:只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用复合索引时遵循最左前缀集合,所以在建立联合索引的时候查询最频繁的条件要放在左边
create table test(
a int,
b int,
c int,
KEY a(a,b,c));
select * from test where a=? and b=? and c=?;查询效率最高,索引全覆盖。
select * from test where a=? and b=?;索引覆盖a和b。
select * from test where b=? and a=?;经过mysql的查询分析器的优化,索引覆盖a和b。
select * from test where a=?;索引覆盖a。
select * from test where b=? and c=?;没有a列,不走索引,索引失效。
select * from test where c=?;没有a列,不走索引,索引失效。
  • 比如(a,b,c)的时候,b+数是按照从左到右的顺序来建立搜索树的,比如当(a=? and b=? and c=?)这样的数据来检索的时候,b+树会优先比较a列来确定下一步的所搜方向,如果a列相同再依次比较b列和c列,最后得到检索的数据

z型遍历二叉树

class Solution {
public:
    vector<vector<int>> zigzagLevelOrder(TreeNode* root) {
        vector<vector<int>> res;
        vector<int> t;
        if(!root) return res;
        queue<TreeNode *> q;
        q.push(root);
        int cnt = 0;
        while(!q.empty()){
            int sz = q.size();
            t.clear();
            for(int i = 0; i < sz; i ++ ){
                TreeNode *node = q.front();
                q.pop();
                t.push_back(node->val);
                if(node->left) q.push(node->left);
                if(node->right) q.push(node->right);
            }
            if(cnt & 1) reverse(t.begin(), t.end());
            res.push_back(t);
            cnt ++ ;
        }
        return res;
    }
};

k个一组反转列表,不足k个也要反转

class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        int len = 0;
        for(auto p = head; p; p = p->next) len ++ ;
        ListNode *pre = NULL, *cur = head;
        for(int i = 0; i < min(k, len); i ++ ){
            auto t = cur->next;
            cur->next = pre;
            pre = cur;
            cur = t;
        }
        if(k > len) return pre;  // 不足k个也要反转
        head->next = reverseKGroup(cur, k);
        return pre;
    }
};

有序链表的合并

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode *dummy = new ListNode(-1), *p = dummy;
        while(l1 || l2){
            if(l1 && l2){
                if(l1->val <= l2 ->val) p = p->next = l1, l1 = l1->next;
                else p = p->next = l2, l2 = l2->next;
            }else if(l1) p = p->next = l1, l1 = l1->next;
            else p = p->next = l2, l2 = l2->next;
        }
        return dummy->next;
    }
};

440. 字典序的第K小数字

class Solution {
public:
    int findKthNumber(int n, int k) {
        // 一位一位看答案前缀是多少
        int prefix = 1;
        while(k > 1){
            int cnt = f(prefix, n);
            if(k > cnt){
                k -= cnt;
                prefix ++ ;
            }else{  // k <= cnt,确定一位prefix,此时prefix新增一位,新增的从0开始
                k -- ;  // 本身也算
                prefix *= 10;
            }
        }
        return prefix;
    }
    // f(prefix, n):1~n中有多少个数前缀是prefix
    int f(int prefix, int n){
        long long p = 1;
        string a = to_string(n), b = to_string(prefix);
        int dt = a.size() - b.size();
        int res = 0;
        // 不足n位的有多少种情况,即只有len(prefix)+i位,这时全都在1~n取值范围内
        for(int i = 0; i < dt; i ++ ){  
            res += p;
            p *= 10;
        }
        a = a.substr(0, b.size());
        // 有n位的满足前缀位prefix且取值范围在1~n的有多少情况
        if(a == b) res += n - prefix * p + 1;
        else if(a > b) res += p;
        return res;
    }
};

synchronized和lock的区别以及在底层的实现(lock不会
mysql的redo log undo log binlog(难顶
事务的特性

进程通信的方式

https://www.zhihu.com/topic/20138175/top-answers

  • 同一主机:

    • 管道:一个进程往管道里面放入数据,只有等待另一个进程访问后才能退出,通信效率低
    • 消息队列:消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了
    • 共享内存:共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。用了共享内存通信方式,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
    • 信号量:信号量其实是一个整型的计数器,主要用于实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。共享内存初始化信号量为1,如果A进入,执行P操作,信号量-1,为0,表示可用,A占用,此时B进入,信号量-1<0,不可用,进入阻塞。等待A结束后执行V操作,信号量+1。这时B才可用
  • 不同主机:

    • Scoket:

操作系统的select poll epoll(听都没听过

final class String,final 关键字的用法:个人讲了语法,前端编译器做的工作

Map 如何做到线程安全:Collections 的装饰器,ConcurrentHashMap,加锁

ConcurrentHashMap 原理:1.7 和 1.8

TCP 和 UDP:为什么要三次握手,TCP 和 UDP 的差异、适合情景,TCP 怎么做到可靠传输,拆包粘包

设计一个 TCP 上的应用层协议,怎么拆包:答的首部长度字段,不太确定

线程和进程的区别

僵尸进程,信号:父子进程信号。。不太了解操作系统

JVM 的垃圾回收算法,两种算法的优势;新生代和老年代的交替。

LeetCode91字符串解码方案数目

class Solution {
public:
    int numDecodings(string s) {
        int n = s.length();
        vector<int> dp(n + 1, 0);
        dp[0] = 1;
        // dp[i] = dp[i - 1] + dp[i - 2](前两位取值范围为10~26)
        for(int i = 1; i <= n; i ++ ){
            dp[i] = s[i - 1] == '0'? 0: dp[i - 1];
            if(i > 1){
                int t = 10 * (s[i - 2] - '0') + s[i - 1] - '0';
                if(t >= 10 && t <= 26) dp[i] += dp[i - 2];
            }
        }
        return dp[n];
    }
};

进程之间通信的方式:pipe,socket,signal,共享内存

接触过什么 HTTP 状态码

怎么做垃圾回收:可达性分析

HTTPS:不会,只知道做了加密

算法题:简单的滑动窗口问题。对于一个 digit 的字符串,是否存在一个 len = 10 的窗口,包含 ‘0’ - ‘9’ 的所有字符。

bool check(vector<char> &digit){
    unordered_map<char, int> mp;
    int n = digit.size();
    if(n < 10) return false;
    for(int i = 0; i < 10; i ++ ){
        mp[digit[i]] ++ ;
    }
    for(int i = 10; i < digit.size(); i ++ ){
        if(mp.size() == 10) return true;
        mp[digit[i - 10]] -- ;
        if(mp[digit[i - 10]] == 0) mp.erase(digit[i - 10]);
        mp[digit[i]] ++ ;
    }
    return mp.size() == 10;
}

ping是通过什么协议实现的?
DNS的主要过程
如果服务器的ip地址发生了变化,如何通知给DNS服务器?
TCP中的CLOSE-WAIT状态是什么,为什么要有CLOSE-WAIT状态?
如何实现UDP的可靠数据传输?
路由器和交换机具体都实现了什么功能?路由选择是如何实现的?
介绍一下IPv6

Redis中的数据类型有哪些?
Redis中的String类型底层是如何实现的?
使用Redis进行数据统计,在高并发的情况下会不会有问题?
数据库的三大范式
数据库事务的特性
事务是如何实现隔离性的?
引入MVCC机制是为了实现什么?
B树和B+树的区别,能不能用二叉搜索树作为数据库的索引?

JDBC中的PreparedStatement有哪些优点?
JDBC的作用是什么?
JDBC是如何实现和数据库连接的,基于什么协议?
Spring中AOP的原理?哪种动态代理方式的性能更好?
进程和线程
互斥和信号量
Java应用都是单进程多线程的吗?有没有在什么项目中遇到过多进程单线程的例子

编程题:

  1. SQL题目
    四个表,学生表(学号,姓名)、成绩表(学号,课程号,成绩)、课程表(课程号、课程名、教师号)、教师表(教师号、教师名)
    查出所有平均成绩大于60的学生学号和平均成绩

Redis的底层数据结构说一下

Redis雪崩、击穿、穿透

Java GC说一下

IOC和AOP说一下

构造函数、析构函数能被重载和重写吗

我:析构函数不了解

两个栈实现一个队列

追问:什么时候第一个栈的数据会进入第二个栈里面

两个队列实现一个栈

因为没有做过,这个想了好久,想了8分钟了,面试官还一直等我!

找出一个字符串中有多少个回文子串,还有输出最大的回文子串

我:说了一个大概,面试官不太满意

为什么有了二叉树还要有平衡二叉树和红黑树

红黑树的性质

又是一波红黑树连问,我太难了

我:只答了一些

红黑树是怎么新增一个节点和删除一个节点

我:只提到了左旋右旋

哪些项目使用到了红黑树

我:HashMap

为什么要用红黑树

进程和线程的区别

进程通信了解吗

管道通信底层是怎么实现的

我:其实,所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。另外,管道传输的数据是无格式的流且大小受限。

使用fork创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个「fd[0]与fd[1]」,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。

什么是内核态和用户态

系统调用是怎么完成的

TCP三次握手对应的Socket编程的API说一下

UDP对应的Socket编程的API说一下

线程之间怎么进行数据同步

说说死锁

Http请求了解吗,说说

数据库索引的实现

详细说说B+树

行锁和表锁了解吗

存在如下层序序列的完全二叉树:[8,7,9,5,6,10,11,1,2,3,4,12,13,14,15],建立如上二叉树,并打印其前序遍历结果

struct TreeNode{
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int val){
        this->val = val;
        this->left = this->right = NULL;
    }
};
void dfs(TreeNode *root){
    if(!root) return;
    cout << root->val << " ";
    dfs(root->left);
    dfs(root->right);
}
int main(int argc, char const *argv[])
{
    vector<int> order = {8,7,9,5,6,10,11,1,2,3,4,12,13,14,15};
    vector<TreeNode *> nodes;
    for(int val: order) nodes.push_back(new TreeNode(val));
    for(int i = 0; i < order.size() / 2; i ++ ){
        nodes[i]->left = nodes[2 * i + 1];
        nodes[i]->right = nodes[2 * i + 2];
    }
    TreeNode *root = nodes[0];
    dfs(root);
    // 8 7 5 1 2 6 3 4 9 10 12 13 11 14 15
    return 0;
}

java基本类型
值传递和引用传递有什么区别
Stringbuffer 和Stringbuilder的区别
Hashmap的底层原理?安全吗?为什么不安全?扩容机制知道吗?在什么时候扩容
ConcurrentHashMap底层实现.为什么它是安全的?简单说一下安全机制
线程和进程的区别。线程池用过吗?里面的核心参数说一下。简单说一下拒绝策略(给自己埋坑拒绝策略不是很熟,说的一塌糊涂)
创建线程的方式?简单说一下callable
设计模式知道吗?说一下单例模式,具体有什么例子。简单说一下双重校验锁实现单例模式的思路
volatile了解吗?简单说一下?sychronized和lock的区别?什么是CAS
GC简单说一下
开发项目过程中用到哪些数据库?讲一讲事务的基本元素、隔离级别。
索引是什么、优缺点分别是什么
mysql引擎有哪些?简单说下Innodb引擎把,innodb数据结构是什么?为什么使用B+树
简单说一下Spring把?AOP怎么实现的?动态代理怎么实现的?IOC简单说一下
写几道算法题把:LRU
简单说一下快排的思路把
redis的缓存雪崩、缓存击穿说一下。解决方案是什么
redis热点了解吗 、说一下哨兵机制
redis怎么实现分布式锁的,说一下底层原理

leetcode76 最小覆盖字串

class Solution {
public:
    string minWindow(string s, string t) {
        unordered_map<char, int> hs, ht;
        for(char c: t) ht[c] ++;
        string res = s;
        int cnt = 0;
        for(int i = 0, j = 0; j < s.length(); j ++ ){
            hs[s[j]] ++ ;
            if(hs[s[j]] <= ht[s[j]]) cnt ++ ;  // 有效
            while(hs[s[i]] > ht[s[i]]) hs[s[i ++ ]] -- ;
            if(cnt == t.length() && res.length() > j - i + 1)
                res = s.substr(i, j - i + 1);
        }
        if(cnt != t.length()) return "";
        return res;
    }
};
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页