1、HttpServletRequest 如何重复读body?
在标准的HttpServletRequest
中,只能通过一次读取request.getInputStream()
或request.getReader()
来获取请求体的内容。一旦读取完成,就无法再次重复读取。这是因为请求体是一个流式数据,只能读取一次。
但是,如果您想在需要时多次访问请求体的内容,您可以考虑将请求体内容保存到一个临时变量或者使用缓冲区来存储它。下面是一个示例代码,展示了如何将请求体内容保存为字符串:
1 | // 读取请求体内容并保存 |
请注意,将请求体保存为字符串或其他形式的变量可能会导致内存占用增加。因此,如果请求体非常大或者频繁访问请求体内容会对性能产生不利影响,您可能需要重新评估您的需求并考虑其他解决方案。
2、说一下你对 JMM 的理解
JMM(Java内存模型)是Java程序中用来定义多线程并发访问共享内存的规范。它确保了不同线程之间对共享变量的读写操作具有可见性、有序性和原子性。
- 可见性:JMM保证一个线程对共享变量的修改对其他线程是可见的。当一个线程修改了共享变量的值,JMM会将这个修改刷新到主内存中,并让其他线程可以看到最新的值。
- 有序性:JMM通过禁止特定类型的指令重排序,保证了程序执行的顺序一致性。即使在编译器或处理器的优化过程中,指令也不能被乱序执行,从而确保了多线程环境下的正确性。
- 原子性:JMM保证基本数据类型(如int、long等)的读写操作是原子的。即一个线程对该变量的操作要么完全执行,要么完全不执行,不存在中间状态。
JMM通过使用”happens-before”(先行发生)关系来建立多线程之间操作的顺序关系。如果某个操作A happens-before 某个操作B,那么线程执行操作A之前的所有操作都会在执行操作B时完成。
- happens-before关系:JMM中的happens-before关系是一种顺序保证,它规定了在多线程环境下,指令的执行顺序。如果一个操作happens-before另一个操作,那么前一个操作的结果对于后一个操作是可见的。happens-before关系可以由各种操作和同步机制(如volatile、锁、线程的启动和终止等)建立。
作为开发者需要了解JMM的概念和规则,以确保正确地处理多线程并发访问共享数据的问题。这包括使用volatile关键字、synchronized关键字、Locks(锁)、Atomic类(原子类)等来实现线程间的协调与同步,以避免出现竞态条件、死锁等并发问题。同时,合理使用JMM提供的内存屏障指令(如volatile的写-读内存屏障)可以进一步优化并发性能。
3、synchronized 如何实现可重入锁
synchronized关键字在Java中实现了可重入锁(也称为递归锁)。可重入锁允许线程多次获得同一个锁而不会导致死锁。
当一个线程进入一个synchronized方法或代码块时,它会尝试获得该方法或代码块的锁。如果锁没有被其他线程持有,那么该线程将获得锁并继续执行。此时,JVM会记下锁的持有线程和持有计数器。
如果同一个线程再次进入同一个synchronized方法或代码块,它会检查当前线程是否已经持有该锁。如果是,则允许线程继续执行,同时增加持有计数器(+1)。这个过程可以一直嵌套下去。
当线程退出synchronized方法或代码块时,它会将持有计数器减1(-1)。只有当持有计数器减为0时,其他线程才有机会获得这个锁。
这种设计实现了可重入锁的原理。通过持有计数器,线程可以多次获得同一个锁,而不会出现死锁的情况。同时,每个线程都需要在释放锁之前将持有计数器减1,确保锁的正常释放。
需要注意的是,可重入锁是基于线程的,而不是基于方法或代码块的。也就是说,线程可以多次进入同一个synchronized方法或代码块,但不同的线程之间仍然需要竞争锁的获取。此外,可重入锁的机制只适用于同一个锁对象,不同的锁对象之间无法实现可重入性。
以下是一个使用synchronized实现可重入锁的示例代码:
1 | public class ReentrantLockExample { |
4、请详细讲一下ReentrantLock实现原理
ReentrantLock是Java中的一个可重入锁实现,它提供了与synchronized相似的功能,但具有更高的灵活性和可扩展性。下面是ReentrantLock实现原理的详细解释:
- 内部锁机制: ReentrantLock内部使用一个同步器(Sync)来提供锁的功能。在Java 5之前,ReentrantLock使用的是自定义的同步器AbstractQueuedSynchronizer(AQS)。而在Java 5及之后的版本中,ReentrantLock改为使用更高效的Sync实现,即非公平锁NonfairSync或公平锁FairSync。
- 非公平锁与公平锁:
- 非公平锁:指线程获取锁时不考虑其他等待线程的情况,直接尝试获取锁。这种方式可能导致后续等待时间较长的线程一直无法获得锁,从而产生饥饿现象,但是在大多数情况下能够获得更高的吞吐量。
- 公平锁:指线程获取锁时会考虑其他等待线程的情况,遵循先来先服务的原则。这种方式可以防止某些线程一直等待锁而无法获取,但是由于需要更多的线程切换和调度开销,吞吐量通常较低。
- 实现原理: ReentrantLock的Sync同步器内部使用了一个state变量来表示锁状态,当state为0时表示未被锁定,大于0时表示已被某个线程获取并且持有锁。另外,每个线程都有一个记录当前持有锁次数的计数器。
- 获取锁(lock):当一个线程调用lock()方法时,它会尝试对state进行CAS(比较交换)操作从而将其置为1。如果CAS成功,表示该线程成功获得了锁,并将持有计数器加1。如果CAS失败,则可能存在其他线程持有锁,这时当前线程会进入等待队列中。
- 释放锁(unlock):当一个线程调用unlock()方法时,它会将持有计数器减1。如果计数器变为0,说明当前线程已经完全释放了锁,并将state重置为0,表示锁已释放。此时,等待队列中的其他线程有机会竞争锁。
- 可重入性: ReentrantLock实现了可重入锁的特性,允许同一个线程多次获得同一个锁而不会导致死锁。这是通过持有计数器和线程标识符来实现的。当一个线程再次获取锁时,会检查当前的锁持有者是否为自己,如果是,则计数器加1,否则需要竞争锁。
ReentrantLock提供了更多的功能和灵活性,例如可定时的、可中断的锁获取、公平锁与非公平锁的选择等。但相对于synchronized,使用ReentrantLock需要显式地手动获取和释放锁,并且需要在finally块中释放锁,以确保在任何情况下都能正确释放锁资源,防止死锁的发生。
下面是一个使用ReentrantLock实现可重入锁的示例代码:
1 | import java.util.concurrent.locks.ReentrantLock; |
5、AQS 阻塞队列有长度限制吗?
在AQS(AbstractQueuedSynchronizer 抽象排队同步器)的阻塞队列中,是没有固定长度限制的。AQS本身并不提供阻塞队列的实现,但它是许多并发类的基础,如ReentrantLock、Semaphore、CountDownLatch等。
对于具体的阻塞队列实现类(如ArrayBlockingQueue、LinkedBlockingQueue等),它们可能会限制队列的容量或长度,但这与AQS本身无关。这些实现类一般会提供一个构造方法来指定队列的容量限制。
例如,ArrayBlockingQueue是一个基于数组的有界阻塞队列,其容量由构造方法中的参数指定。只能存储固定数量的元素,一旦队列满了,后续的插入操作将会被阻塞。而LinkedBlockingQueue则是一个基于链表的可选有界或无界阻塞队列,如果创建时没有指定容量,则默认为无界队列。
在使用阻塞队列时,我们可以根据具体的需求选择适合的阻塞队列实现,有界队列可以帮助我们控制队列的长度,而无界队列则可以灵活地处理不确定数量的任务。不同的实现类会有不同的特点和适用场景,我们可以根据具体情况进行选择和使用。
6、ReentrantLock 支持公平锁吗,怎么实现的?
是的,ReentrantLock支持公平锁。公平锁是指多个线程按照申请锁的顺序来获取锁的机制,即先到先得。
在ReentrantLock中,可以通过构造方法来创建公平锁或非公平锁,默认情况下是非公平锁。要创建公平锁,可以将fair参数设置为true。
示例代码如下所示:
1 | import java.util.concurrent.locks.ReentrantLock; |
在上述示例中,我们使用ReentrantLock创建了一个公平锁,通过设置构造方法中的fair参数为true。然后创建了5个Worker线程,每个线程都会尝试获取锁并输出相应的信息。
如果是公平锁,则Worker线程会按照先到先得的顺序依次获取到锁,输出的结果可能是:
1 | Worker 1 is trying to acquire the lock. |
如果是非公平锁,则线程获取锁的顺序是不确定的。
需要注意的是,实际运行中,公平锁可能会导致性能下降,因为需要维护一个有序队列来记录线程的申请顺序。因此,在选择公平锁还是非公平锁时,需要根据具体场景和需求进行权衡和选择。
7、请说一下公平锁和非公平锁的适用场景有哪些?
公平锁和非公平锁都有各自适用的场景,如:
- 公平锁适用场景:
- 当系统对线程的执行顺序有严格要求时,即按照线程申请锁的先后顺序进行处理。
- 在高并发环境下,避免某些线程长时间被饿死(始终得不到锁),确保每个线程公平地获取资源。
- 对于一些对锁竞争不是特别激烈的情况,可以使用公平锁提高代码的可读性和可维护性。
- 非公平锁适用场景:
- 当对性能要求较高且对线程的执行顺序没有强制要求时,可以使用非公平锁。
- 在某些场景下,当一个线程频繁地申请锁并且持有锁的时间较长时,非公平锁可能会提供更好的吞吐量。
- 当已经知道在特定场景下非公平锁可以提供更好的性能,并且确认由于应用特性或负载模式造成的可能优势时,可以选择非公平锁。
需要注意的是,非公平锁并不是不考虑线程的执行顺序,而是在锁可用时,会尝试直接获取锁,只有在锁不可用时才进入等待队列。而公平锁则会先检查是否有前面排队的线程,如果有则进入等待队列,保证了线程按照申请的先后顺序获得锁。
在实际应用中,我们需要根据具体的业务场景和性能需求来选择合适的锁类型。如果对执行顺序有严格要求或者考虑公平性的问题,可以选择公平锁;如果追求更高的吞吐量或者确定非公平锁在特定场景下可以带来优势,可以选择非公平锁。
8、说一下你对方法区的理解
方法区(Method Area)是Java虚拟机(JVM)的一个重要组成部分,它是线程共享的内存区域之一。在Java 8及之前的版本中,方法区是虚拟机规范的一部分。从Java 8开始,方法区被移除,并由元空间(Metaspace)取代。
方法区主要用于存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它的生命周期与虚拟机的生命周期保持一致,即虚拟机启动时被创建,虚拟机关闭时被销毁。
在方法区中,主要包含以下内容:
- 类信息:方法区保存了每个类的完整结构信息,包括类的字段、方法、父类、接口以及常量池等。
- 运行时常量池:方法区中的运行时常量池是每个类的常量池的运行时表示形式,包含了编译期生成的各种字面量和符号引用。
- 静态变量:方法区中存储了类的静态变量,它们在类加载时被创建,存在于整个生命周期中。
- 即时编译器编译后的代码:在方法区中,即时编译器将字节码编译为机器码,并将编译后的代码存储在方法区供后续使用。
需要注意的是,在Java 8及之前的版本中,方法区的大小是通过-Xmx和-XX:MaxPermSize参数进行设置的。而在Java 8及以后的版本中,方法区由元空间(Metaspace)取代,并且元空间的大小默认不受限制,可根据需要动态分配。
总结来说,方法区是用于存储类信息、常量、静态变量和即时编译器生成的代码等数据的内存区域。它在虚拟机启动时被创建,在虚拟机关闭时被销毁。
补充:
- 字面量(Literal): 字面量是指在源代码中直接出现的常量值。在编译期间,Java编译器会将源代码中的字面量转换为对应的字节码表示。常见的字面量包括整数、浮点数、字符、字符串、布尔值等。例如,源代码中的”Hello World”字符串字面量会在编译时被转换为相应的字节码指令。
- 符号引用(Symbol Reference): 符号引用是指在Java源代码中使用的类、方法、字段等的符号名称。在编译期间,编译器会将这些符号引用转化为符号引用表来描述,并在链接阶段进一步解析为直接引用。符号引用包括类的全限定名、方法的签名、字段的名称等。例如,当源代码中使用到一个类或者方法时,编译器会将其表示为符号引用,待链接时再通过符号引用表找到对应的直接引用。
9、说一下Java对象的生命周期
Java对象的生命周期可以分为以下几个阶段:
- 创建阶段(Creation): 在创建阶段,通过关键字
new
或者其他实例化方式(如反射、对象池等),在堆内存中为对象分配内存空间,并调用对象的构造方法进行初始化。在这个阶段中,对象已经被创建,但还没有被完全初始化。 - 使用阶段(Usage): 在使用阶段,对象被引用并用于程序的各种操作。此时,可以通过对象的引用调用其方法、访问其属性等。
- 不可达阶段(Unreachable): 当对象不再被任何引用所指向时,进入不可达阶段。对象可能因为引用被置为null、超出了引用的作用域、或者引用被重新赋值而变得不可达。在此阶段,对象仍然存在于堆内存中,但无法通过任何途径访问。
- 垃圾收集阶段(Garbage Collection): 一旦对象进入不可达阶段,Java虚拟机的垃圾收集器将扫描内存,检测并回收这些不可达的对象。垃圾收集过程会释放对象占用的内存空间,并可能执行一些清理操作,如执行finalize()方法、关闭文件句柄等。
- 回收阶段(Finalization): 在垃圾收集阶段中,可能会对不可达对象执行finalize()方法。finalize()方法是对象的一个特殊方法,用于在对象被垃圾收集之前进行清理工作。但需要注意的是,finalize()方法的执行并不能保证实时性和可靠性。
- 内存回收阶段(Memory Reclaim): 在垃圾收集完成后,空闲的内存将被回收,并可以供给未来的对象创建和分配使用。
需要注意的是,Java的内存管理是自动的,开发人员无需显式地释放对象占用的内存,而是由垃圾收集器负责管理和回收不再使用的对象。对象的生命周期由Java虚拟机自动进行管理,开发人员主要关注对象的创建、使用和适时地释放引用。
总结来说,Java对象的生命周期包括创建、使用、不可达、垃圾收集、回收等阶段。通过自动的垃圾收集机制,Java虚拟机负责管理对象的内存分配和回收,开发人员可以专注于对象的使用和逻辑处理。
10、说一下Spring Bean的生命周期
第一种理解:
Bean的生命周期概括起来有四个阶段:
实例化 -> 属性赋值 -> 初始化 -> 销毁
1、实例化
实例化一个Bean,即new。
2、IOC依赖注入
按照Spring上下文对实例化的Bean进行配置。
3、setBeanName实现
如果这个Bean已经实现了BeanNameAware接口,就会调用它实现的setBeanName(String)方法,此处传递的是Spring配置文件中Bean的id值。
4、setBeanFactory实现
如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(BeanFactory)方法,传递的是Spring工厂自身。
5、setApplicationContext实现
如果这个Bean实现了ApplicationContextAware接口,会调用它实现的setApplicationContext(ApplicationContext)方法,传递的是Spring的上下文。
6、postProcessBeforeInitialization实现-初始化预处理
如果这个Bean实现了BeanPostProcessor接口,将会调用它实现的postProcessBeforeInitialization(Object obj,String s)方法。BeanPostProcessor被用作Bean内容修改,并且由于这个是在Bean初始化结束的时候调用的这个方法,也可以被用于内存或缓存技术。
7、init-method
如果这个bean在Spring配置文件中配置了init-method属性,会自动调用其配置的初始化方法。或则@Bean(initMethod=”…”)指定的方法
8、postProcessAfterInitialization
如果这个Bean实现了BeanPostProcessor接口,将会调用它实现的postProcessAfterInitialization(Object obj,String s)方法。
以上工作完成以后就可以应用这个Bean了。
9、Destory过期自动清理
当Bean不再需要时,如果这个Bean实现了DisposableBean这个接口,会调用其实现的.destory()方法。
10、destory-method
如果这个bean在Spring配置文件中配置了destory-method属性,会自动调用其配置的销毁方法。或者@Bean(destroyMethod=”…”)指定的方法
第二种理解:
一、Spring Bean的生命周期总共分为4个阶段
一阶段:Bean的实例化和DI(dependency injection)
二阶段:检查Spring Awareness
三阶段:创建bean生命周期回调
四阶段:销毁bean生命周期回调
二、4个阶段
1、Bean的实例化和DI(dependency injection)
1.1 扫描XML文件、注释类(例:@Component)、配置类中bean的定义(@Configuration -> @Bean)
1.2 创建Bean实例
1.3 注入Bean依赖项(调用setter或构造方法,为自动装配字段设置值)
2、 检查Spring Awareness(以下只是几个例子)
2.1 如果Bean实现了BeanNameAware接口,则调用setBeanName(…);
2.2 如果Bean实现了BeanClassLoaderAware接口,则调用setBeanClassLoader(…);
2.3 如果Bean实现了ApplicationContextAware接口,则调用setApplicationContext(…);
Aware接口用于告知Spring容器某个Bean需要某些特殊的处理,从而让Spring容器在创建Bean的过程中对它进行处理。
ApplicationContextAware:让Bean实现这个接口可以让它获得一个ApplicationContext实例。
BeanFactoryAware:让Bean实现这个接口可以让它获得一个BeanFactory实例。
BeanNameAware:让Bean实现这个接口可以让它获得它自己在容器中的名字。
ServletContextAware:让Bean实现这个接口可以让它获得一个ServletContext实例。
ResourceLoaderAware:让Bean实现这个接口可以让它获得一个ResourceLoader实例。
3、创建Bean生命周期回调
3.1 @PostConstruct注释,注释回调的方法上,1、2阶段Bean创建完毕即调用;
3.2 实现InitializingBean接口,调用afterPropertiesSet(…),1、2阶段Bean创建完毕即调用;
3.3 Bean定义中包含init-method(在XML中标签
4、销毁bean生命周期回调
4.1 @PreDestroy注释,注释回调方法上,销毁Bean之前调用;
4.2 实现DisposableBean接口,调用destroy(…),销毁Bean之前调用;
4.3 Bean定义中包含destroy-method(在XML中标签
11、对象何时被回收,哪些对象可以作为 GC Roots 根节点?
当对象在Java中不再被引用时,它将成为垃圾收集(Garbage Collection)的候选对象。具体来说,以下情况下对象可能被回收:
- 引用计数法:
- 如果使用引用计数法进行垃圾收集,当对象的引用计数变为0时,即没有任何活动引用指向该对象时,对象将被回收。(补充:引用计数无法解决循环引用的问题,例如循环依赖,BeanA依赖BeanB,BeanB又依赖BeanA,导致BeanA和BeanB引用计数一直为1无法为0而一直无法被gc视为垃圾而回收掉)
- 可达性分析法:
- Java使用可达性分析法来判断对象是否可被回收。通过GC Roots作为起点,遍历对象引用链,如果对象不可达(无法从GC Roots到达),则认为该对象是不可达的,即可被回收。
GC Roots包括以下几种类型的对象:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象:活动线程的栈帧中保存了局部变量,如果局部变量引用了某个对象,那么该对象就被视为GC Roots。
- 方法区中类静态属性引用的对象:所有被类的静态字段引用的对象都被视为GC Roots。
- 方法区中常量引用的对象:常量池中的常量引用的对象也是GC Roots。
- 本地方法栈中JNI(Java Native Interface)引用的对象:JNI是Java与其他本地语言进行交互的接口,在JNI代码中使用的对象也是GC Roots。
除了以上GC Roots的对象,其他所有对象都可能被判定为不可达而被回收。
需要注意的是,Java垃圾收集器并不保证会立即回收所有不可达对象,而是在合适的时机进行垃圾回收。具体的回收策略和时机是由垃圾收集器决定的。
12、请简单介绍下JVM有哪些垃圾回收算法?
JVM(Java虚拟机)中常见的垃圾回收算法包括以下几种:
- 标记-清除算法(Mark and Sweep):首先标记出所有活动对象,然后清除掉未被标记的对象。该算法容易产生内存碎片。
- 复制算法(Copying):将可用内存分为两块,每次只使用其中一块。当当前块内存使用满后,将活动对象复制到另一块未使用的内存上,并清理之前的内存。该算法消耗较大的内存空间。
- 标记-整理算法(标记-压缩算法)(Mark and Compact):首先标记出所有活动对象,然后将这些对象向一端移动,然后清理掉端边界以外的全部内存。该算法可以解决内存碎片问题。
- 分代收集算法(Generational):根据对象的生命周期将堆分为多个区域,通常分为新生代和老年代。新生代中的对象生命周期较短,采用复制算法进行回收;老年代中的对象生命周期较长,采用标记-清除或标记-整理算法进行回收。
这些垃圾回收算法具有不同的特点和应用场景,并根据具体情况选择合适的算法进行垃圾回收,以优化内存的使用和程序的性能。