常用面试题及回答三

131 阅读12分钟

1.IO模型有哪些?如何理解NIO,和BIO、AIO的区别是啥?谈谈reactor模型。

IO模型分为:
    1.同步阻塞IO(BIO):在执行IO操作时,如果数据没有准备好,线程或进程会被阻塞,直到数据准备好为止。
    2.同步非阻塞(NIO):线程或进程在发起IO请求后,不会等待数据准备好,而是立即返回。如果数据没有准备好,返回一个错误码,应用程序需要不断地轮询检查数据是否准备好。
    3.IO多路复用:允许一个线程或进程监视多个文件描述符,一旦某个文件描述符就绪(例如,有数据可读或可写),就能通知应用程序进行相应的处理。
    4.信号驱动IO(SIGIO):应用程序告诉内核,当某个文件描述符就绪时,使用信号通知它。
    5.异步非阻塞IO(AIO):应用程序发起IO请求后,可以立即返回,内核会在数据准备好后通知应用程序,或者在操作完成后通过回调函数返回结果。
reactor模型:
    reactor是一种网络服务器端用来处理高并发网络IO请求的编程模型。它的工作机制是:
        1.三类事件处理:reactor模型处理的事件主要包括连接事件、写事件、读事件。
        2.三个关键角色:
            Reactor:负责监听和分发事件,将IO事件分发给对应的处理器。
            Acceptor:负责连接事件,当有新的连接请求时,接受连接并创建新的处理器(Handler)来处理该连接后续的读写事件。
            Handler:处理读写事件,对具体的IO操作进行相应。
        3.背压机制:Reactor模型的一个显著性能优势是提供了背压机制,这意味着当系统负载过高时,可以向上游发送信号,请求减少数据的发送,从而避免过载。
        4.事件驱动的任务处理:
            Task:待处理的任务,可以被打断并在适当的时候由Executor调度执行。
            Executor:调度器,维护任务队列,包括等待运行的任务和被阻塞的任务。
            Reactor:维护事件队列,当有事件发生时,通知Executor来唤醒对应的任务。
在实际应用中,如使用java的Netty框架,Reactor模型可以通过事件循环(EventLoop)来实现,每个事件循环是一个Reactor,它负责监听和分发事件,而事件的处理则由Handlers来完成。

2.反射的原理是什么?反射创建类实例的三种方式是什么?

反射的原理是基于Java虚拟机(JVM)的类加载机制。当类被加载时,JVM会在内存中创建一个对应的java.lang.Class对象,这个对象包含了类的结构信息,如构造函数、方法、字段等。通过这个Class对象,我们可以摘运行时动态地获取类的信息、创建对象实例、访问和修改成员变量、调用方法等。
反射创建类实例的三种方式如下:
1.java.lang.Class.newInstance():这种方式会调用类的无参构造函数来创建实例。如果没有无参构造函数,这种方式将抛出异常。
    Class<?> clazz = Class.forName("com.example.MyClass");
    Object instance = clazz.newInstance();
2.java.lang.reflect.Constructor.newInstance():这种方式通过Constructor来创建实例,可以指定调用类的特定构造函数。
    Class<?> clazz = Class.forName("com.example.MyClass");
    Constructor<?> constructor = clazz.getConstructor(String.class);
    Object instance = constructor.newInstance("parameter");
3.sum.reflect.ReflectionFactory.newConstructorForSerialization().newInstance():这种方式是用于序列化时的特殊创建实例的方法。通过这种方式创建的实例不会初始化类的成员变量。
    // 注意:这种方式通常不推荐在普通应用中使用,因为它可能不会执行类的初始化代码
    sum.reflect.ReflectionFactory factory = sum.reflect.ReflectionFactory.getReflectionFactory();
    Constructor<?> constructor = factory.newConstructorForSerialization(clazz, Object.class.getDeclaredConstructor());
   Object instance = constructor.newInstance(); 

3.反射中,Class.forName和ClassLoader 区别

在反射中,Class.forName和ClassLoader.loadClass都可以用来加载类,但它们在使用和加载类时的行为有一些区别:
    1.Class.forName:
        这是一个静态的方法,可以直接通过类名来加载对应的类。
        它不仅会加载类,还会对类进行初始化,即执行类的静态初始化代码块。
        使用该方法时,需要传递类的全限定名(包括包名)作为参数。
        示例代码:
            Class<?> clazz = Class.forName("com.example.MyClass");
    2.ClassLoader.loadClass:
        这是一个实例方法,需要通过ClassLoader的实例来调用。
        它只加载类,但不会进行初始化,即不会执行类的静态代码块。
        使用该方法时,也需要传递类的全限定名作为参数。
        ClassLoader可以是系统类加载器、自定义加载器等,这允许我们更灵活地控制类的加载过程。
        示例代码:
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            Class<?> clazz = loader.loadClass("com.example.MyClass");
Class.forName更适合需要立即使用类,并且希望类被完全初始化的场景。
ClassLoader.loadClass更适合于当你需要更细粒度地控制类加载过程,比如在实现自定义类加载器时。

4.动态代理有几种方式?有什么优缺点?

动态代理在java中主要有两种实现方式:JDK自带的代理(JDK Proxy)和基于字节码操作的库(如CGLIB、Javassis等)。
    1.JDK Proxy:
        优点:
            最小化依赖:JDK Proxy不需要依赖额外的库,简化了开发和运维。
            JDK版本升级平滑:随着JDK版本升级,JDK Proxy通常不需要额外的适配。
            实现简单:使用JDK提供的API即可直接创建代理实例。
        缺点:
            必须实现接口:JDK Proxy要求代理的目标类必须实现至少一个接口,这对没有实现接口的类不适用。
        性能:传统观点认为JDK Proxy的性能不如CGLIB,但在现代JDK版本中,性能差距已经大幅缩小。
    2.CGLIB(Code Generation Libraty):
        优点:
            无需实现接口:CGLIB通过继承的方式生成代理类,因此不需要目标类实现任何接口,对于没有接口的类非常实用。
            高性能:CGLIB直接操作字节码,通常性能会比JDK Proxy好。
            灵活:可以只对关心的类生成代理,而不影响其他相关类。
        缺点:
            依赖外部库:使用CGLIB需要引入额外的依赖。
            JDK版本兼容:字节码操作库可能在新版本Java中需要更新以保持兼容性。
            变成复杂度:相对于JDK Proxy,CGLIB的使用稍微复杂一些。
代码实例:
JDK Proxy的简单使用:
    InvocationHandler handler = new MyInvocationHandler(targetObject);
    MyInterface proxy = (MyInterface) Porxy.newProxyInstance(
        targetObject.getClass().getClassLoader(),
        targetObject.getClass().getInterfaces(),
        handler);

CGLIB的简单实用:
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(MyClass.class);
    enhancer.setCallback(new MethodInterceptor() {
        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            // 代理逻辑
            return proxy.invokeSupper(obj, args);
        }
    });
    MyClass proxy = (MyClass) enhancer.create();

5.简述Mybatis的插件运行原理,以及如何编写一个插件?

运行原理:
    Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这四种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体是InvocationHandler的invoke()方法,当然,只会拦截指定需要拦截的方法。
插件编写:
    实现Mybatis的Interceptor接口并复写intercept()方法,然后再给插件编写注解,指定要拦截哪一个接口的哪些方法即可,配置文件中药配置编写的插件。

6.redis的高并发和快速的原因

1.redis是基于内存的,内存读写速度非常快;
2.redis是单线程的,省去了很多上下文切换线程时间
3.redis采用多路复用技术,允许它同时处理多个客户端请求,可以处理并发的连接,从而实现高吞吐率。
4.高效的数据结构:redis使用了如哈希表、跳表等高效的数据结构,这些数据结构能够提供快速的读写操作。

7.redis有哪些数据结构?

1.字符串(String)
    SET mykey "hello"
    GET mykey
2.列表(Lists)
    LPUSH mylist "item1"
    LRANGE mylist
3.集合(Sets)
    SADD myset "member1"
    SMEMBERS myset
4.哈希(Hashes)
    HMSET myhash field1 "Hello" filed2 "world"
    HGETALL myhash
5.有序集合(Sorted Set)
    ZADD myzset 1 "member1"
    ZRANGE myzset 0 -1 WITHSCORES
6.流(Streams,从Redis5.0开始支持)
7.位图(Bitmaps)
8.超日志(HyperLogLogs)
底层数据结构方面,Redis使用了以下几种
1.整数数组
2.双向链表
3.哈希表
4.压缩列表(ziplist)
5.跳表(skiplist)

8.谈谈Spring Bean的生命周期和作用域

image.png

Spring Bean的生命周期主要包含以下阶段:
    1.实例化:Spring容器负责创建Bean的实例。
    2.依赖注入:如果Bean有依赖关系,Spring容器会在这个阶段处理依赖注入。
    3.初始化:在这个阶段,Spring容器会调用Bean的初始方法(如定义了init-method)。如果Bean实现了InitializingBean接口,那么在初始化阶段还会调用afterPropertiesSet方法。
    4.使用:这是Bean可以自行其业务逻辑的阶段。
    5.销毁:当容器关闭时,如果Bean实现了DisposableBean接口,Spring容器会调用destroy方法。同样,如果定义了destroy-method,改方法也会调用。
    
        import javax.annotation.PostConstruct;
        import javax.annotation.PreDestroy;
        import org.springframework.beans.factory.DisposableBean;
        import org.springframework.beans.factory.InitializingBean;
        import org.springframework.stereotype.Component;

        @Component
        public class MyBean implements InitializingBean, DisposableBean {

            // 构造函数

            public MyBean() {
                // 构造函数注入
            }

            // 依赖注入完成后调用
            @Override
            public void afterPropertiesSet() throws Exception {
                // 初始化逻辑
            }

            // 使用@PostConstruct注解的方法
            @PostConstruct
            public void init() {
                // 初始化逻辑
            }

            // Bean销毁前调用
            @Override
            public void destroy() throws Exception {
                // 销毁逻辑
            }

            // 使用@PreDestroy注解的方法
            @PreDestroy
            public void cleanup() {
                // 销毁逻辑
            }
        }

Spring Bean的作用域:
1.singleton:默认作用域,容器中只存在一个Bean实例。
2.prototype:每次请求都会创建一个新的Bean实例。
3.request:每次HTTP请求都会创建一个新的Bean实例,仅适用于Web应用。
4.session:同一个HTTP Session共享一个Bean实例,仅适用于Web应用。
5.globalSession: 用于portlet应用环境,仅适用于Web应用。
6.application:整个Web应用共享一个Bean实例,仅适用于Web应用。
7.websocket: WebSocket的生命周期内共享一个Bean实例。

9.介绍下netty

Netty是一个基于Java NIO(非阻塞I/O)的成熟网络应用框架,它提供了异步和事件驱动的网络应用程序的开发工具。Netty主要用于构建高性能、高可靠性的网络服务器和客户端程序。

以下是对Netty的总结:

1.  提高稳定性:由于网络编程的复杂性,Netty提供了处理网络闪断、客户端重复接入、连接管理、安全认证等细节的解决方案,这些功能经过长时间打磨,稳定性高。

2.  降低开发成本:Netty封装了Java NIO的复杂性,开发者不需要处理底层的复杂多线程模型和缓冲区管理,可以快速开发网络应用。

3.  性能优势:

    -   支持多种序列化框架,通过ChannelHandler机制,可以自定义编码和解码器。
    -   提供线程池模式和Buffer重用机制,有助于性能调优。

4.  易于学习和使用:Netty通过简单的例子和组件,如EchoClient和EchoServer,让开发者容易理解其工作原理。

以下是使用Netty创建一个简单服务端的代码示例:

public class EchoServer {
    private final int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public void start() throws Exception {
        final EchoServerHandler serverHandler = new EchoServerHandler();
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(group)
             .channel(NioServerSocketChannel.class)
             .localAddress(new InetSocketAddress(port))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ch.pipeline().addLast(serverHandler);
                 }
             });
            ChannelFuture f = b.bind().sync();
            f.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 8080;
        new EchoServer(port).start();
    }
}

class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

在这个示例中,我们创建了一个EchoServer,它监听8080端口,并将接收到的消息打印到控制台。EchoServerHandler类继承自ChannelInboundHandlerAdapter,用于处理服务器端的I/O事件。这段代码展示了如何使用Netty构建一个简单的服务器端应用程序。

10.常用的分布式ID设计

常用的分布式ID设计主要解决在分布式系统中如何生成全局唯一ID的问题
1.单体环境下的解决方案:
    使用内存中的计数器:在单节点应用中,可以通过内存中的计数器来计算生成唯一ID。这种方法简单但可能导致ID含义暴露,需要通过加密算法混淆。
2.分布式环境下的解决方案:
    独立组件远程调用:例如,使用Redis的INCR命令或Zookeeper等中间件生成唯一ID。
    Snowflake算法:Twitter开源的分布式ID生成算法,通过组合时间戳、机器ID、数据中心ID和序列号来生成唯一ID。
    MongoDB ObjectId:MongoDB内置12字节ID,包含时间戳、机器ID、进程ID和计数序列。
    国内大厂开源实现:如微信的seqsvr、百度的uid-generator等,这些实现通常根据特定的业务场景进行优化。
3.关于Snowflake算法:
    snowfke算法通常不会受到夏令时或冬令时切换的影响,因为它依赖于System.currentTimeMillis(),这个时间戳是从197011号UTC时间开始计算的,与具体的时区无关。
代码实现如下:

public class SnowflakeIdGenerator {

    private long workerId;
    private long datacenterId;
    private long sequence = 0L;

    private long twepoch = 1288834974657L;

    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private long sequenceBits = 12L;

    private long workerIdShift = sequenceBits;
    private long datacenterIdShift = sequenceBits + workerIdBits;
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    private long sequenceMask = -1L ^ (-1L << sequenceBits);

    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("workerId can't be greater than %d or less than 0");
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than %d or less than 0");
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long timestamp = timeGen();

        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate id");
        }

        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }
}
这段代码是一个简化版的Snowflake算法实现,用于生成全局唯一的ID。实际使用时,需要根据实际环境设置合适的workerId和datacenterId。