2.阿里面试题

1.Java基础

1.1JAVA中的几种基本数据类型是什么,各自占用多少字节。

  Java中有八种基本数据类型,分别是byte(1bit)、boolean(不占用)char(2bit)、short(2bit)、int(4bit)、long(8bit)、float(4bit)、double(8bit)。

1.2 String类能被继承吗,为什么。

  String类不能被继承,因为其被final关键字声明,不允许被继承。

1.3 String,Stringbuffer,StringBuilder的区别。

  1. String是一个不可变的字符串,线程安全;
  2. StringBuffer和StringBuilder都是可变的字符串,与String主要的区别是允许修改其内容,StringBuffer其内部方法是同步的,线程安全;
  3. StringBuilder内部方法不是同步的,线程不安全;由于StringBuffer内部方法是通过synchronized关键字修饰,在获取锁和释放锁消耗性能,因此在操作效率上低于StringBuilder。

1.4 ArrayList和LinkedList有什么区别。

  1. ArrayList底层是由动态数组构建的,LinkedList底层是链表结构
  2. ArrayList由于其数组结构并实现了RandomAccess接口,可以实现随机访问,而LinkedList不能
  3. LinkedList在头插、尾插元素(某些情况)效率上比ArrayList效率高
  4. ArrayList相对于LinkedList占用更少的内存,由于LinkedList需要存储前驱和后驱指针

1.5 讲讲类的实例化顺序,比如父类静态数据,构造函数,父类字段,子类静态数据,构造函数,字段,当new的时候,他们的执行顺序。类静态数据----->子类静态数据----->父类字段----->父类构造方法----->子类字段----->子类构造方法

  • 静态初始化块和字段只在类中初始化一次。

1.6 用过哪些Map类,都有什么区别,HashMap是线程安全的吗,并发下使用的Map是什么,他们内部原理分别是什么,比如存储方式,hashcode,扩容,默认容量等。

  • 使用过HashMap、ConcurrentHashMap;HashMap是线程不安全的,ConcurrentHashMap是线程安全的。
  • HashMap和ConcurrentHashMap底层结构都是数组+链表/红黑树组成的。
  • HashMap默认容量为16,当集合中元素超过容量的0.75时,进行扩容,每次扩容增长为原来的两倍。为减少哈希冲突,需要将通过hashcode()方法计算得到的hash值与数组长度进行取余操作,将存储的元素散列到各个位置。如果除数为2的幂次,那么取余操作相当于除数减一的与操作,采用二进制与操作,能够提升运算效率。
  • ConcurrentHashMap默认容量和扩容机制同HashMap,在JDK1.7时,其由Segment数组和HashEntry组成来保证多个线程同步,在JDK1.8以后,通过CAS操作和Node数组以及synchronized关键字保证每个Node数组之间的安全性(只要不产生hash冲突)。

1.7 JAVA8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。

  JDK1.7的ConcurrentHashMap是利用Segment和HashEntry保证多个线程访问,Segment实现了Lock接口,是一种重量级锁,当数据分布不均匀时,会加剧锁竞争。使用CAS和局部重排序能够更细粒度实现线程同步,还能减少Segment分配的锁对象开销,提高性能。

1.8 有没有有顺序的Map实现类,如果有,他们是怎么保证有序的。

  TreeMap就是有顺序的Map实现类,它是通过实现Comparator的compareTo方法保证有序

1.9 抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口

区别:

  • 接口强调对行为的约束,类强调的时从属关系;
  • 一个类只能继承一个父类,但是可以实现多个接口
  • 接口中的成员变量只能通过private static final修饰且必须初始化复制或构造函数赋值,抽象类中的成员变量是default类型,可由其子类重新定义或修改。

  类不能继承多个类,接口可以继承多个接口,类可以实现多个接口

1.10 继承和聚合的区别在哪。

  • 继承关注的是类之间的继承和扩展关系,而聚合关注的是类之间的组合和包含关系
  • 继承是一种强耦合关系,因为子类紧密依赖于父类的实现。而聚合是一种较弱的耦合关系,因为成员类可以独立于容器类存在和改变
  • 继承通常用于表示类之间的层次结构和抽象与具体的关系,而聚合用于表示类之间的组合和整体与部分的关系

1.11 IO模型有哪些,讲讲你理解的nio ,他和bio,aio的区别是啥,谈谈reactor模型。

  • IO模型有NIO(同步非阻塞)、BIO(同步阻塞)、AIO(异步非阻塞)三种IO模型
  • BIO是指应用程序发起Read调用,会一直阻塞,直到内核把数据复制到应用层
  • NIO是指同步非阻塞,它可以通过多次发起Read调用不阻塞,但当内核将数据拷贝到应用层是阻塞的
  • AIO基于事件和回调机制实现,应用操作后直接返回,当后台处理完,操作系统通知响应线程进行后续操作。

reactor模型

reactor模型是一种处理大量并发I/O请求的设计模式,常用于构建高性能服务器

  1. 单个或多个输入处理器(Reactor):负责监听和分发来自客户端的事件或请求到相应的处理器
  2. 事件处理器(Handler):负责处理实际的业务逻辑,如读取数据、执行计算、写回响应等。
  3. 多路复用:Reactor使用I/O多路复用技术(如select、poll、epoll等)来监听多个连接,而不是为每个连接分配一个线程。
  4. 事件驱动:系统基于事件驱动,当有新的事件发生时,Reactor会将事件分发给对应的处理器进行处理

1.12 反射的原理,反射创建类实例的三种方式是什么。

反射是指运行时分析类,执行类中方法的机制

反射创建实例的方法分为已知具体类和不知具体类

已知类使用类.class方法

不知类

  1. Class.forName(“全路径”)方法
  2. 对象实例instance.getClass
  3. 类加载ClassLoader.getSystemClassLoader.loaderClass(“全路径”)方法

1.13 反射中,Class.forName和ClassLoader区别 。

  Class.forName()和ClassLoader都用于加载类,但它们之间还是存在区别:

  • Class.forName()是一个静态方法,ClassLoader是一个类
  • Class.forName()和ClassLoader类都可能在加载类时出现ClassNotFoundException,但ClassLoader拥有更多加载选项和更细粒度控制。
  • Class.foName()加载的类如果没有初始化,那么它会通过Class.forName()进行初始化,ClassLoader默认不会初始化类,除非反射调用newIInstance()方法或其他初始化方法。

1.14 描述动态代理的几种实现方式,分别说出相应的优缺点。

JDK动态代理,代理接口,Proxy类的newProxyInstance()方法通过类加载器、接口、以及InvocationHandler对象以及重写InvocationHandler的invoke()方法实现动态代理

优点

  • 简单易用,只需重写InvocationHandler的invoke方法即可
  • 无需引入额外库
  • 目标类需要实现接口,才可创建代理对象

缺点

  • 目标类没有实现接口,无法使用JDK动态代理
  • final方法和private方法,无法进行代理

CGLIB动态代理,代理类。通过字节码技术,子类覆盖父类实现代理

优点

  • 无需类实现接口就能实现动态代理
  • 功能更强大,可代理final和private方法

缺点:

  • 需引入额外库(CG)
  • 相对于JDK代理更复杂,性能较差
  • 通过生成子类方法实现,可能会产生继承的副作用。

1.15 动态代理与cglib实现的区别。

  1. JDK动态代理是通过实现InvocationHandler的invoke方法结合反射增强被代理类,再Proxy.newProxyInstance()方法根据接口和InvocationHandler对象产生代理对象完成代理功能;
  2. CGLIB动态代理是通过实现MethodInterceptor接口中的intercept()方法结合反射增强被代理类,通过Enhancer.create()方法产生代理对象完成代理功能。

1.16 为什么CGlib方式可以对接口实现代理。

  其实CGlib是通过对接口的实现类实现动态代理,而不是对接口进行代理

1.17 final的用途。

  final可以用来修饰成员变量、类,方法。被final修饰的变量一旦赋值不能被修改。被final修饰的类不能被继承,被final修饰的方法不能被重写。

1.18 写出三种单例模式实现 。

饿汉

public class Singleton{
    private Singleton(){}
    private static Singleton singleton = new Singleton();
    private static Singleton getSingleton(){
        return singleton;
    }
}

懒汉式(线程不安全)

复制代码
public class Singleton{
    private Singleton(){}
    private static Singleton singleton;
    private static Singleton(){
        if(singleton == null){
             singleton = new Singleton();
        }
        return singleton;
    }
}
复制代码

双重检测懒汉式(线程安全)

复制代码
public class Singleton{
    private Singleton(){}
    private static Singleton singleton;
    private static Singleton(){
        if(singleton == null){
             sychronized(){
                  if(singleton == null){
                       singleton = new Singleton();
                  }
             }
        }
        return singleton;
    }
}
复制代码

 枚举懒汉式

public enum Singleton{
    INSTANCE;
}

1.19 如何在父类中为子类自动完成所有的hashcode和equals实现?这么做有何优劣。

   在Java中,我们不能直接在父类中为子类自动完成所有hashCode和equals方法的实现,因为每个类可能具有不同的相等性和哈希码计算逻辑。但可以创建一个通用基类或抽象类,提供默认的、基于对象状态的hashCode和equals方法,并鼓励子类重写这些方法以适应自己的需求。

优点

  • 父类提供的通用方法做模板,减少重复代码
  • 子类可以通过父类得到基本的equals方法和hashcode方法

缺点

  • 父类无法预知子类所有的字段,因此无法提供准确的equals方法和hashcode方法
  • 子类有不相等性规则时,不正确的继承会导致违反equals和hashcode约定

1.20 请结合OO设计理念,谈谈访问修饰符public、private、protected、default在应用设计中的作用。

  1. public

    • public访问修饰符表示成员(类、方法或变量)对所有其他类都是可见的,不受包或类层次结构的限制
  2. private:访问修饰符表示成员仅对其所在的类可见,对外部类完全不可见。常用于封装类的内部属性,只暴露必要的操作接口。

  3. protected

    • protected访问修饰符允许成员在同一个包内或者子类中可见,但在包外的其他类中不可见。
    • 保护成员对于实现继承关系下的复用和扩展特别有用。子类能够访问父类的受保护成员,以便于进行重写(override)或者使用父类的非公共功能
    • 使用protected关键字有助于实现OO中的继承和多态特性,同时保持一定程度的封装。
  4. default(无修饰符,也称为package-private):

    • 默认访问修饰符没有显式指定,成员仅在同一个包内的其他类中可见,而在包外则不可见。
    • 它支持同一包内的类之间共享某些功能,但不鼓励跨包间的直接访问,从而降低了不同包之间不必要的依赖关系。
    • 在包级私有成员的帮助下,可以构建更模块化的系统,其中各个包相对独立,相互之间只通过公共API进行通信。

1.21 深拷贝和浅拷贝区别。

  • 深拷贝和浅拷贝都是通过clone()方法拷贝父类引用。
  • 深拷贝是拷贝原对象的整个对象,包括原对象的内部对象。
  • 浅拷贝只是拷贝了原对象的内部对象的引用地址,浅拷贝对象和原对象还是共用同一个内部对象。

1.22 数组和链表数据结构描述,各自的时间复杂度。

  • 数组是内存连续的,支持随机访问,插入删除效率较链表低
  • 链表是内存不连续的,不支持随机访问,插入删除效率较数组高

1.23 error和exception的区别,CheckedException,RuntimeException的区别。

  • error和Exception有一个共同的父类Throwable。
  • error是应用程序无法处理的错误,不建议通过catch捕获,如虚拟机异常,Exception是程序可以处理的异常,可以使用catch进行捕获。
  • 异常分为受检查异常(CheckedException)和不受检查异常(RuntimeException及其子类)。
  • CheckedException是受检查异常,一定要捕获,不然无法编译通过
  • RuntimeException及其子类被称为非受检查异常,不捕获也能编译通过

1.24 请列出5个运行时异常。

  • NullPointerException(空指针错误)
  • IllegalArgumentException(参数错误比如方法入参类型错误)
  • NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
  • ArrayIndexOutOfBoundsException(数组越界错误)
  • ClassCastException(类型转换错误)

1.25 在自己的代码中,如果创建一个java.lang.String类,这个类是否可以被类加载器加载?为什么。

  不可以,String类是Java的核心类,由BootstrapClassLoader加载。Java类的加载遵循双亲委派加载机制。

1.26 说一说你对java.lang.Object对象中hashCode和equals方法的理解。在什么场景下需要重新实现这两个方法。

  hashcode和equal方法都是用来判断两个对象是否相等,其中先进行hashcode判断,如果不等,则两个对象不相等,反之进一步通过equals方法比较两个对象内存地址是否相等(Object默认),实际应用中一一判断两个对象的属性值是否全部相等,进而判断两个对象是否相等。

场景

  • 类作为哈希表的键或值
  • 基于字段判断对象是否相等
  • 一旦重写equals方法,必须重写hashcode方法,保证两个相等的对象hash值相等

1.27 在jdk1.5中,引入了泛型,泛型的存在是用来解决什么问题。

  泛型在编译时,通过参数表示类型,再具体使用(运行)时实现类型转换。

1.28 这样的a.hashcode() 有什么用,与a.equals(b)有什么关系。

  求对象的hash值,在使用equals比较两个对象是否相等时,会先计算两个对象的hashcode是否相等,如果不等则两个对象不相等,如果相同,则进一步通过equals方法比较两个对象的内存地址判断两个对象是否相等(Object默认),实际应用是一一判断两个对象的属性值是否全部相等

1.29 有没有可能2个不相等的对象有相同的hashcode。

  有可能,因为两个对象并不是具有相同的hashcode就是同一对象,这还需要通过equals()方法比较两个对象中内存地址相等(Object默认),实际应用中一一判断两个对象的全部属性值是否相等,才能决定两个对象是否相等。

1.30 Java中的HashSet内部是如何工作的。

  HashSet用于存储唯一且不重复的元素,当一个元素加入HashSet时,会先根据哈希算法计算该元素的hash值,如果hash值与HashSet中已加入元素的hash值不同,则加入到HashSet中;反之则进一步通过元素的equals方法计算它们是否相等,如果相等则该元素已存在,不必加入HashSet中;反之则通过拉链法解决哈希冲突,将元素以链表或红黑树形式链接。

1.31 什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决。

  序列化是将对象转换为二进制字节流进行数据传输。在Java中类可以通过实现Serializable接口实现序列化,也可以引入其他序列化协议进行序列化,如kryo、protobuf、Hession等。网络数据传输、文件存储、数据库、内存存储都是以二进制字节流流进行存储。如果我们需要将对象进行网络数据传输、文件、数据库、内存存储就需要进行序列化。

反序列化问题

安全问题:反序列化前需要对数据进行验证和过滤

兼容性问题:当对类中字段进行增删改时,序列化的类版本与运行时类版本不一致导致序列化异常或数据丢失;对于实现了Serializable但是没有自定义serialVersionUID的类,Java会自动生成一个序列化号,在改变类结构时容易出现序列化失败。

性能问题:大量数据反序列化会消耗大量内存和CPU资源,可通过分块读取和处理数据、合理设置缓冲区大小。

异常处理:在反序列化时会出现ClassNotFoundException、InvalidClassException(类格式错误或版本不匹配)、IOException,应妥善处理这些异常,保证序列化健壮性。

数据一致性问题:序列化包含无法持久化的引用(网络连接),反序列化会导致无效状态。

1.32 java8的新特性

  • lambda表达式:简洁方式定义匿名函数,提高代码可读性,简化集合流操作、事件处理
  • Stream流:简化集合的排序、过滤、映射、聚合操作
  • removeIf:简化集合中元素删除操作

2.JVM知识

2.1 什么情况下会发生栈内存溢出。

  • 递归调用过深:超过了JVM为线程(多个栈帧(局部变量、操作数栈、方法返回地址))分配的栈空间,导致栈内存溢出。确保递归有合理退出条件
  • 大量本地变量占用大内存:方法中声明过多大对象(几百K或几M的对象)作为局部变量如数组、大型对象占用大量栈空间,导致栈内存溢出;尽量将大对象放入堆中
  • 栈线程设置过小JVM通过-Xss设置线程大小,因栈限制导致栈内存溢出;适当增加-Xss参数,增大线程大小。
  • 死锁或循环等待引起栈内存溢出

2.2 JVM的内存结构,Eden和Survivor比例。

  在JVM内存结构中,Eden和Survivor都是堆中的新生代内存模块用于存储新创建的或生命周期较短的对象,其中Survivor有Survivor0区和Survivor1区,Eden与其余两Survivor内存占比为8:1:1

2.3 JVM内存为什么要分成新生代,老年代,持久代。新生代中为什么要分为Eden和Survivor。

  JVM内存被划分为不同区域,是为了更好管理和优化内存使用提高垃圾回收效率。 

新生代

新生代主要是存储新创建的或生命周期较短的对象,大多数对象在刚创建时很快变得不可达,成为垃圾对象。新生代通过复制算法进行垃圾回收,效率高,大多数对象在第一个GC后死亡。新生代栈堆内存较小部分。

老年代

老年代用于存储经过多次垃圾回收仍存活的对象,这些对象生命周期较长,或大对象(占用内存达到内存阈值的队形)。由于这些对象生命周期较长且数量相较稳定,因此采用标记-清除或标记压缩算法进行垃圾回收,频率较低。

持久代

  在JDK1.7及之前,方法区的一部分被称为持久代,用于存储类信息、常量池、静态变量、即时编译后的代码。持久代内存受限制,容易出现内存溢出问题。在JDK1.8之后,元空间取代了持久代,主要存储类元数据;它位于本地内存而不是堆内存,其大小受物理内存限制,JVM能动态调整元数据大小,有效管理内存。

Eden区和Survivor区的区别

  新创建的对象首先被分配到Eden区,Eden区满,会触发Minor GC(年轻代垃圾回收);Minor GC时,会检查Eden区和一个Survivor区(假设S0)中的对象,仍然存活的对象会被复制到另一个Survivor区(S1),这样可通过复制算法快速释放无用对象占用的内存,达到清理内存带来的碎片化问题经过15次检查仍存活的对象会进入Old区

2.4 JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代,说说你知道的几种主要的JVM参数。

  一次完整GC包含标记阶段、复制阶段、清理阶段、晋升检查与对象晋升、混合GC或Full GC。

标记阶段

  垃圾收集器首先通过可达性分析算法标记所有从根对象集合(全局变量、栈中引用)能直接或间接访问到的对象

复制阶段

  在新生代内存区域,Eden区和Survivor区都是采用复制算法进行垃圾回收,当Eden区空间不足,出发Minor GCMinor GC会将Eden区和一个Survivor区的对象复制到另一个Survivor区,在该过程中,对象年龄+1

清理阶段

  清理没有被复制对象占用的空间

晋升检查与对象晋升

  每次Minor GC多会检查对象的年龄当一个对象在Survivor区经历一定次数(默认为15次,可通过-XX MaxTenuringThreshold设置)的Minor GC依然存活,则晋升为老年代

混合GC或Full GC

  如果在进行Minor GC的过程中,发现要晋升的对象过多使得Survivor区无法容纳或者老年代也无法提供足够的空间,那么可能会触发对整个年轻代和部分老年代的混合式GC(如果使用CMS垃圾收集器),或者触发Major GC/Full GC(如果是G1或者Parallel Old垃圾收集器)来清理老年代和年轻代的所有空间

晋升为老年代的场景

  • 多次Minor GC仍存活的对象
  • 大对象(即需要分配大量连续内存大于特定阈值的对象,默认由-XX:PretenureSizeThreshold参数设定),它们在分配时可能直接进入老年代

主要的JVM垃圾收集相关参数

  • -Xms 和 -Xmx:设置堆内存初始大小和最大大小
  • -XX:NewRatio:设置年轻代和老年代的大小比例(默认1:2)
  • -XX:SurvivorRatio:设置年轻代中Eden区和Survivor区的比例(默认8:1:1)
  • -XX:+UseConcMarkSweepGC 或 -XX:+UseG1GC 等:选择垃圾收集器策略
  • -XX:MaxTenuringThreshold:设置对象晋升到老年代前在年轻代中经历的GC次数上限(默认15)
  • -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize(Java 8及之后版本):元空间大小的初始值和最大值
  • -XX:+PrintGCDetails:输出详细的垃圾收集日志信息

2.5 你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。

  垃圾回收器是JVM中负责回收内存的组件。Serial GC、Parallel GC/Parallel Old GC、CMS、G1等垃圾收集器

Serial-GC

原理流程:Serial GC是单线程的垃圾回收器,它在进行垃圾回收时会暂停所有用户线程(Stop the world)。整个过程包括年轻代的Minor GC和老年代的Major GC,采用标记-复制或标记-压缩算法

优缺点:简单高效,适合客户端应用或单核CPU服务器;在多核处理器系统中,由于只有一个GC线程可能导致较长时间的STW

Parallel GC/Parallel Old GC

原理流程:Parallel GC是对Serial GC的并行版本,在多核环境下使用多个线程进行垃圾回收,同样在回收过程中会暂停所有用户线程

优缺点:在多核CPU环境中性能较好,显著缩短STW时间;在高并发场景下,并行处理可能导致系统负载较大

CMS

原理流程

  • 初始标记(Initial Mark)STW阶段仅标记从根对象直接可达的对象
  • 并发标记(Concurrent Mark)遍历对象图,并标记所有可达对象,此时用户线程可继续运行
  • 重新标记(Remark)STW阶段修正并发标记期间因用户程序运行而发生变化的引用关系
  • 并发清除(Concurrent Sweep)清理未被标记的对象,此阶段用户线程依然可以运行

优缺点

  • 优点:大部分工作都并发执行,减少STW时间适用于对响应时间有较高要求的应用场景
  • 缺点:并发标记和并发清除阶段会导致系统资源竞争,可能影响应用程序性能;碎片化问题严重需要定期进行并发压缩(CMS的并发压缩功能在Java 9之后已经废弃)。

G1

原理流程

  • 区域化管理:将堆划分为多个大小相等的Region,每个Region都可以作为Eden、Survivor或Old区域
  • 并发标记:与CMS类似,先进行初始标记和根区域扫描,然后并发标记全堆
  • 空间回收:优先回收收益最大的Region(即垃圾最多),避免全堆整理,降低停顿时间
  • 完整GC(Full GC)在空间不足时进行,包含全局并发标记和压缩操作,但G1尽量避免触发这种类型的GC

优缺点

  • 优点:目标是 pauses are predictable and garbage collection work is done incrementally,提供了接近实时的垃圾回收预测模型通过并发标记和部分压缩来减少STW时间,并且能够自动进行分区平衡
  • 缺点:对于大量小对象分配的场景,G1的表现可能不如其他收集器;在大内存机器上,由于其复杂性,启动和初始化阶段可能会比较慢

2.6 垃圾回收算法的实现原理。

  垃圾回收算法主要是如何自动识别和释放不再使用的内存区域。垃圾回收算法分为垃圾标记算法和垃圾清除算法。垃圾标记算法包括、引用计数法和可达性分析;垃圾清除算法包括标记-清除算法、复制算法、标记-压缩算法、分代收集、增量式手机、并发收集、分区收集。

  1. 引用计数法 (Reference Counting)每个对象都有一个引用计数器,当有新的引用指向该对象时,计数器加一;当引用失效时,计数器减一。当对象的引用计数为零时,表示没有其他对象引用它,可以被回收。然而,这种方法无法处理循环引用的问题。

  2. 可达性分析/根搜索算法 (Reachability Analysis): 通过从一系列称为“根”的对象集合(如栈帧中的本地变量表、方法区静态变量、JNI局部引用等)开始遍历整个对象图,如果某个对象不能从这些根对象通过引用链到达,则认为它是不可达的,即垃圾。Java虚拟机采用的就是这种算法,并在此基础上发展出了多种垃圾收集器。

  3. 标记-清除 (Mark-Sweep): 分为两个阶段:首先,对堆中所有对象进行可达性分析,标记出存活的对象;然后,清理未被标记的所有对象所占用的内存空间。缺点是会产生不连续的内存碎片

  4. 复制算法 (Copying): 把内存分为两部分,每次只使用其中一部分,当这部分内存用完时,就将存活的对象复制到另一部分,同时清理掉原来的那部分内存。这样可以避免内存碎片,但会浪费一半的空间新生代的Eden区和Survivor区就是采用复制算法进行垃圾回收的。

  5. 标记-压缩/整理 (Mark-Compact): 类似于标记-清除,但在清除后会对存活对象进行压缩整理,把它们移到内存的一端,从而消除内存碎片。这种方式在老年代通常更为适用

  6. 分代收集 (Generational Collection): 基于对象生命周期假设,将内存划分为新生代和老年代,不同年龄段的对象使用不同的垃圾回收策略。新生代大多采用复制算法,而老年代一般采用标记-压缩或标记-清除

  7. 增量式收集 (Incremental Collecting): 将垃圾回收过程分成多个小步骤执行,每次执行一小部分,使得应用程序可以在垃圾回收过程中继续运行,减少STW(Stop-The-World)的时间。

  8. 并发收集 (Concurrent Collecting): 在应用线程运行的同时进行垃圾回收,比如CMS(Concurrent Mark Sweep)和G1(Garbage First)收集器的部分阶段就是并发执行的。

  9. 分区收集 (Region-Based Collecting): G1垃圾收集器进一步改进了分代收集,将堆划分为多个大小相等的独立区域,并优先回收垃圾最多的区域,以此降低停顿时间

2.7 当出现了内存溢出,你怎么排错。

   当出现内存溢出,可以通过识别错误类型收集详细信息分析堆转储文件排查代码逻辑调整JVM参数

识别错误类型通过系统日志或错误堆栈信息确定哪种类型的内存溢出。如Java Heap Space(堆内存不足)、PermGen Space/MetaSpace(方法区内存不足)、dnative memory(本地内存不足)

收集详细信息

  • 使用JVM参数 -XX:+HeapDumpOnOutOfMemoryError 在发生OOM时生成堆转储文件(heap dump),便于后续分析
  • 使用 jstack 工具获取线程快照,以了解程序在发生溢出时的状态和活动线程情况
  • 调整日志级别,输出详细的GC日志,使用 -XX:+PrintGCDetails 和 -Xlog:gc* 参数

分析堆转储文件

  • 使用如VisualVM、MAT(Memory Analyzer Tool)、JProfiler等工具分析堆转储文件找出占用大量内存的对象和可能的内存泄漏点
  • 查看是否存在大对象、长生命周期的对象或者对象数量异常增长的情况。

排查代码逻辑

  • 根据堆转储分析结果,结合代码审查,寻找可能导致内存泄漏的地方,比如静态集合类中的对象没有及时清理、长时间持有的大对象引用等。
  • 检查是否存在循环依赖导致对象无法被垃圾回收。

调整JVM参数

  • 根据应用需求和实际情况适当增加堆内存大小,例如 -Xms 和 -Xmx 设置初始和最大堆内存。
  • 对于Metaspace,可以调整 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 参数

2.8 JVM内存模型的相关知识了解多少,比如重排序,内存屏障,happen-before,主内存,工作内存等。

  JVM内存模型(JMM)定义了如何访问和修改共享变量的规则。

  1. 主内存与工作内存

    • 主内存(Main Memory):在JMM中,所有变量都存储在主内存中,它是共享资源区域所有线程都可以直接或间接地访问
    • 工作内存(Working Memory/Local Memory):每个线程拥有自己的工作内存,里面保存了该线程使用到的变量副本。当一个线程对变量进行读取或写入时,会先操作工作内存中的副本然后按照一定规则与主内存进行同步
  2. 重排序 (Reordering)

    • 多核处理器或者编译器优化过程中,为了提高执行效率,可能会改变代码的实际执行顺序,这种现象称为指令重排序。但重排序必须遵循as-if-serial语义,即单线程环境下看起来像是按照程序顺序执行的,但在多线程环境下可能会影响可见性和一致性。
  3. 内存屏障 (Memory Barrier/Fence)

    • 内存屏障是一种CPU指令,用于确保某些内存操作的顺序性它可以防止重排序保证特定的内存访问顺序,并且强制更新工作内存和主内存之间的数据一致性。例如,Load Barrier可以确保加载操作完成后再执行后面的指令,Store Barrier则可以确保前面的写操作提交到主内存后才执行后续的写操作。
  4. Happens-Before原则

    • Happens-Before是JMM中定义的一种先行发生关系,用来保证多线程环境下的程序执行的一致性和正确性如果一个操作A happens-before 操作B,那么操作A的结果对于操作B来说是可见的,且A的执行顺序早于B
    • JVM通过以下几种规则保证happens-before关系:
      • 程序次序规则在一个线程内,按照代码顺序,前面的操作happens-before后面的操作
      • 锁定规则:解锁操作happens-before随后对同一锁的加锁操作。
      • volatile变量规则对一个volatile变量的写操作happens-before后面对该变量的读操作
      • 传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C。

2.9 简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。

  类加载器再JVM负责加载类和接口二进制数据,并将它们转化为JVM可以直接使用的运行时类对象。每个类由特定的类加载器加载到JVM中,确保类的唯一性和安全性。其分为三种,分别是引导加载器(Bootstrap ClassLoader)、扩展加载器(Extension ClassLoader)、应用(系统)加载器(Application ClassLoader/System ClassLoader)。

  可以,在Java的类加载机制中,有一种双亲委派机制。在该机制中,当一个类加载器收到加载类请求时,首先会将请求委派给父加载器加载,如果父加载器可以加载,则向上层层递进,由父类的父加载器加载,直至父类的加载器无法加载,再通过父类的子加载器加载。这种自底向上,层层委托的加载机制可以防止类的重复加载,保证Java核心API库的稳定性和安全性

  在某些场景下,确实需要打破双亲委派模型。例如:

  • 实现隔离性:在容器环境如Tomcat或OSGi框架中,为了支持不同应用之间的类隔离,可能会创建多个类加载器,每个加载器负责加载各自的应用程序类,即使这些类有相同的全限定名,也可以通过不同的类加载器加载并视为不同的类

  • 插件系统一些插件框架允许用户动态加载扩展功能,而这些扩展可能需要使用与宿主应用程序相同名称但不同版本的类库。为了实现这一点,需要定制类加载器来加载插件私有的类库。

  打破双亲委派模型通常涉及自定义类加载器,即继承java.lang.ClassLoader类并重写其loadClass()方法,在此方法内部添加自定义加载逻辑,比如先尝试自己加载然后再委派给父加载器

2.10 你们线上应用的JVM参数有哪些。

  线上应用的JVM参数配置非常丰富,用于优化和调整Java应用程序在生产环境中的性能内存管理垃圾回收线程栈大小堆大小等多个方面。以下是一些常用的JVM参数示例:

  1. 内存相关参数(堆大小、元空间大小)

    • -Xms:设置Java虚拟机初始堆内存大小。
    • -Xmx:设置Java虚拟机最大堆内存大小。
    • -Xmn(年轻代大小):设置新生代的大小,适用于使用Parallel GC或CMS等垃圾收集器。
    • -XX:MetaspaceSize:设置元空间(MetaSpace,在Java 8及更高版本中取代了PermGen)的初始大小。
    • -XX:MaxMetaspaceSize:设置元空间的最大大小。
  2. 垃圾回收相关参数

    • -XX:+UseG1GC 或 -XX:+UseConcMarkSweepGC 等:选择垃圾收集器策略。
    • -XX:SurvivorRatio:设置年轻代Eden区与Survivor区的比例。
    • -XX:MaxTenuringThreshold:设置对象晋升到老年代的年龄阈值。
  3. 并发和线程栈相关参数

    • -Xss:每个线程的栈大小
    • -XX:ThreadStackSize:另一个设置线程栈大小的选项,具体用法取决于JDK版本。
    • -XX:ParallelGCThreads:指定并行垃圾回收时使用的线程数(针对Parallel GC)。
  4. 性能监控与诊断参数

    • -XX:+PrintGC 或 -verbose:gc:开启垃圾回收日志输出
    • -XX:+PrintGCDetails:输出详细的垃圾回收信息
    • -XX:+HeapDumpOnOutOfMemoryError:当发生OOM错误时,生成堆转储文件(heap dump)
  5. 其他高级特性参数:

    • -XX:+UseStringDeduplication:启用字符串去重功能(减少重复字符串对内存的占用)。
    • -XX:NewRatio:设置年轻代与老年代的内存比例
  6. 服务端模式启动

    • -server:告知JVM以服务器模式运行,通常提供更好的性能

2.11 g1和cms区别,吞吐量优先和响应优先的垃圾收集器选择。

  G1和CMS都是Java Hotspot中的垃圾回收器,它们在设计目标和实际应用上有所不同。

G1垃圾收集器:

  • 设计目标:兼顾吞吐量与低延迟。G1被设计为在大内存服务器上提供可预测的停顿时间,并且能处理非常大的堆内存(数十GB甚至上百GB)。
  • 特点:
    • 区域化管理:将整个堆划分为多个大小相等的Region,每个Region可以是Eden、Survivor或老年代。
    • 并发标记:大部分垃圾回收阶段并发执行,减少STW(Stop-The-World)时间。
    • 优先处理垃圾最多的区域:根据Region中垃圾对象的比例选择优先回收的Region,从而实现更高效的内存回收。
    • 记忆集:用于跟踪跨Region引用,以避免全堆扫描。
    • Pause Prediction模型:允许用户指定最大暂停时间目标

CMS垃圾收集器:

  • 设计目标:响应时间优先,适用于对响应时间要求较高的应用环境
  • 特点:
    • 并发标记清除:同样采用了并发标记阶段来减少STW时间,但清理阶段不并发,可能导致碎片问题。
    • 两个线程并行工作:一个进行标记,另一个负责重新标记可能在并发标记过程中改变的对象引用关系。
    • 无法设置最大暂停时间目标:CMS的目标是尽可能缩短每次GC造成的应用暂停时间,但不支持像G1那样预设最大暂停时间。

吞吐量优先与响应优先的选择:

  • 如果应用对系统整体的吞吐量(单位时间内完成的工作量)有较高要求,且能够接受较长的偶尔停顿时间,可以选择Parallel GC或Parallel Old GC
  • 如果应用对响应时间敏感,需要尽量降低因垃圾回收导致的程序暂停,那么CMS或者G1通常是更好的选择
  • G1在大多数现代应用场景下是一个更为综合的选择,因为它既可以优化吞吐量,也能保持较低的暂停时间,同时还能处理大堆内存

2.12 怎么打出线程栈信息。

  1. 通过Thread.currentThread().getStackTrace()获取当前线程的堆栈信息打印出当前线程调用栈的所有元素,包括类名、方法名、文件名和行号

  2. 使用Thread.getAllStackTraces()获取所有活动线程的堆栈信息

  3. Linux或Unix环境下使用jstack命令: 在终端窗口中,如果JDK已安装并配置好环境变量,可以直接执行 jstack 命令来获取指定Java进程的详细线程堆栈信息

  4. Windows环境下也可以使用类似的方法: 同样可以通过jstack命令来获取线程堆栈信息,只是命令行界面稍有不同。

  5. 触发JVM崩溃转储(Heap Dump)时附带线程堆栈: 可以向Java进程发送SIGQUIT信号(Linux/Unix环境下)或者在Windows上通过Ctrl+Break快捷键来触发一个核心转储文件,其中包含了所有的线程堆栈信息。然后可以使用像jhat(在较早版本的JDK中可用)或VisualVM等工具分析这个转储文件

2.13 请解释如下jvm参数的含义:

-server -Xms512m -Xmx512m -Xss1024K

服务器模式启动,内存管理、现成调度有更好性能;堆初始大小为512M,堆最大内存512M,每个线程栈最大内存1M。
-XX:PermSize=256m -XX:MaxPermSize=512m -

持久代初始内存256M,最大内存512M
XX:MaxTenuringThreshold=20 XX:CMSInitiatingOccupancyFraction=80 -

从新生代晋升为老年代的Survivor区复制的GC阈值为20次:该参数指定了触发CMS进行垃圾回收的阈值。当老年代空间使用率达到80%时,CMS将会启动垃圾回收
XX:+UseCMSInitiatingOccupancyOnly。

只根据 CMSInitiatingOccupancyFraction 参数来决定何时触发CMS垃圾回收,而不考虑其他因素如当前时间间隔等因素

3.开源框架知识

3.1 简单讲讲tomcat结构,以及其类加载器流程,线程模型等。

  Apache Tomcat是一个开源的Servlet和JavaServer Pages(JSP)容器,用于部署和运行Java Web应用程序。其主要结构和组件包括:

  1. Tomcat结构

    • Catalina:Catalina是Tomcat的核心引擎,负责处理请求与响应,并管理整个生命周期中的Web应用
    • Connector:连接器是Tomcat服务器与客户端之间的通信接口,它监听特定端口上的HTTP请求,并将请求传递给Catalina进行处理。常见的连接器有BIO、NIO和APR等。
    • Container容器层级结构包括Engine、Host、Context和Wrapper,它们共同构成了一个层次化的管理结构,分别对应服务级别、虚拟主机级别、Web应用级别以及Servlet级别。
      • Engine:顶级容器,代表整个Catalina Servlet引擎
      • Host每个Engine下可以包含多个Host代表服务器上的一个虚拟主机或IP地址+端口组合
      • Context:每个Host下可以有多个Context,代表一个Web应用程序上下文,即通常所说的部署在服务器上的WAR文件
      • Wrapper最底层容器代表了在Web应用程序中被加载的一个Servlet
  2. 类加载器流程

    • Tomcat遵循Java标准的类加载器架构,并在此基础上进行了扩展以支持Web应用的隔离性
    • 类加载器主要有以下几类:
      • Bootstrap ClassLoader:由JVM提供,加载核心Java库
      • System ClassLoader 或 Common ClassLoader:加载Tomcat自身的库和配置文件
      • Catalina ClassLoader加载Tomcat服务器内部使用的类
      • Shared ClassLoader共享类加载器,用于加载所有Web应用都可以访问的通用类库
      • WebApp ClassLoader(也称为Application ClassLoader):为每个Web应用创建独立的类加载器实例加载各自WAR包中的类,从而实现不同Web应用间类的隔离。
  3. 线程模型

    • Tomcat使用多线程来并发处理客户端请求
    • 对于处理请求的线程池,Tomcat可以根据不同的连接器配置不同的线程模型,如:
      • BIO(Blocking I/O):每个请求分配一个新的线程,适用于并发请求较少的情况。
      • NIO(Non-blocking I/O):通过Selector复用少量线程处理大量连接,适合高并发场景。
      • APR (Apache Portable Runtime):使用操作系统级别的API优化网络I/O性能,同样支持异步非阻塞模式

3.2 tomcat如何调优,涉及哪些参数 。

  Apache Tomcat调优是一个涉及多个方面的过程,主要是为了提高服务器性能减少响应时间以及优化资源使用。以下是一些关键的Tomcat调优参数和策略:

  1. 内存配置(JVM参数)

    • -Xms:设置JVM初始堆内存大小。
    • -Xmx:设置JVM最大堆内存大小。
    • -XX:MaxMetaspaceSize(Java 8及更高版本):设置元空间的最大大小,用于存储类元数据等。
    • -Xmn 或 -XX:NewRatio:调整年轻代与老年代的大小比例。
  2. 垃圾回收器选择与参数调整

    • -XX:+UseG1GC 或 -XX:+UseConcMarkSweepGC 等:选择适合应用特性的垃圾收集器。
    • -XX:MaxTenuringThreshold:控制对象晋升到老年代的年龄阈值。
  3. 连接器优化

    • maxThreads:在server.xml中配置Connector组件,设置最大并发处理线程数。
    • acceptCount:当所有请求处理线程都忙碌时,可以排队等待的最大请求数量。
    • connectionTimeout:设置客户端连接超时时间。
    • enableLookups:禁用DNS查找以加快处理速度(通常设为false)。
    • 启用压缩 (compression 和 compressableMimeType):对特定类型的HTTP响应进行压缩,降低网络传输负载
  4. 静态资源缓存

    • server.xml中的Context元素中配置resources标签,并启用静态资源缓存
  5. Tomcat内部机制优化

    • 考虑是否使用apr连接器来提升I/O性能
    • 根据实际情况设置线程池相关参数,如minSpareThreadsmaxSpareThreads等。
  6. 类加载器优化:

    • 控制Web应用程序之间类库共享,合理配置commonLoadersharedLoaderwebappLoader
  7. 日志输出级别:

    • 适当调整日志级别,避免过多的日志输出消耗系统资源
  8. SSL/TLS配置优化:

    • 使用更高效的加密算法,减少握手阶段的延迟。
    • 配置会话复用,提高HTTPS连接性能
  9. 性能监控与分析工具:

    • 利用JMX监控Tomcat性能指标
    • 使用JConsole、VisualVM或专门的APM工具(如New Relic、Dynatrace)监控和分析应用性能

3.3 讲讲Spring加载流程(注册(将类注册到Spring容器中)、实例化(将注册到BeanDefinition的类实例化一个Bean)、初始化(给实例化的Bean赋值))。

  1. 启动Spring容器:通过ClassPathXmlApplicationContextFileSystemXmlApplicationContextAnnotationConfigApplicationContext等类创建Spring IoC(控制反转)容器实例,这些类通常在应用程序主入口或者Servlet初始化时被调用

  2. 定位配置元数据:定义Bean以及依赖关系的配置信息

  3. 加载和解析配置元数据:将Xml文件中的Bean和注册Bean注解标注的类转化为BeanDefinition(类信息、作用域、属性、依赖关系),注册到BeanDefinitionRegistry中

  4. BeanFactoryPostProcessor:在Bean定义被完全加载并注册后,Spring容器执行所有实现了BeanFactoryPostProcessor接口的类,这些处理器可以修改已经注册但尚未实例化的Bean的定义属性

  5. Bean的实例化与依赖注入

      • Spring容器根据BeanDefinition开始实例化Bean,并执行依赖注入(DI)。这个过程包括:
      • 分析Bean的依赖关系,通过
    构造函数注入、setter方法注入等方式满足其依赖需求
      • 对于singleton作用域的Bean,仅实例化一次,并缓存以供后续请求重复使用
      • 对于prototype作用域的Bean,每次请求都会创建一个新的实例
  6. BeanPostProcessor

    • 每个Bean实例化之后但在初始化之前以及初始化之后但返回给客户端代码之前,Spring容器会调用实现了BeanPostProcessor接口的类对Bean进行预处理和后处理操作
  7. 生命周期回调方法

    • 实现了InitializingBean接口或声明了@PostConstruct注解的方法会在Bean实例化并完成依赖注入后自动调用,用于初始化Bean
    • 实现了DisposableBean接口或声明了@PreDestroy注解的方法则在容器关闭时调用,用于资源清理工作
  8. 容器启动完成

    • 当所有的初始化工作完成后,Spring容器即处于就绪状态,可以响应客户端对Bean的请求

3.4 Spring AOP的实现原理。

  Spring AOP即面向切面编程,它是基于动态代理的。Spring AOP基于原对象生成一个代理对象,在目标方法前后插入额外的功能,即切面。SpringAOP实现主要包括切面(一个或多个切点定义和相应的通知,即需要增强功能的具体实现类)、切点(定义通知的位置,即哪些方法需要增强)、通知(在某个连接点执行的行为,增强的功能)、连接点(程序执行中明确的点,即目标执行方法)。

3.5 讲讲Spring事务的传播属性。

Spring事务的传播属性定义了在已有事务环境下执行一个方法时新开启事务的方式和当前事务之间的关系。Spring框架提供了七种不同的事务传播行为:

  1. PROPAGATION_REQUIRED: 这是最常见的事务传播设置。如果当前没有事务,那么就新建一个事务;如果已经存在一个事务,则加入到这个事务中。这是默认的事务传播行为

  2. PROPAGATION_SUPPORTS: 如果当前存在事务,则方法将在该事务内运行;如果当前没有事务,则以非事务方式运行该方法。即支持当前事务的存在,但并不强制创建新的事务

  3. PROPAGATION_MANDATORY必须在一个已存在的事务内部执行。如果当前没有事务,会抛出异常。这种方式确保了业务方法必须在事务环境中执行。

  4. PROPAGATION_REQUIRES_NEW总是新建一个新的事务,并且当前事务(如果有)会被挂起。这意味着即使在嵌套调用中,新方法也会拥有自己的独立事务,它与外部事务互不影响。无论外部方法是否回滚,新事务都会被提交或回滚

  5. PROPAGATION_NOT_SUPPORTED: 执行方法时不支持事务环境,如果当前存在事务,会暂停该事务直到方法执行完毕。即总是以非事务方式执行,即便有父事务存在

  6. PROPAGATION_NEVER严格禁止在事务上下文中执行,如果当前存在事务,则会抛出异常。这种方法必须在无事务的环境中执行。

  7. PROPAGATION_NESTED(仅对部分数据库事务管理器有效): 在现有事务中执行一个嵌套事务,嵌套事务可以独立于外部事务进行提交或回滚,而对外部事务的影响是:如果嵌套事务正常完成(提交),其效果将反映到外部事务中;但如果嵌套事务回滚,外部事务仍可继续进行。

 

3.6 Spring如何管理事务的。

   Spring提供了声明式事务,通过AOP技术实现对事务的管理和控制。以下是Spring管理事务的主要步骤和原理:

1.配置事务管理器

  Spring提供了多种PlatformTransactionManager实现类,如DataSourceTransactionManager、HibernateTransactionManager等,用于与具体的持久化技术进行交互并管理事务

2.使用@Transactional注解

  在Service层或DAO层的方法上添加@Transactional注解,以指示该方法需要在事务中执行。可以指定事务传播行为隔离级别只读属性回滚规则等属性。

3.事务拦截器

  1. Spring通过一个实现了MethodInterceptor接口的TransactionInterceptor类处理带有@Transactional注解的方法调用
  2. 调用被@Transactional注解标记的方法时Spring AOP会织入TransactionInterceptor,并在方法前后分别执行开启事务、提交/回滚事务的操作

4.事务生命周期管理

  • 开启事务:当调用方法前,根据@Transactional注解的配置创建新的事务或者加入到当前已存在的事务中。
  • 提交事务:如果方法执行过程中没有抛出异常,则在方法结束时提交事务。
  • 回滚事务:如果方法执行过程中抛出了未捕获的异常,并且符合@Transactional注解的回滚规则(默认情况下是运行时异常和Error),则事务会被回滚。

5.事务传播行为

  通过设置@Transactional注解的propagation属性,可以定义事务如何在嵌套方法调用之间传播。例如PROPAGATION_REQUIRED表示无论是否存在当前事务都应参与事务;PROPAGATION_REQUIRES_NEW表示总是新建一个事务,并挂起当前事务。

3.7 Spring怎么配置事务(具体说出一些关键的xml 元素)。

  1. 配置事务管理器:在Xml中定义一个实现了PlatformTransactionManager实现类的bean作为事务管理器
  2. 导入tx命名空间并启动事务代理:开启事务注解驱动
  3. 配置事务属性:配置通知、传播级别、隔离级别,回滚机制
  4. 切面编程配置

3.8 说说你对Spring的理解,非单例注入的原理?它的生命周期?循环注入的原理,aop的实现原理,说说aop中的几个术语,它们是怎么相互工作的。

非单例注入原理

  在Spring中,默认情况下大部分bean是单例的,即在整个应用上下文中只有一个实例。然而,如果需要为每次请求创建一个新的bean实例,可以通过@Scope("prototype")注解或者在XML配置中指定bean的作用域为prototype来实现。当一个原型作用域的bean被请求时,Spring容器会每次都生成一个新的bean实例,并且执行相应的初始化逻辑和依赖注入过程。

Bean的生命周期

  • 实例化:通过构造器创建Bean的实例
  • 设置属性值/依赖注入:Spring根据Bean定义信息填充其属性值。
  • 初始化:如果Bean实现了InitializingBean接口或定义了init-method,会在所有必需的属性设置完成后调用初始化方法。
  • 使用:Bean准备就绪后,可以由客户端代码或其它bean通过容器获取并使用。
  • 销毁:在容器关闭或Bean不再需要时,如果Bean实现了DisposableBean接口或定义了destroy-method,则会调用销毁方法进行资源清理。

循环注入原理: 循环依赖是指两个或多个bean之间相互依赖形成闭环Spring通过三级缓存机制处理循环依赖问题,对于构造器注入不支持循环依赖,但对于setter注入可通过“提前暴露引用”的方式解决。具体来说,在Spring容器完成bean实例化但尚未进行属性注入阶段将已经实例化的bean放入到一个专门用于处理循环依赖的早期曝光缓存中,这样在后续注入属性时就可以从这个缓存中拿到依赖项

AOP实现原理: Spring AOP主要基于动态代理(JDK Proxy 或 CGLIB)来实现。其原理如下:

  1. 切点(Pointcut):定义了哪些连接点(如方法调用)会被切面增强。这通常通过正则表达式或自定义注解等方式来标识。
  2. 通知(Advice):包含了切面的具体功能,分为前置通知(Before)、后置通知(AfterReturning)、环绕通知(Around)、异常抛出通知(AfterThrowing)和最终通知(After)等五种类型。
  3. 切面(Aspect):由切点和通知组成,描述了横切关注点的功能模块。
  4. 织入(Weaving):将切面应用到目标对象的过程。在运行时,Spring通过代理模式在调用目标方法前插入相关的通知逻辑,从而实现了对业务逻辑的交叉关注点的分离。

3.9 Springmvc 中DispatcherServlet初始化过程。

  1. 加载和实例化: 当Web应用启动时,容器(如Tomcat、Jetty等)会根据web.xml或Java配置方式中定义的Servlet信息来创建DispatcherServlet实例。通常在Servlet规范下,容器会调用Servlet的默认构造器创建对象,并调用其生命周期方法之一——init()

  2. 初始化父类HttpServletBeanDispatcherServlet继承自HttpServletBean,因此它的初始化流程首先经过父类的初始化。在HttpServletBeaninit()方法中,会调用initServletBean()方法来进行Spring Bean风格的初始化该方法是在FrameworkServlet中实现的,DispatcherServletFrameworkServlet的一个子类

  3. 初始化WebApplicationContext: 在DispatcherServletinitServletBean()方法内部,它会创建或获取一个WebApplicationContext(Web应用上下文)WebApplicationContext是Spring ApplicationContext的一个特殊变体,专门设计用于Web应用程序,可以与ServletContext关联起来。如果通过XML或者Java配置指定了上下文配置文件的位置,那么此时就会加载并解析这些配置文件,构建出完整的Spring Bean容器

  4. 载入配置: SpringMVC框架会读取与DispatcherServlet关联的Spring配置信息,包括<mvc:annotation-driven><context:component-scan>、HandlerMapping、HandlerAdapter等配置,将它们注册到WebApplicationContext中。

  5. 初始化处理器映射器和适配器: 初始化过程中,Spring MVC会自动装配或根据配置加载各种处理器映射器(如RequestMappingHandlerMapping)和处理器适配器(如RequestMappingHandlerAdapter),它们负责将HTTP请求与Controller中的处理方法进行匹配和执行

  6. 初始化拦截器链: 如果有配置的拦截器(Interceptor),则会在初始化阶段被添加到一个全局的拦截器链中,以便在请求处理的过程中被执行

  7. 完成初始化所有的初始化工作完成后DispatcherServlet就处于准备接收并处理HTTP请求的状态

  总结来说,DispatcherServlet的初始化是一个综合了Servlet生命周期管理和Spring IoC容器初始化的过程,最终形成一个能够处理HTTP请求并将其路由至相应控制器进行处理的前端控制器

3.10 netty的线程模型,netty如何基于reactor模型上实现的。

  Netty 是一个高性能的异步事件驱动网络应用框架,其线程模型基于Reactor模式实现。Reactor模式是一种处理大量并发I/O请求的设计模式,通过一个或多个反应器(Reactor)来监听和分发事件给相应的处理器进行处理

在Netty中,Reactor线程模型主要有以下几种形式:

  1. 单线程Reactor模型:

    • 仅使用一个NIO线程作为Selector,负责监听所有Channel上的事件
    • Channel上有新的连接请求、读写事件时,该线程会负责接收并调度到对应的Handler进行处理
    • 这种模型适用于CPU密集型任务且并发量不高的场景,因为所有事件处理都在同一个线程内完成
  2. 多线程Reactor模型(主从Reactor或多线程EventLoopGroup):

    • 使用一个Acceptor线程池(通常只有一个线程)监听新的客户端连接请求
    • 当有新的连接到来时,由Acceptor将新建立的SocketChannel注册到另一个I/O线程池中的某个工作线程上,这个线程(即EventLoop)负责后续的读写事件处理
    • 每个EventLoop有自己的Selector,并与一组Channel关联,负责这些Channel的所有I/O事件
    • 这种模型可以充分利用多核CPU资源提高系统的并行处理能力
  3. 主从Reactor多线程模型:

    • 主Reactor用于接受新的连接请求然后将其分配给从Reactor(也就是I/O线程池)
    • 从Reactor负责各自关联的Channel的I/O事件处理,每个从Reactor线程拥有自己的Selector,并在其上进行轮询和事件处理。
    • 高并发场景下,主从Reactor模型能够有效避免单一Reactor成为性能瓶颈

3.11 为什么选择netty。

  1. 高性能与高并发: Netty基于NIO技术,采用了事件驱动和异步编程模型,能有效地利用系统资源支持高并发连接。通过内存池化零拷贝技术等优化手段,极大地提高了吞吐量和降低了延迟

  2. 线程模型优化: Netty提供了灵活且高效的线程模型,如Reactor模式,可以避免频繁的线程上下文切换带来的开销,保证了系统的可伸缩性和稳定性。

  3. 易于使用和扩展: Netty提供了丰富的API和高度可定制化的组件,开发者可以方便地搭建和扩展网络应用,如Web服务器、游戏服务器、RPC框架等。同时,Netty对各种协议(如HTTP、FTP、WebSocket等)都有很好的支持

  4. 健壮性与稳定性: Netty在网络异常处理、流量控制、超时重传等方面做了精心设计,保证了网络通信的健壮性和稳定性。同时,经过大量生产环境的检验,Netty在长期运行下的表现也非常稳定可靠。

  5. 社区活跃与持续更新: Netty背后有一个活跃的开源社区,不断对其功能进行改进和完善,及时修复bug,同时紧跟行业发展趋势,支持新的协议和特性,保障了其与时俱进的能力。

3.12 什么是TCP粘包,拆包。解决方式是什么。

   TCP是一种面向连接、可靠的基于字节流的传输层协议。但在实际应用中,TCP为了提高传输效率,并不保证每个发送的数据包都能够单独作为一个完整的单位送达接收方,这就可能出现“TCP粘包”和“TCP拆包”现象。

  1. TCP粘包(Packet Coalescing): 当发送端连续发送多个小的数据包时,TCP有可能会合并这些数据包成为一个较大的数据块进行传输接收端一次性接收多个数据包的内容无法区分原本的单个数据包边界。这是因为TCP是以字节流的方式传输数据,而不保证应用层的消息边界

  2. TCP拆包(Packet Fragmentation): 另一方面,如果一个大数据包在网络传输过程中因MTU(Maximum Transmission Unit)限制或其他网络因素被分割成了多个较小的数据包接收端可能会分多次接收到这些数据包,造成原始数据的拆分,这也称为拆包现象。

解决TCP粘包和拆包问题的方式主要有以下几种:应用层固定长度大小区分、每个数据包末尾特定结束符

  1. 协议头设计设计自定义的应用层协议加入明确的数据包长度字段,使得接收端可以根据长度字段正确区分不同的数据包

  2. 消息边界标识: 在每个数据包末尾添加特定的结束符(delimiter),如换行符、特殊字符序列等,接收端通过检测这些结束符来识别一个完整消息的结束

  3. 固定长度消息: 如果应用层消息长度都是固定的,可以直接根据长度来判断每个消息的结束位置。

  4. 消息帧设计: 设计成帧(framing)协议,例如在每个消息前加上消息头,消息头包含消息长度或序列号等信息,这样接收端可以根据消息头来解析出每个独立的消息

  5. 基于协议内容的拆包粘包处理: 在接收端程序中实现逻辑,根据预先约定的协议解析数据,当接收到一定数量的数据后按照协议格式解析出完整的消息

3.13 netty的通讯协议是什么样的。

  netty 并不是一个具体的通讯协议,而是一个用于简化高性能网络编程的 Java 开源框架。它提供了一系列工具和API,使得开发者能够更容易地实现基于各种传输协议的应用程序,比如TCP、UDP、HTTP等等。Netty 支持自定义协议的设计和实现,同时也提供了对多种标准协议的支持

  在Netty中,开发者可以根据自己的业务需求设计和实现自定义的通讯协议。通常情况下,一个自定义的通讯协议会在Netty中表现为一系列的编码(Encoder)和解码(Decoder)器,用于处理网络传输的数据流,确保数据在发送端按照协议进行编码封装,并在接收端正确解码还原

3.14 springmvc用到的注解,作用是什么,原理。

  1. @RequestMapping

    • 作用:用于映射HTTP请求到控制器的方法上。它可以放在类级别上,表示该类中所有处理方法都将共享相同的请求映射规则;也可以放在方法级别上,精确指定处理某个HTTP请求的方法
    • 原理:Spring MVC的DispatcherServlet会根据@RequestMapping注解的属性(如value、method、params、headers等)来决定哪个Controller的方法应该处理接收到的请求
  2. @GetMapping, @PostMapping, @PutMapping, @DeleteMapping

    • 作用:这些注解是@RequestMapping的特殊变体,分别用于映射GET、POST、PUT、DELETE等不同HTTP方法的请求。它们简化了对HTTP方法的指定。
    • 原理:与@RequestMapping一样,根据HTTP方法筛选请求,并映射到相应的处理方法。
  3. @RequestParam

    • 作用:用于将请求参数绑定到控制器方法的形参上,可以从请求的查询参数或POST表单数据中获取值
    • 原理:Spring MVC在解析请求时,会自动查找@RequestParam注解的参数,并从请求中提取对应的值
  4. @PathVariable

    • 作用:用于映射URL模板中的占位符到控制器方法的形参上,常用于RESTful风格的路由。
    • 原理:在解析请求URL时,根据URL模板中的占位符名称与@PathVariable注解的参数名称相匹配,将实际路径参数值传递给方法参数
  5. @RequestBody

    • 作用:用于将HTTP请求体的内容转换并绑定到控制器方法的形参上,主要用于处理JSON、XML等非表单数据的POST、PUT请求
    • 原理:通过HttpMessageConverter接口实现将请求体内容转换为Java对象的过程。
  6. @ResponseBody

    • 作用:用于将方法返回的对象转化为HTTP响应体的内容,通常配合@ControllerAdvice、@RestController等使用,实现REST API的数据返回
    • 原理:通过HttpMessageConverter接口将方法返回的对象序列化为客户端可识别的格式(如JSON、XML等),然后写入到HTTP响应体
  7. @ControllerAdvice

    • 作用:全局异常处理和统一响应处理,可以定义一个类,其中的方法可以处理所有Controller抛出的异常或对所有Controller的响应进行统一格式化处理
    • 原理:Spring MVC框架在执行过程中,如果捕获到符合@ControllerAdvice注解的类中定义的异常处理方法签名匹配的异常,便会调用该方法进行处理
  8. @ModelAttribute

    • 作用:用于表示一个方法或方法参数应该从模型中获取,或者表示一个方法应该往模型中添加一个对象,通常用于处理表单数据和模型数据的绑定
    • 原理:在请求处理过程中,Spring MVC会自动调用标有@ModelAttribute的方法,将返回的对象添加到模型中,然后在后续的处理方法中可通过该注解的参数名称从模型中获取数据。

  这些注解的处理是在Spring MVC的DispatcherServlet处理请求的过程中完成的,涉及到了一系列的处理器映射数据绑定视图渲染等中间环节。Spring MVC通过一系列的拦截器链和策略模式实现了这些注解的功能。

3.15 springboot启动机制。

   SpringBoot的启动机制基于Java内置的Servlet容器(如Tomcat、jetty、Undertow),并通过Spring Framework和自身的自动配置简化Web应用的开发。

1.启动类

  Spring Boot 应用通常会包含一个带有 @SpringBootApplication 注解的主类,这个注解是复合注解,包含了 @SpringBootConfiguration(相当于 @Configuration,表示类是配置类)、@EnableAutoConfiguration(开启自动配置)和 @ComponentScan(组件扫描)注解。

2.引导加载

  当运行主类的 main 方法时,Spring Boot 会启动一个新的 JVM 进程SpringApplication.run(Application.class, args)方法被调用,该方法负责初始化 Spring 上下文并启动应用

3.创建上下文

  • Spring Boot 初始化 SpringApplication 类并创建一个 ApplicationContext(应用上下文)实例,它是 Spring IoC 容器的入口点
  • SpringApplication会根据类路径下的 META-INF/spring.factories 文件中定义的SpringApplicationRunListeners来监听应用启动过程中的事件

4.自动配置

  Spring Boot 通过 EnableAutoConfiguration 注解扫描类路径下的jar包,根据jar包中的 META-INF/spring.factories 文件中的 EnableAutoConfiguration 条目来自动配置 Bean。自动配置的目的是基于应用类路径中的类和 jar 包,推断出应用可能需要的 Bean,并自动创建它们

5.组件扫描

  @ComponentScan 注解用于扫描包含在主类包及其子包下的所有标记了 @Component@Service@Repository@Controller 或 @Configuration 的类,并将它们作为 Bean 加载到 Spring 容器

6.加载额外配置

  Spring Boot 会加载 application.properties 或 application.yml 中的配置信息,并将它们绑定到 Spring 环境(Environment)中,供应用使用

7.启动嵌入式容器

  Spring Boot 会加载 application.properties 或 application.yml 中的配置信息,并将它们绑定到 Spring 环境(Environment)中,供应用使用

8.启动应用

  所有 Bean 都被加载和初始化完成后,Spring Boot 应用就准备好了,嵌入式容器开始监听 HTTP 请求,等待客户端连接

3.16 如何解决循环依赖,Spring的三级缓存如何解决循环依赖,为什么二级缓存不能解决?

Spring中三大循环依赖场景:

  • 构造器注入循环依赖
  • filed属性注入(setter方法注入)循环依赖
  • prototype属性注入循环依赖

原因:

  • 构造器构成的循环依赖无法解决,只能抛出。原因:Spring解决循环依赖是利用Bean的“中间态(已实例化但未初始化)”,而构造器负责的是实例化,故无法解决构造器构成的循环依赖。
  • filed字段注入是最常见的依赖注入,不存在循环依赖问题。
  • 非单例Bean默认不会初始化,而是使用时才会初始化手动 getBean() 或者在一个单例 Bean 内 @Autowired A 或 B 就会出错

 

  1. 一级缓存(singletonObjects):当一个单例bean的完整实例化过程完成(即构造函数调用和所有属性注入完毕),该实例会被放入一级缓存中。

  2. 二级缓存(earlySingletonObjects):在处理循环依赖时,Spring首先尝试从一级缓存获取bean,如果未找到,则会继续查找二级缓存。二级缓存存放的是已实例化但尚未进行属性注入或初始化的“半成品”bean对象引用。如果存在循环依赖,A需要B,而B又需要A,那么在创建B的过程中,Spring会将已实例化但未注入属性的A放入二级缓存中,以便B可以获取到A的引用并继续其自身的初始化。

  3. 三级缓存(singletonFactories):如果在二级缓存中依然没有找到bean,此时就涉及到三级缓存了。三级缓存存储的是能产生对应bean实例的对象工厂(ObjectFactory)。当Spring检测到循环依赖时,它会在实例化A的过程中记录下能够生产A的工厂,并将其放入三级缓存。然后在创建B时,发现对A的依赖,通过三级缓存中的工厂直接生成一个未完全初始化的A实例,再放入二级缓存供B使用。这样,即使在初始化过程中也能打破循环依赖

为何二级缓存不能单独解决循环依赖: 二级缓存虽然能够存储部分初始化的bean引用,但它无法解决初次实例化bean时的循环依赖问题。例如,在A和B同时初始化且相互依赖的情况下,如果先初始化A,在创建B之前A还未能被放入二级缓存中,因为A的初始化过程还在等待B的注入,这就形成了死锁状态。而三级缓存的存在则可以在A实例化阶段,通过存储一个能够立即生产A实例的工厂来突破这一瓶颈,使得即使在A还未完成初始化时,也能向B提供其所需的部分初始化引用,从而成功解决循环依赖问题。

Spring中的“三级缓存”_spring三级缓存-CSDN博客

3.17 事务失效

事务失效是指在事务执行过程中,未能达到预期的ACID(原子性、一致性、隔离性、持久性)属性的情况。以下是几种常见的事务失效情况及其解决方法:

  1. 数据不一致:由于并发控制不当或其他原因导致事务操作后数据库状态不符合业务规则。解决方法是使用恰当的事务隔离级别和并发控制机制(如锁机制),以及在事务中确保所有的业务逻辑都能保证数据的一致性。

  2. 死锁两个或多个事务互相等待对方释放资源,造成循环等待,无法继续执行。解决方法包括设置合理的事务超时时间、采用乐观锁或者悲观锁策略时避免长时间持有不释放,以及通过检测并解除死锁的机制来避免长期僵持。

  3. 部分提交/回滚:在网络问题、系统崩溃等异常情况下,事务可能只完成了一部分操作就中断了。为了解决这个问题,可以采用分布式事务管理技术(如两阶段提交、三阶段提交、补偿事务、Saga模式等),确保事务要么全部成功,要么全部失败。

  4. 脏读、不可重复读、幻读:这是由于不同的事务隔离级别造成的读取数据不准确的问题。针对这些现象,可以通过提高事务隔离级别来解决,例如:

    • 脏读:使用“可重复读”(Repeatable Read)或“串行化”(Serializable)隔离级别。
    • 不可重复读:使用“串行化”隔离级别,或者在某些数据库系统中,“快照隔离”也可以避免不可重复读。
    • 幻读:通常需要使用“串行化”隔离级别才能完全防止。
  5. 系统故障恢复:当系统发生故障时,可能会丢失未提交的事务修改或者已经提交的事务结果没有持久化到磁盘上。解决方法是利用数据库系统的日志(redo log、undo log)进行事务的前滚和回滚,实现故障恢复

4.操作系统

4.1 Linux系统下你关注过哪些内核参数,说说你知道的。

  1. 网络相关参数

    • /proc/sys/net/core/somaxconn设置listen队列的最大长度,影响服务器可以同时处理的连接请求排队数量
    • /proc/sys/net/ipv4/tcp_max_syn_backlog类似于somaxconn,但专门针对TCP SYN队列,影响SYN洪水攻击防护和高并发连接处理
  2. 内存管理参数

    • /proc/sys/vm/swappiness决定系统在内存紧张时交换内存到磁盘的速度,值越大越倾向于使用交换分区
    • /proc/sys/vm/min_free_kbytes系统希望在任何时候都至少保留这么多空闲内存
  3. 文件系统与I/O调度

    • /sys/block/<device>/queue/scheduler设置特定磁盘设备的I/O调度策略,如完全公平排队(CFQ)、noop、deadline等。
    • /proc/sys/fs/inotify/max_user_watches设置单个进程可以监视的最大文件数
  4. 系统行为参数

    • /proc/sys/kernel/pid_max系统可以创建的最大进程ID数量
  5. CPU调度参数

    • /proc/sys/kernel/sched_migration_cost_ns调度器迁移任务到另一个CPU的成本(纳秒),影响负载均衡策略
    • /proc/sys/kernel/sched_child_runs_first控制子进程是否比父进程更优先运行

4.2 Linux下IO模型有几种,各自的含义是什么。

  1. 阻塞IO(Blocking I/O) 在这种模型下,当进程发起一个IO操作(如read或write)时,如果没有数据可供读取或者内核缓冲区满无法立即写入,进程会被阻塞,直到数据准备好或有足够的缓冲空间可以写入。在此期间,进程无法执行其他任何操作

  2. 非阻塞IO(Non-blocking I/O) 在非阻塞IO模型下,进程发起IO操作后,不论数据是否准备好,内核都会立即返回一个状态值。如果数据没有准备好,返回EAGAIN或EWOULDBLOCK错误。进程需要不断地轮询,检查IO操作是否可以继续执行

  3. IO复用(I/O Multiplexing) 最常见的IO复用技术是select、poll和epoll。在这种模型下,一个进程可以同时监控多个文件描述符的状态,一旦有任何描述符变为可读或可写,就会通知进程进行相应的IO操作。这种方式可以让进程在一个系统调用中同时等待多个IO操作的完成,解决了多连接同时处理的问题,但并不会减少IO本身的阻塞问题。

  4. 信号驱动IO(Signal-driven I/O,又称为异步IO) 信号驱动IO允许进程注册一个信号处理函数,当IO操作就绪时,内核会发送一个SIGIO信号通知进程。进程在接收到信号后,在信号处理函数中完成IO操作。这种模型下,进程注册后可以继续做其他事情,直到收到信号为止

  5. 异步IO(Asynchronous I/O,又称AIO) 在异步IO模型中,进程发起IO请求后,无需等待,可以立即返回去做其他工作当IO操作真正完成时,无论成功与否,内核都会通过回调函数通知进程。与信号驱动IO相比,异步IO提供了完成通知,而不仅仅是就绪通知,所以真正的IO操作全过程都是非阻塞的。

4.3 epoll和poll有什么区别。

  poll和epoll都是IO多路复用技术,用于同时监控多个文件描述符的事件,但它们之间存在一些区别:

  • 效率差异:poll每次调用时都需要遍历整个文件描述符集合(即使没有活动);epoll在高并发连接中性能较好,它采用LT和ET两种模式,在内部维护红黑树存储监控的文件描述符,当事件到来,返回就绪的文件描述符,减少无效遍历。
  • 事件通知:epoll采用了事件驱动的回调机制。当文件描述符上有事件发生时,epoll_wait会立刻返回,而且返回的是就绪的文件描述符的具体集合,而poll则返回的是所有发生了事件的文件描述符的数量,但需要应用程序自行遍历所有描述符来找出哪些描述符真正发生了事件
  • 内存占用:epoll在内核中维护了一个事件表,只需要将关注的事件和文件描述符添加到该表中,后续的事件通知过程不需要每次都传递这个大的描述符集合,这节省了内存拷贝开销
  • LT(水平触发)和ET(边缘触发):epoll在内核中维护了一个事件表,只需要将关注的事件和文件描述符添加到该表中,后续的事件通知过程不需要每次都传递这个大的描述符集合,这节省了内存拷贝开销

4.4 平时用到哪些Linux命令。

在日常使用Linux操作系统时,以下是一些经常使用的Linux命令,涵盖了文件管理、系统信息查看、用户管理、进程管理、软件安装与升级等方面:

  1. 文件与目录操作:

    • ls:列出目录内容。
    • cd:切换工作目录。
    • mkdir:创建新目录。
    • touch:创建新文件或更新文件的时间戳。
    • rm:删除文件或目录。
    • mv:移动或重命名文件与目录。
    • cp:复制文件与目录。
    • find:查找文件系统中的文件。
    • catlessmoreheadtail:查看文件内容。
    • ln:创建硬链接或符号链接。
  2. 文件权限与所有权管理:

    • chmod:更改文件或目录的权限。
    • chown:更改文件或目录的所有权。
    • chgrp:更改文件或目录所属的组。
  3. 系统信息查看与管理:

    • uname:显示系统信息。
    • tophtop(需要安装):查看系统资源使用情况(CPU、内存等)。
    • freevmstat:查看内存使用情况。
    • dfdu:查看磁盘空间使用情况。
    • pspgreppkill:查看和管理进程。
    • uptime:查看系统运行时间和负载。
  4. 网络相关:

    • ifconfig(旧版Linux)或 ip addr(新版Linux):查看网络接口信息。
    • ping:测试网络连通性。
    • netstat(旧版Linux)或 ss(新版Linux):查看网络连接状态和端口监听情况。
  5. 软件安装与包管理:

    • 对于Debian/Ubuntu系列:apt 或 apt-get 更新、安装、卸载软件包。
    • 对于Red Hat/CentOS系列:yum 或 dnf 更新、安装、卸载软件包。
    • 对于Arch Linux系列:pacman 更新、安装、卸载软件包。
  6. 脚本与编程相关:

    • bashshzsh:执行shell脚本或进入交互式Shell。
    • nanovimemacs:编辑文本文件。
  7. 日志查看:

    • journalctl(Systemd系统):查看系统和服务日志。
    • tail -f:追踪查看实时滚动的日志文件。

4.5 用一行命令查看文件的最后五行。

在Linux中,你可以使用tail命令查看文件的最后五行。命令如下:

tail -n 5 [filename]

4.6 用一行命令输出正在运行的java进程。

在Linux中,可以通过ps命令结合grepawk命令来查找并输出正在运行的Java进程,命令如下:

ps aux | grep java | grep -v grep | awk '{print $2}'

这条命令的执行过程分解说明如下:

  • ps aux:列出所有用户的进程信息,包括进程ID(PID)、用户、CPU和内存使用情况以及命令行等。
  • grep java:从进程列表中筛选出含有"java"字样的行,这通常代表Java进程
  • grep -v grep排除掉包含"grep java"这一行(即自身的grep命令行)
  • awk '{print $2}'使用awk命令打印出每一行的第二列,即进程ID(PID)

4.7 介绍下你理解的操作系统中线程切换过程。

  操作系统中线程切换过程:保存当前线程上下文,选择下一个线程、加载目标线程上下文、切换硬件上下文、恢复目标线程执行。

4.8 进程和线程的区别。

  进程是资源调度的基本单位,各个进程之间相互独立,进程是程序的一次运行。线程时CPU调度的基本单位,多个线程共享进程资源,线程之间存在一定关联关系。

4.9 top 命令之后有哪些内容,有什么作用。

  top 命令在Linux系统中是一个实时显示系统状态的动态视图工具,它提供了当前系统中进程的详细信息,包括CPU、内存、任务队列等系统资源的使用情况

  • 监控系统性能实时监控系统的CPU、内存、交换空间等资源使用情况
  • 查找资源占用大户:通过排序和查看进程列表,可以迅速定位到占用系统资源最多的进程,便于系统性能优化和故障排查
  • 进程管理:可以根据需要对进程进行终止、挂起、优先级调整等操作

4.10 线上CPU爆高,请问你如何找到问题所在。

  1. 执行top命令,查看所有进程占用系统CPU的排序
  2. 执行top -Hp PID查看进程下所有线程占CPU情况
  3. 由于jvm的进程快照中线程显示是16进制的,所以需要将PID转换为16进制,查看具体CPU爆满信息

5.多线程

5.1 多线程的几种实现方式,什么是线程安全。

  多线程可以通过继承Thread类、实现Runnable接口、实现Callable接口,以及线程池等方式创建多线程。

  线程安全是指多个线程访问类,不会因为多线程同时操作类导致数据不一致或其他错误行为。可通过同步代码块(synchronizes block、synchronized method)、Lock、原子类、JUC工具包解决

5.2 volatile的原理,作用,能代替锁么。

  volatile的作用是保证线程对主内存的可见性、以及防止指令重排。它的原理是通过内存屏障来防止指令之间的重排、保证代码执行的顺序,防止数据出错。

5.3 画一个线程的生命周期状态图。

5.4 sleep和wait的区别。

  • sleep没有释放锁、wait会释放锁
  • sleep是线程暂停执行,wait用于线程交互/通信
  • sleep方法执行后,线程自动苏醒;wait方法调用后,线程需要另一个线程调用notify或notifyAll方法才能苏醒
  • sleep是Thread类的本地方法、wait是Object的本地方法

5.5 sleep和sleep(0)的区别。

  在操作系统中,sleep() 函数通常指的是让当前线程或进程暂停执行一段时间。而 sleep(0) 是调用 sleep() 函数时传入的一个具体参数值,即让线程睡眠时间为0秒。在Java中主要用于提示当前线程放弃CPU,让其他线程执行,是提高并发性能的一种方式

 

5.6 Lock与Synchronized的区别 。

  Lock和Synchoronized都是Java中的线程同步机制。它们在实现方式、灵活性、异常处理、性能和并发、锁粒度上有所区别

复制代码
public synchronized void method(){
    //同步代码块
}

Lock lock = new ReentrantLock();
lock.lock();
try{

}catch(){
    //同步代码块
}finally{
  lock.unlock();  
}
synchronized和ReentrantLock使用
复制代码
  1. 实现方式:synchronized是Java内置的关键字,可直接在方法或代码块上使用;Lock是JUC包下的一个接口(常用的ReentrantLock),是一种可拓展的互斥锁,使用时实例化一个ReentrantLock对象便可加锁解锁。
  2. 灵活性:synchronized适用于简单的互斥场景、Lock可提供公平锁、尝试获取锁、定时锁、可中断锁,控制锁的获取和释放。
  3. 异常处理:synchronized在遇到异常时会自动释放锁,也可能因异常无法释放所。Lock必须在finally中手动调用unlock()方法释放锁,若不释放则会导致死锁。
  4. 性能和开发:两者性能相差不大,Lock在非公平锁时并发性能更高,无竞争情况下锁获取更快
  5. 锁粒度:synchronized关键字锁粒度基于对象,Lock支持读写锁,允许读操作并发执行,写操作互斥。

5.7 synchronized的原理是什么,一般用在什么地方(比如加在静态方法和非静态方法的区别,静态方法和非静态方法同时执行的时候会有影响吗),解释以下名词:重排序,自旋锁,偏向锁,轻

量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁。

  synchronized关键字主要用于线程同步,确保同一时刻只有一个线程访问被保护的方法或代码块。原理是基于对象头中的锁状态和Monitor机制。对象头中的锁状态存储在对象头中的MarkWord中,MarkWord包括GC年龄,锁状态,hashcodeMonitor由Owner、EntryList、WaitSet组成Owner存放正在执行的synchronized(obj)代码块的线程,EntryList存放获取不到对象阻塞的线程、WaitSet存放获取过锁,但因资源不足而等待的线程

执行流程:当线程试图获取一个对象锁时,会先尝试获取对象关联的Monitor,若Monitor未被其它线程持有,则线程获取Monitor进入临界区执行代码,执行完毕后释放Monitor,若被其他线程持有,则在Monitor的EntryList中排队等待。

synchronized标注的地方:

  • synchronized加在静态方法上,锁的是类对象,同一时刻只允许一个线程访问静态同步方法。
  • synchronized加在非静态方法上,锁的是实例对象,一个类可以有多个实例对象,多个实例对象可以独立执行同步的非静态方法,互不影响。
  • 静态方法和非静态方法同时执行互不影响,因为它们持有的锁(类对象锁和实例对象锁)不同

重排序:JMM允许在不改变单线程程序的执行结果前提下,编译器和处理器优化程序性能对指令进行重新排序。多线程环境下,为保证内存可见性,可通过volatile关键字的内存屏障机制限制重排序

自旋锁:JDK1.6引入的一种轻量级锁表现形式。线程在获取锁失败,不会挂起,而是原地循环等待(自旋),短时间内不断尝试获取锁,减少线程上下文切换开销

偏向锁:它会偏向于第一个获取锁的线程后续在该线程再次获取锁时,无需进行CAS操作,从而降低线程间切换的开销。当有其他线程尝试获取锁时偏向锁才会升级为轻量级锁或重量级锁

轻量级锁:在无竞争情况下,通过CAS和自旋获取和释放锁,避免操作系统层面的线程调度,减小锁开销。适用于无竞争和竞争程度较低场景

可重入锁:可称为递归锁,同一线程在外层方法获取锁后,进入内存方法仍然可再次获取锁而不会引起死锁。synchronized关键字支持可重入锁。

公平锁:分配锁时按照请求先后顺序分配锁,反之不按照请求先后顺序分配锁。synchronized时非公平锁,ReentrantLock可指定公平/非公平锁。

乐观锁:总认为发生并发冲突时,获取资源时直接加锁

悲观锁:乐观认为在部分情况下不会发生并发冲突。在访问资源时不加锁,但在更新时会检查此期间资源是否被其他线程修改,若未被修改则更新成功,否则重试。可通过版本号或CAS实现

5.8 用过哪些原子类,他们的原理是什么。

  AtomicInteger(JNI调用底层C++实现的CAS指令实现无锁操作)、AtomicLong、AtomicBoolean、AtomicReference、AtomicStampedReference(可通过时间戳解决ABA问题)、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater和AtomicReferenceFieldUpdater、AtomicMarkableReference等原子类。

原理:这些原子类都依赖于CAS操作或者其他低级别的硬件同步原语来实现无锁并发编程。CAS操作是一个CPU级别的指令,它可以检查内存中的某个值是否与预期相符,如果相符则替换新的值,这个过程是原子性的,不会受到线程上下文切换的影响。由于避免了使用传统的互斥锁(synchronized关键字),因此在高并发环境下,原子类通常能提供更好的性能和可扩展性。

5.9 JUC下研究过哪些并发工具,讲讲原理。

  1. ReentrantLock: ReentrantLock是一个可重入的互斥锁,它提供了比synchronized关键字更灵活的锁机制。原理上,ReentrantLock基于AQS(AbstractQueuedSynchronizer)实现,通过CAS操作和线程挂起/唤醒机制,保证了线程安全的同步。它支持公平锁和非公平锁,以及可中断、不可中断和超时获取锁等功能。

  2. Semaphore: Semaphore是一个信号量工具类,用于控制同时访问特定资源的线程数量。通过计数信号量来限制并发线程数,超过许可数目的线程将会被阻塞。其原理同样是基于AQS,通过调整许可证的数量来控制并发。

  3. CountDownLatch: CountDownLatch允许一个或多个线程等待其他线程完成一组操作后才能继续执行。它通过一个计数器来控制,当计数器为0时,所有等待的线程会被释放。原理上,也是基于AQS,通过countDown()方法递减计数器,当计数器为0时,调用await()方法的线程得以继续执行。

  4. CyclicBarrier: CyclicBarrier允许一组线程等待所有线程都到达某个屏障点(或者说完成某个阶段)后再全部执行下去。当所有线程都调用await()方法到达屏障时,所有线程会被释放并继续执行。CyclicBarrier支持循环使用,当所有线程都到达后,计数器会重置。

  5. ConcurrentHashMap:ConcurrentHashMap是线程安全的哈希表,它通过分段锁(Segment)实现并发控制,后来在Java 8中进行了重构,使用了CAS算法和数组加链表加红黑树的结构,降低了锁的粒度,进一步提高了并发性能。

5.10 用过线程池吗,如果用过,请说明原理,并说说newCache和newFixed有什么区别,构造函数的各个参数的含义是什么,比如coreSize,maxsize等。

  Java中线程池是通过JUC并发包中的ThreadPoolExecutor实现的,它能够有效管理线程的创建和销毁,复用已存在的线程,减少线程创建和销毁的开销,还可控制并发线程数量防止过多线程导致资源浪费

  1. corePoolSize: 核心线程数,线程池中的常驻线程数。当线程池中的线程数目少于corePoolSize时,即使线程池中的线程都处于空闲状态,线程池也会优先创建新线程去处理任务,而不是直接放入队列。

  2. maximumPoolSize: 线程池所能容纳的最大线程数。当线程池中的线程数达到corePoolSize,且工作队列已满时,线程池会继续创建新线程,直到线程数达到maximumPoolSize。

  3. keepAliveTime: 空闲线程存活时间。当线程池中的线程数目超过corePoolSize时,多余的线程会在空闲keepAliveTime时间后被终止,以减少资源消耗。

  4. unit: keepAliveTime参数的时间单位,例如 TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)等。

  5. workQueue: 工作队列,用于存放等待执行的任务。可以选择无界队列(如LinkedBlockingQueue)、有界队列(如ArrayBlockingQueue)或其他类型的工作队列,不同类型的工作队列会影响线程池的工作模式和性能。

  6. threadFactory: 创建新线程的工厂,用于定制线程的创建逻辑,如设置线程的名称、优先级等。

  7. RejectedExecutionHandler: 拒绝策略,当线程池和工作队列均满时,新提交的任务无法被接受时,将调用此拒绝策略处理新任务。例如AbortPolicy(抛出异常)、CallerRunsPolicy(调用者线程自己执行任务)、DiscardPolicy(丢弃任务)、DiscardOldestPolicy(丢弃队列中最老的任务,尝试执行新任务)等。

  newFixedThreadPool创建核心线程数和最大线程数相等且固定数目的线程池,一旦创建,线程池大小不变,超过线程数的任务会被放在队列中等待执行。该线程池适用于任务数目固定且执行时间可预测的场景。

  newCachedThreadPool创建一个可缓存的线程池,核心线程数为0,最大线程数为Integer.MAX_VALUE。线程池中的线程在闲置一段时间(默认60s)就会被回收。当有新任务提交时,线程池会创建新线程或复用空闲线程执行任务,适用于处理大量短生命周期任务的场景

5.11 线程池的关闭方式有几种,各自的区别是什么。

  线程池关闭方式有两种,分别是shutdown()和shutdownNow()方法;

shutdown()方法会停止新任务的提交,但依然会执行完队列中等待的任务,直到所有任务执行完毕;即shutdown()方法调用前提交的任务会继续执行完,shutdown()方法调用后的任务会停止。

shutdownNow()方法会尽快停止所有任务,并返回一个尚未开始执行任务的任务列表。

5.12 假如有一个第三方接口,有很多个线程去调用获取数据,现在规定每秒钟最多有10个线程同时调用它,如何做到?

  可以使用令牌算法或信号量Semaphore控制并发访问

复制代码
public class RateLimiter {
    private final Semaphore semaphore;

    public RateLimiter (int permitsSecond){
        this.semaphore = new Semaphore(permitsSecond);
    }
    
    public void acquirePermission() throws InterruptedException{
        semaphore.acquire();//获取一个令牌,无令牌阻塞
        try{
            excuteTask();
        }finally{
            semaphore.release();
        }
    }
}

public void excuteTask(){
}

public static void main(String[] args){
    RateLimiter rateLimiter = new RateLimiter(10);//每秒最多10个请求
    ScheduledExecutoeService executor = Executors.newScheduledThreadPool(100)
    for(int i = 0; i < 1000; i++){
        Runnable task = () ->{
            try{
                 rateLimiter.acquirePermission()
            }catch(InterruptedException e){
                 Thread.currentThread.interrupted();
            }
        };
            executor.submit(task);
    }
}
Semaphore控制并发访问
复制代码

5.13 spring的controller是单例还是多例,怎么保证并发的安全。

  默认是单例的,因此在该类中定义的非静态实例变量在多个请求间会被共享,可能导致线程安全问题。

解决方法:

  1. 避免在Controller类中使用实例变量
  2. 使用synchronized关键字保证关键临界区资源在同一时刻,只有一个线程访问

5.14 用三个线程按顺序循环打印abc三个字母,比如abcabcabc。

复制代码
public class printABCInorder{
    private final Lock lock = new ReentrantLock();
    private final Condition conditionA = lock.newCondition();
    private final Condition conditionB = lock.newCondition();
    private final Condition conditionC = lock.newCondition();
    private int state = 0;//初始状态打印"a"

    public void printA() throws InterruptedException{
        while(true){
         lock.lock();
         try{
             while(state %3 != 0){
                 conditionA.await();
             }
                 System.out.print("a");
                 state++;
                 conditionB.signal();
            }
        }finally{
             lock.unlock();
        }
    }

    public void printB() throws InterruptedException{
        while(true){
         lock.lock();
         try{
             while(state %3 != 1){
                 conditionB.await();
             }
                 System.out.print("b");
                 state++;
                 conditionC.signal();
            }
        }        }finally{
             lock.unlock();
        }
    }

    public void printC() throws InterruptedException{
        while(true){
         lock.lock();
         try{
             while(state %3 != 2){
                 conditionC.await();
             }
                 System.out.print("c");
                 state = 0;
                 conditionA.signal();
            }
        }        }finally{
             lock.unlock();
        }
    }

    public static void main(String[] args) {
        PrintABCInOrder printer = new PrintABCInOrder();
        for(int i = 0; i< 3;i++){
            new Thread(printer::printA).start();
            new Thread(printer::printB).start();
            new Thread(printer::printC).start();
        }
    }
}
ReentrantLock实现abcabcabc
复制代码

5.15 ThreadLocal用过么,用途是什么,原理是什么,用的时候要注意什么。

  ThreadLocal是一种线程绑定的局部变量工具类,它可为每个线程提供独立的变量副本,各个线程互不影响,能解决多线程环境下的数据共享问题

用途

  • 存储线程私有数据在处理HTTP请求时,每个线程保存一个与当前请求相关的session或Request对象
  • 数据库连接管理:同一个请求生命周期内,为每个线程分配并复用一个数据库连接,而不是每次操作都创建新的连接
  • 日志记录上下文信息:记录每条日志的线程特定上下文信息,使得日志输出能够包含针对当前线程的信息

原理:ThreadLocal内部维护一个Map,键为当前执行线程对象,值为存储的对象

使用注意事项

  1. 内存泄漏:ThreadLocal为每一个线程保留一份线程副本,我们在使用完后需要通过remove()方法移除关联对象
  2. 使用场景:适用于线程内部使用,不适用于跨线程间的数据共享
  3. 线程池中的使用:在线程池中使用时,要清除ThreadLocal变量,防止线程复用,导致不同任务数据交叉污染,可通过Thread.UncaughtExceptionHandler接口来清理线程池中的ThreadLocal

5.16 如果让你实现一个并发安全的链表,你会怎么做。

  为保证链表的并发安全,可以通过synchronized关键字,Lock锁,原子类,COW(写少读多,写时复制整个链表)、无锁数据结构和CAS操作。

复制代码
class ConcurrentLinkedList<T> {
    private final ReentrantLock lock = new ReentrantLock();
    private Node<T>head;
    private static class Node<T>{
        T data;
        Node<T>next;
        Node(T data){
            this.data = data;
        }
    }
    public void add(T data){
        lock.lock();
        try{
            //修改操作
        }finally{
            lock.unlock();
        }
    }
}
ReentrantLock实现
复制代码

5.17 有哪些无锁数据结构,他们实现的原理是什么。

  无锁数据结构是指多线程环境下,读写操作不需要显示获取和释放锁,通过原子操作、CAS机制保证数据的一致性和正确性。

  1. 无锁栈:

    • 实现原理:通常采用链表结构,并结合CAS操作实现节点的入栈(push)和出栈(pop)操作。当尝试修改栈顶指针时,使用CAS检查栈顶是否发生变化,如果未变,则更新栈顶;若已变,则重新尝试。
  2. 无锁队列:

    • Michael-Scott非阻塞队列:使用双端链表和CAS操作,插入和删除操作都在尾部进行,同时保持一个“哑元”节点作为哨兵,以简化边界条件处理。
    • Treiber Stack-Based Queue:基于栈的数据结构实现无锁队列,每个元素是一个指向下一个元素的指针,插入和删除都通过CAS来竞争修改这个指针。
  3. 无锁哈希表:

    • 例如Hazard Pointers或基于CAS的扩展如ConcurrentHashMap (Java):通过划分内部数组为段(segment),每个段都有自己的锁,大部分操作在一个段内是无锁的,部分跨段操作需要加锁。另外,也利用了CAS操作更新桶(bucket)中的链表节点。
  4. 无锁计数器:

    • 基于CAS的原子整型变量,每次更新计数值时都会比较当前值与预期值,只有在两者相等的情况下才会成功更新,否则会一直循环重试直到成功为止。
  5. 无锁链表节点删除:

    • 通过CAS来改变前一个节点的next指针指向待删除节点的后继节点,从而完成节点的删除操作。
  6. 无锁环形缓冲区(Ring Buffer):

    • 利用原子操作更新生产者和消费者索引,确保在多线程环境下安全地进行数据的生产和消费。

  这些无锁数据结构的核心思想是利用现代处理器提供的原子指令集来代替传统的互斥锁,尽量减少甚至消除由于锁竞争导致的性能瓶颈和死锁等问题。但需要注意的是,无锁数据结构的设计和实现相对复杂,且存在ABA问题(即一个值被多次修改后又恢复原值的情况),有时需要引入版本号或其他机制来解决这个问题。

5.18 讲讲java同步机制的wait和notify。

  wait和notify方法都是Object类中的方法,用于线程间通信和同步。

  • wait()方法:在调用wait()方法之前会先去获取对象的监视器(线程占用Monitor的Ower),否则报错IllegalMonitorStateException异常。当一个线程调用wait()方法时,该线程会释放监视器,进入等待状态(Monitor中的WaitSet),直到有一个线程调用noti()或notifyAll()方法唤醒该线程,重新获取监视器,若没有获取到则阻塞(Monitor的EntryList)
  • notify()方法:一个线程调用notify()是为了唤醒另一个调用了wait()方法的线程,使其重新获取对象的监视器。

5.19 CAS机制是什么,如何解决ABA问题。

  CAS是一种无锁算法,CAS包含三个操作数,内存位置V,预期值A和新值B。执行原子操作时,当V的值与预期值相同时,才会将V的值更新为B,反之说明其他线程对V的值进行了修改CAS不会执行任何操作并返回一个是否成功的状态

ABA问题:在多线程环境下,内存位置V上有一个初始值A,线程1准备更新它,但在其更新前,线程2将A修改为了B,线程3又将B修改为了A。这时线程1检查内存位置V的值还是A,CAS操作会认为没有冲突,实际在此期间变量发生了变化

解决方法:版本号

版本号:每次修改值,不仅改变其值,还增加版本号。进行CAS操作时,不仅比较内存位置V与预期值,还要比较版本号是否递增。可通过内部维护一个版本号引用的AtomicStampedRefernce类解决该问题。

复制代码
public class ABATest {
    //初始值为1,版本号为0
    static AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(1, 0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() ->{
            int stamp = stampedRef.getStamp();//获取版本号
            System.out.println("thread1 try to update value 1 to 2");
            stampedRef.compareAndSet(1, 2, stamp, stamp+1);
            if (stampedRef.getReference() == 2) { // 如果成功更改,则输出并休息一段时间
                System.out.println("Thread 1 successfully updated the value to 2.");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            stamp = stampedRef.getStamp();//更新stamp准备下一次比较

        });

        Thread thread2 = new Thread(() ->{
            System.out.println("thread1 try to update value 2 back to 1");
            int currentStamp = stampedRef.getStamp();//获取版本号
            stampedRef.compareAndSet(2, 3, currentStamp, currentStamp+1);
            stampedRef.compareAndSet(3, 1, currentStamp+1, currentStamp+2);
            if (stampedRef.getReference() == 1 && stampedRef.getStamp() > 0) { // 如果成功更改且版本号增加,则输出
                System.out.println("Thread 2 caused an ABA problem but it's detected by version stamp: " +
                        "value is 1 and stamp is " + stampedRef.getStamp());
            }
        });
        thread1.start();
        thread2.start();

        // 等待线程执行
        thread1.join();
        thread2.join();
AtomicStampedRefernce
复制代码

5.20 多线程如果线程挂住了怎么办。

  当多个线程挂起时,可以先定位问题、接着排查问题、然后根据真正问题所在搜寻解决方案。

  • 定位问题:日志分析:检查应用程序日志,是否包含有异常信息、死锁或其他导致线程挂起的情况;堆栈跟踪:通过jstack或者其他类似工具获取线程堆栈信息,查看挂起线程的状态以及它在执行哪个方法或代码块时被阻塞
  • 排查问题:死锁、无限循环、IO操作/网络堵塞、同步对象等待(wait())、外部资源限制
  • 解决方案:修复代码逻辑、设置超时机制、资源管理优化

5.21 countdowlatch和cyclicbarrier的内部原理和用法,以及相互之间的差别

Countdowlatch内部原理与用法:

内部原理: CountDownLatch通过抽象队列同步器AQS(AbstractQueuedSynchronizer)实现,它维护了一个计数器。当构造一个CountDownLatch时,需要传入一个初始的计数值每当调用countDown()方法时,计数器减1;当调用await()方法时,如果计数器值大于0,则当前线程会被阻塞,直到计数器为0为止一旦计数器为0,所有等待的线程都会被唤醒并继续执行

用法:通常用于主线程等待多个子线程完成任务后才继续执行的情况,例如初始化阶段有多个资源需要加载主线程需要等待所有资源加载完毕才能开始后续工作

复制代码
public class CountDownLatchTest {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        for (int i = 0; i <5 ; i++) {
            new Thread(()->{
                latch.countDown();

            }).start();
        }
        latch.await(); // 主线程在此处等待,直到计数器变为0
        System.out.println("所有任务已完成,可以进行下一步操作...");
    }
}
CountDownLatch
复制代码

CyclicBarrier内部原理与用法:

内部原理: CyclicBarrier也基于AQS实现,但它的目标是让一组线程到达某个屏障点时一起执行某项操作,而不是简单的计数到零。每个线程调用await()方法时会阻塞在那里直到所有参与的线程都到达了这个屏障点

用法:当需要一组线程同时达到某个状态或者完成各自的任务后再共同执行下一个任务时,可以使用CyclicBarrier。

复制代码
public class CyclicBarrierTest {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(5, () ->{
            System.out.println("所有线程已到达屏障点,开始执行后续任务...");
        });
        for (int i = 0; i < 5; i++) {
            new Thread(() ->{
                //执行任务
                try {
                    barrier.await();
                }catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
CyclicBarrier
复制代码

差别:

  • 行为差异:CountDownLatch是一次性的,计数器递减到0后就不可重用;而CyclicBarrier是可以循环使用的,当所有线程到达屏障后,计数器自动重置,并且可以指定一个Runnable作为屏障动作,在所有线程到达后运行。
  • 目标不同:CountDownLatch主要用于一个或多个线程等待其他线程完成一系列操作后才能继续执行;CyclicBarrier则是为了实现多个线程间的同步,确保所有线程都达到某一状态后,再集体进行下一步操作

5.22 countdownlatch的await方法和是怎么实现的。

   CountdownLatch中的await()方法被调用时会判断计数器是否为0,如果是则直接返回。反之则调用该方法的线程在AQS维护的队列中等待,直到其它线程调用countdown()方法,计数器中的值为0,AQS才会释放等待的线程,这些线程重新竞争CPU资源继续执行。

await()方法的实现流程:

  • 调用AQS的tryAcquireShared(int arg)方法尝试获取资源,这里arg通常为0,表示需要计数器为0才能成功获取
  • 如果不能立即获取资源(即计数器大于0),则通过AQS的doAcquireSharedInterruptibly(int arg)方法将当前线程加入同步队列并挂起等待
  • 当其他线程调用countDown()导致计数器归零时,AQS会触发相应逻辑,唤醒等待队列中的线程,使其能够从await()方法中返回并继续执行。

5.23 对AbstractQueuedSynchronizer了解多少,讲讲加锁和解锁的流程,独占锁和公平所加锁有什么不同。

   AQS时一种构建所和同步器的组件,其基于FIFO和CAS实现线程的阻塞和唤醒,以及状态管理。

获取锁:

独占式获取锁:当线程调用tryAquire(int args)获取锁时,会通过CAS尝试将AQS内部的状态变量设置为指定值,如果设置成功则成功获取锁;反之则线程会被封装成节点加入同步队列的尾部,并进入等待状态。被阻塞线程会在其他线程释放或特定条件时被唤醒,并再次尝试获取锁

释放锁:线程执行完临界区代码会调用release(int args)释放锁,更新状态变量,并检查是否有现成被唤醒;AQS会选择合适的线程从等待队列中移除并将其转为可运行状态,使其有机会竞争资源。

独占锁与公平锁的区别:

  • 非公平锁(默认): 在非公平锁下,获取锁的操作并不保证按照线程请求锁的顺序进行,即使有其他线程已经在等待队列中,新到达的线程也可能直接尝试获取锁。这样可以减少上下文切换开销,提高系统的吞吐量,但可能导致某些线程长期得不到锁资源,即“饥饿”现象

  • 公平锁: 公平锁在设计上遵循FIFO原则,当锁空闲时,它总是优先分配给等待时间最长的线程。这意味着新到达的线程不会立即获得锁,而是必须等到所有比它更早请求锁的线程都获得过锁之后才轮到它。这种策略有助于避免饥饿问题,但也可能降低系统的整体性能,因为频繁的线程上下文切换会导致更高的开销

  AQS通过维护一个抽象的等待队列和状态变量配合CAS原子操作,实现了高效的线程同步机制,并且可以根据实际需求选择不同的加锁策略(如公平锁与非公平锁)

5.24 使用synchronized修饰静态方法和非静态方法有什么区别。

  synchronized修饰静态方法,锁住的时类对象,synchronized修饰非静态方法锁住的是类的实例对象,两者互不影响,可并发进行。

5.25简述ConcurrentLinkedQueue和LinkedBlockingQueue的用处和不同之处。

ConcurrentLinkedQueue

  • 用途:它是一个无界、非阻塞的并发队列基于链表结构实现,适用于高并发环境下生产者消费者模式,尤其适合处理大量快速入队和出队操作,并且不需要考虑队列满的情况。
  • 特点:
    • 无界:这意味着它可以无限增长,除非系统资源耗尽,否则不会因为队列满而导致插入操作失败。
    • 非阻塞:使用了 CAS(Compare and Swap)等无锁算法来保证线程安全,因此在大多数情况下,添加或移除元素时不会引起线程阻塞,提高了性能。
    • FIFO:遵循先进先出的原则。

LinkedBlockingQueue

  • 用途:同样用于生产和消费场景,但它是有界的、支持阻塞的并发队列,可以根据需要设置队列容量
  • 特点:
    • 有界:可以设定队列的最大容量,当队列已满时,尝试向其中插入元素的线程会阻塞,直到其他线程从队列中移除了元素;同理,当队列为空时,尝试获取元素的线程也会被阻塞,直到有新的元素加入。
    • 阻塞:通过使用ReentrantLock和Condition进行线程同步,实现了插入和删除操作的阻塞等待机制,确保线程间的协调和资源的有效利用。
    • FIFO:同样遵循先进先出的原则。

5.26导致线程死锁的原因?怎么解除线程死锁。

  导致死锁有四种原因:互持资源、请求并等待、不可剥夺、循环等待

解决办法:

  1. 破坏请求并保持状态:一次性申请所有资源
  2. 破坏不可剥夺条件:占有资源的线程申请不到其他资源时,主动释放其占有的资源
  3. 破坏循环等待条件:按需申请资源

5.27 非常多个线程(可能是不同机器),相互之间需要等待协调,才能完成某种工作,问怎么设计这种协调方案。

  1. 分布式锁: 使用分布式锁服务如ZooKeeper、Redis或etcd等实现资源的互斥访问。每个参与任务的线程在执行关键操作前获取锁,完成后释放锁。这样可以确保同一时间只有一个线程(或一个机器上的线程)执行特定的操作

  2. 消息队列/中间件: 利用RabbitMQ、Kafka或其他消息队列系统,让各个线程将任务结果或状态发布到队列中其他线程根据接收到的消息进行下一步操作(发布订阅)。通过持久化和顺序保证特性,消息队列能很好地处理异步协作与顺序依赖问题

  3. 分布式事务/两阶段提交(2PC) /三阶段提交(3PC): 如果是涉及数据库操作的场景,可以考虑使用分布式事务技术来确保多个参与者的一致性

5.28 用过读写锁吗,原理是什么,一般在什么场景下用。

  读写锁时多线程编程中常见的一中线程同步机制,它允许多个线程同时读取共享资源,,只允许同一时间只有一个线程更新共享资源。

  • 读锁(Read Lock):当一个线程获取到读锁后,其他线程仍然可以获取读锁并进行并发读取操作

  • 写锁(Write Lock):一旦某个线程获得了写锁,其他任何线程(包括读线程和写线程)都不能再获得任何锁,直至该写锁被释放。这样就保证了在进行写操作期间不会有读操作或其它写操作发生,确保数据一致性

适用场景:适合读多写少的场景,如缓存系统、数据库读写操作

通过合理地使用读写锁,可以在不牺牲正确性的前提下,最大化地利用CPU和内存资源,提高系统的并发处理能力和吞吐量。

5.29 开启多个线程,如果保证顺序执行,有哪几种实现方式,或者如何保证多个线程都执行完再拿到结果。

  可以使用Thread.join()、Object.wait(),notify()、lock.await()、release()方法都可以实现多线程顺序执行

5.30 延迟队列的实现方式,delayQueue和时间轮算法的异同。

  延迟队列中是一种队列中的元素在特定时候后才能消费的优先队列。Delayed接口定义了一个方法getDelay(TimeUnit unit)用于获取剩余延迟时间,并且每个元素都有一个可比较的到期时间。当尝试从队列中获取元素时,只有已经到期的元素才会被成功取出

时间轮算法(Timing Wheel): 时间轮算法是一种空间换时间的数据结构,用于高效地处理定时任务。它将时间划分为一系列固定的时间间隔(称为槽或桶),形成一个环形数组或链表结构。每一个槽代表一定时间范围内的定时事件。每当时间流逝,指针会顺时针移动到下一个槽,处理该槽内所有到期的任务。

  • 相同点:

    • 都是为了处理具有延迟特性的任务
    • 都可以支持多个线程安全地进行任务的添加和移除
  • 不同点:

    • 数据结构与实现复杂度DelayQueue基于优先队列实现,其内部排序逻辑较为复杂;而时间轮算法通常使用环形数组或者链表,实现相对简单,查找和删除操作有固定的复杂度

    • 效率与资源占用DelayQueue由于采用堆结构,插入和删除元素时可能需要做堆调整,对于大量短生命周期任务可能会有一定性能损耗;时间轮算法则通过“滚动”机制来处理过期事件,对于密集的、周期性调度任务更为高效,特别是小步长的时间间隔,因为可以通过索引直接定位。

    • 内存消耗:时间轮算法可以根据时间间隔大小选择合适的槽位数量,对内存空间有一定的预估和控制能力;而DelayQueue没有固定的内存限制,随着元素增多,其内存消耗也会增加。

    • 扩展性:时间轮算法可以设计为多级时间轮,以支持更长时间跨度的延迟任务;而DelayQueue本身不支持这种层级结构,但可以通过其他手段(如分层处理)实现类似效果。

  DelayQueue适合于处理任意延迟时间的任务,并且无需额外自定义复杂的定时器数据结构;而时间轮算法更适合于处理大量周期性或离散时间间隔的定时任务,在高并发场景下具有更好的性能表现。

6.HTTP与TCP

6.1 http1.0和http1.1有什么区别。

  HTTP1.1相较于HTTP1.0增加了长连接、管道。

  • 长连接:为解决HTTP/1.0发送一次请求,建立一次TCP,因此HTTP/1.1新增了长连接,减少连接重复创建和断开
  • 管道:解决HTTP/1.0在一个TCP连接中每发送一个请求需等待一个响应的问题,HTTP/1.1新增管道,一个TCP中可发送多个请求,不必等其响应,减少整体响应时间。

6.2 TCP三次握手和四次挥手的流程,为什么断开连接要4次,如果握手只有两次,会出现什么。

TCP三次握手

  客户端和服务端双方开始都是CLOSED状态,服务端首先监听某个端口进入LISTEN状态。

  • 客户端随机初始化序号client_isn,然后将其存储在TCP的首部序号中,同时将SYN标记为1,表示SYN报文。接着客户端将SYN报文发送给服务端,表示向服务端发起连接,之后客户端进入SYN-SEND状态
  • 服务端接收到客户端的SYN报文后,随机初始化序列号server_isn然后存储在其TCP的首部序号中,然后将client_isn+1添加到其确认应答号中,同时将SYN和ACK标记为1。接着将报文发送给客户端,进入SYN-RCVD状态。
  • 客户端收到服务端的SYN-ACK报文后,返回一个确认应答号(server_isn+1)给服务端,然后将ACK标志为1。最后将ACK报文发送给服务端,这次连接客户端可携带数据,然后进入ESTABLISHED状态。
  • 服务端收到客户端的应答报文后,也进入ESTABLISHD状态。

三次握手原因无中间状态给客户端防止重复连接,造成重复连接导致的资源浪费同步双方序列号

TCP四次挥手

  • 客户端想断开连接,客户端会发送一个TCP头部的FIN控制位置为1的报文,即FIN报文,然后客户端进入FIN_WAIT1状态。
  • 服务端收到客户端的报文后,发送一个ACK,然后进入CLOSE_WAIT状态。
  • 客户端收到服务端的ACK报文后,进入FIN_WAIT2状态。
  • 等待服务端处理完数据后,发送一个FIN报文,然后进入LAST+ACK状态。
  • 客户端收到服务端的FIN报文后,回复一个ACK报文,然后进入TIME_WAIT状态。
  • 服务端收到客户端的ACK报文后,进入CLOSE状态。服务端完成连接关闭
  • 客户端经过2LSM后,自动进入CLOSE状态。客户端完成连接关闭。

四次挥手原因:服务端需要等待数据的处理和发送,且服务端的FIN报文和ACK报文是分开发送的。

6.3 TIME_WAIT和CLOSE_WAIT的区别。

  TIME_WAIT和CLOSE_WAIT都是TCP四次挥手中的状态,其中TIME_WAIT是客户端的挥手状态,CLOSE_WAIT是服务端的挥手状态。

6.4 说说你知道的几种HTTP响应码,比如200, 302, 404。

  • 2XX,表示响应数据成功,200表示服务端成功响应数据
  • 3XX表示重定向,302表示临时重定向,303表示永久重定向,304表示协商缓存重定向
  • 4XX表示客户端错误,404表示客户端请求路径错误
  • 5XX表示服务端错误

6.5 当你用浏览器打开一个链接(如:)的时候,计算机做了哪些工作步骤。

  URL解析、DNS解析域名、TCP传输、IP传出(计算哪个网卡发出源IP)、网络接口层计算出目标地址的MAC地址,在网卡层将在数据首部添加起始帧,在尾部添加校验和。然后将数据信号转换为电信号在电路中传输,传输到交换机时;交换机首先将电信号转化为数据信号,然后通过数据包中的校验和校验数据是否完整。如果完整则根据路由表将数据传输给路由器。

TCP/IP如何保证可靠性,说说TCP头的结构。

  TCP/IP是依靠TCP协议来保证数据的可靠性的。

TCP头包括四元组(源端口、目标端口、序列号(解决网络包乱序)、确认应答号(解决丢包问题)、首部长度、控制位、窗口大小、校验和、选项)

如何避免浏览器缓存。

如何理解HTTP协议的无状态性。

简述Http请求get和post的区别以及数据包格式。

HTTP有哪些method

简述HTTP请求的报文格式。

HTTP的长连接是什么意思。

HTTPS的加密方式是什么,讲讲整个加密解密流程。

Http和https的三次握手有什么区别。

什么是分块传送。

Session和cookie的区别。

7.架构设计与分布式

 

8.算法

 

9.数据库知识

9.1 数据库隔离级别有哪些,各自的含义是什么,MYSQL默认的隔离级别是是什么。

9.2 什么是幻读。

9.3 MYSQL有哪些存储引擎,各自优缺点。

9.4 高并发下,如何做到安全的修改同一行数据。

9.5 乐观锁和悲观锁是什么,INNODB的标准行级锁有哪2种,解释其含义。

9.6 SQL优化的一般步骤是什么,怎么看执行计划,如何理解其中各个字段的含义。

9.7 数据库会死锁吗,举一个死锁的例子,mysql怎么解决死锁。

9.8 MYsql的索引原理,索引的类型有哪些,如何创建合理的索引,索引如何优化。

9.9 聚集索引和非聚集索引的区别。

9.10 select for update 是什么含义,会锁表还是锁行或是其他。

9.11 为什么要用Btree实现,它是怎么分裂的,什么时候分裂,为什么是平衡的。

9.12 数据库的ACID是什么。

9.13 某个表有近千万数据,CRUD比较慢,如何优化。

9.14 Mysql怎么优化table scan的。

9.15 如何写sql能够有效的使用到复合索引。

9.16 mysql中in 和exists 区别。

9.17 数据库自增主键可能的问题。

9.18 MVCC的含义,如何实现的。

9.19 你做过的项目里遇到分库分表了吗,怎么做的,有用到中间件么,比如sharding jdbc等,他们的原理知道么。

9.20 MYSQL的主从延迟怎么解决。

10 消息队列

10.1 消息队列的使用场景。

10.2 消息的重发,补充策略。

10.3 如何保证消息的有序性。

10.4 用过哪些MQ,和其他mq比较有什么优缺点,MQ的连接是线程安全的吗,你们公司的MQ服务架构怎样的。

10.5 MQ系统的数据如何保证不丢失。

10.6 rabbitmq如何实现集群高可用。

10.7 kafka吞吐量高的原因。

10.8 kafka 和其他消息队列的区别,kafka 主从同步怎么实现。

10.9 利用mq怎么实现最终一致性。

10.10 使用kafka有没有遇到什么问题,怎么解决的。

10.11 MQ有可能发生重复消费,如何避免,如何做到幂等。

10.12 MQ的消息延迟了怎么处理,消息可以设置过期时间么,过期了你们一般怎么处理。

11.缓存

11.1 常见的缓存策略有哪些,如何做到缓存(比如redis)与DB里的数据一致性,你们项目中用到了什么缓存系统,如何设计的。

11.2 如何防止缓存击穿和雪崩。

11.3 缓存数据过期后的更新如何设计。

11.4 redis的list结构相关的操作。

11.5 Redis的数据结构都有哪些。

11.6 Redis的使用要注意什么,讲讲持久化方式,内存设置,集群的应用和优劣势,淘汰策略等。

11.7 redis2和redis3的区别,redis3内部通讯机制。

11.8 当前redis集群有哪些玩法,各自优缺点,场景。

11.9 Memcache的原理,哪些数据适合放在缓存中。

11.10 redis和memcached 的内存管理的区别。

11.11 Redis的并发竞争问题如何解决,了解Redis事务的CAS操作吗。

11.12 Redis的选举算法和流程是怎样的。

11.13 redis的持久化的机制,aof和rdb的区别。

11.14 redis的集群怎么同步的数据的。

11.15 知道哪些redis的优化操作。

11.16 Reids的主从复制机制原理。

11.17 Redis的线程模型是什么。

11.18请思考一个方案,设计一个可以控制缓存总体大小的自动适应的本地缓存。

11.19 如何看待缓存的使用(本地缓存,集中式缓存),简述本地缓存和集中式缓存和优缺点。

11.20 本地缓存在并发使用时的注意事项。

12.搜索

12.1 elasticsearch了解多少,说说你们公司es的集群架构,索引数据大小,分片有多少,以及一些调优手段 。elasticsearch的倒排索引是什么。

12.2 elasticsearch 索引数据多了怎么办,如何调优,部署。

12.3 elasticsearch是如何实现master选举的。

12.4 详细描述一下Elasticsearch索引文档的过程。

12.5 详细描述一下Elasticsearch搜索的过程。

12.6 Elasticsearch在部署时,对Linux的设置有哪些优化方法?

12.7 lucence内部结构是什么。

 参考链接

 Java并发常见面试题总结(上) | JavaGuide(Java面试 + 学习指南)

3.1 HTTP 常见面试题 | 小林coding (xiaolincoding.com)

  • 相同点

    • 都是为了处理具有延迟特性的任务。
    • 都可以支持多个线程安全地进行任务的添加和移除。
  • 不同点

    • 数据结构与实现复杂度DelayQueue基于优先队列实现,其内部排序逻辑较为复杂;而时间轮算法通常使用环形数组或者链表,实现相对简单,查找和删除操作有固定的复杂度。

    • 效率与资源占用DelayQueue由于采用堆结构,插入和删除元素时可能需要做堆调整,对于大量短生命周期任务可能会有一定性能损耗;时间轮算法则通过“滚动”机制来处理过期事件,对于密集的、周期性调度任务更为高效,特别是小步长的时间间隔,因为可以通过索引直接定位。

    • 内存消耗:时间轮算法可以根据时间间隔大小选择合适的槽位数量,对内存空间有一定的预估和控制能力;而DelayQueue没有固定的内存限制,随着元素增多,其内存消耗也会增加。

    • 扩展性:时间轮算法可以设计为多级时间轮,以支持更长时间跨度的延迟任务;而DelayQueue本身不支持这种层级结构,但可以通过其他手段(如分层处理)实现类似效果。

总结来说,DelayQueue适合于处理任意延迟时间的任务,并且无需额外自定义复杂的定时器数据结构;而时间轮算法更适合于处理大量周期性或离散时间间隔的定时任务,在高并发场景下具有更好的性能表现。

posted @   求知律己  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示