快手 Java 面经实操
讲一下java内存区域
Java的内存主要分为以下几个区域:
堆(Heap):存放对象实例的地方,包括新生代(Eden区,From Survivor区,To Survivor区)和老年代。方法区(Method Area):存储类的结构信息如运行时常量池,字段和方法数据等。虚拟机栈(VM Stack):存储每个线程的执行信息,包括局部变量、操作数栈、动态链接和方法退出信息。本地方法栈(Native Method Stack):为虚拟机使用到的Native方法服务。程序计数器(Program Counter Register):指示当前线程正在执行的字节码指令。
什么情况下新建的对象不存储在eden中?
在Java中,新创建的对象通常是被存储在Eden区(堆内存的一部分)中的。然而,有两种情况下,新建的对象不会被存储在Eden区:
大对象直接进入老年代:所谓的"大对象"是指需要大量连续内存空间的Java对象。例如,很长的字符串或者大的数组。由于Eden区和Survivor区可能无法存放这样大的对象,所以大对象在新建时直接被分配到老年代中。长期存活的对象进入老年代:Java的垃圾收集器假设大部分对象都是"朝生暮死"的,所以采取了分代收集的方法。当Eden区中的对象在Minor GC(小型垃圾收集)后仍然存活,且年龄(被Minor GC清理的次数)达到一定值(默认15)时,这些对象会被移动到老年代中。这个年龄阈值可以通过-XX:MaxTenuringThreshold参数进行设置。
讲一下线程池
线程池(Thread Pool)是一种基于池化的思想管理线程的工具,可复用存在的线程,减少对象创建、消亡的开销,响应速度更快。线程池在Java中主要通过java.util.concurrent.ExecutorService接口和它的实现类,如ThreadPoolExecutor和ScheduledThreadPoolExecutor来实现。
线程池的主要工作机制是:
创建线程:在没有任何线程或者池中的线程都处于活动状态时,新的任务来临会创建新的线程处理任务。线程复用:创建的线程会被复用,即执行完任务后,不会被立即销毁,而是可以用来继续执行其他任务。线程销毁:线程空闲时间超过某一阈值后会被销毁,直到线程池中的线程数等于corePoolSize。如果允许设置allowCoreThreadTimeOut,则会直到线程池中的线程数为0。
线程池的主要参数包括:
corePoolSize:线程池的基本大小。maximumPoolSize:线程池最大线程数,这个参数会根据你的资源情况来配置。在tasks突然增加,并且超过corePoolSize,线程池便会创建新的线程来处理任务,直到达到maximumPoolSize。然后任务会被放入block queue中。keepAliveTime:线程空闲时间超过keepAliveTime,线程会退出,直到线程数量等于corePoolSize。如果设置allowCoreThreadTimeOut,则线程池中的线程数可以减少到0。unit:keepAliveTime的时间单位。workQueue:任务的阻塞队列,用于存放还未处理的任务,一种缓冲机制。
线程池的主要优点:
重复利用存在的线程,减少线程创建和销毁造成的消耗。能有效控制最大并发线程数,提高系统资源的使用率,避免过多资源竞争,避免阻塞。能够对线程进行简单的管理并提供定时执行、周期执行、单线程、并发数控制等功能。
场景题:一瞬间提交140个请求,核心线程数为40,最大线程数为100,请求最大延迟为5ms,平均rt为3ms,如何设计线程池参数降低处理延迟?
在这个场景下,我们的目标是尽量降低请求的处理延迟。为了实现这个目标,我们需要确保线程池有足够的线程来处理这些请求,并尽量减少请求在队列中等待的时间。
首先,考虑到我们一瞬间会收到140个请求,而最大线程数为100,这意味着至少有40个请求会被放入队列等待。因此,我们需要确保队列的容量至少为40。
其次,我们可以考虑提高核心线程数。核心线程数是线程池中始终存活的线程数,即使没有任务处理,这些线程也不会被回收。增加核心线程数可以让更多的请求立即得到处理,从而降低延迟。然而,这也可能会增加系统的负载,因此需要根据系统的实际情况来决定是否这样做。
最后,我们需要设置一个合适的keepAliveTime。由于我们的请求都是一瞬间到达的,而处理一个请求的平均时间为3ms,因此,我们可以设置一个稍大于3ms的keepAliveTime,这样可以保证线程在处理完一个请求后不会立即被回收,而是可以用来处理下一个请求。
ThreadLocal原理。
ThreadLocal是Java中的一种线程封闭机制,它可以为每个线程都创建一个独立的变量副本,使得每个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。ThreadLocal对于实现线程的局部变量以及线程安全十分有用。
下面是ThreadLocal的基本工作原理:
内部存储:每个Thread都有一个ThreadLocalMap类型的成员变量threadLocals,这个变量是ThreadLocal的内部类。ThreadLocalMap的键为ThreadLocal对象,值为线程局部变量。也就是说,ThreadLocal本身并不存储值,它只是作为一个key来帮助线程从ThreadLocalMap获取一个线程局部变量。get()方法:当调用ThreadLocal的get()方法时,实际上是去调用Thread.currentThread().getThreadLocalMap().get(this)。这个操作就是获取当前线程的threadLocals成员变量,然后以当前ThreadLocal实例作为key,去获取线程局部变量。set()方法:当调用ThreadLocal的set()方法时,实际上是去调用Thread.currentThread().getThreadLocalMap().set(this, value)。这个操作就是获取当前线程的threadLocals成员变量,然后以当前ThreadLocal实例作为key,和将要存储的值作为value,存入ThreadLocalMap中。初始值与null:ThreadLocal提供了一个initialValue()方法,用于设置ThreadLocal变量的初始值,默认返回null。当线程首次调用get()方法时,如果没有先调用set()方法,那么返回的就是这个初始值(默认为null)。内存泄漏问题:ThreadLocal可能会导致内存泄漏。因为ThreadLocalMap的生命周期是跟Thread相同的,只有线程结束后,ThreadLocalMap才会被回收。如果长生命周期的Thread持有ThreadLocal,那么就需要特别注意清理ThreadLocal,因为大量不被使用的ThreadLocal可能会占有大量内存,甚至导致OOM。
讲一下mysql中的锁。
在MySQL中,有两种主要的锁类型:共享锁(Shared Locks)和排他锁(Exclusive Locks)。除此之外,MySQL还支持多种其他类型的锁,如意向锁(Intention Locks)、记录锁(Record Locks)、间隙锁(Gap Locks)等。这些锁主要被用于解决数据库中的并发问题,以实现数据的一致性和完整性。
共享锁(S): 又称为读锁,它允许一个事务去读一行数据。其他事务可以同时读取这行数据,但任何事务都不能够写入这行数据。只要这个锁存在,其他事务就不能获得对这行数据的排他锁。排他锁(X): 又称为写锁,它允许一个事务去读取和写入一行数据,并且阻止其他事务取得该行数据的任何锁。意向锁: 这是一种表级别的锁,主要是为了在一个事务需要获取一个表中某几行的排他锁时,优先检查是否有其他事务持有对该表的意向共享锁或意向排他锁。记录锁: 这是一种行级别的锁,MySQL通过给索引项加锁来对记录进行加锁,防止多个事务同时修改同一行数据。间隙锁: 这是InnoDB中特有的一种锁定机制,主要是为了防止幻读的出现。间隙锁锁定的是两个索引之间的间隙,而不是锁定记录本身。
什么情况下读数据会出现死锁?
在数据库中,死锁是指两个或者更多数量的事务在执行过程中,由于竞争资源而造成的一种互相等待的现象,若无外力作用,这些事务都将无法向前推进。死锁不仅可以在写操作中发生,也可以在读操作中发生。
以下是一个读操作可能导致死锁的例子:
假设我们有两个事务,事务A和事务B。它们都在尝试读取一些共享资源,比如表中的某些行。
事务A开始运行,并对行1加上共享锁(读锁),准备对其进行读取操作。事务B开始运行,并对行2加上共享锁,准备对其进行读取操作。接着,事务A想要读取行2,因此试图对行2加读锁。但是,因为行2已经被事务B锁定,所以事务A必须等待事务B释放它的锁。与此同时,事务B想要读取行1,因此试图对行1加读锁。但是,因为行1已经被事务A锁定,所以事务B必须等待事务A释放它的锁。
这就导致了死锁。事务A在等待事务B释放行2的锁,而事务B在等待事务A释放行1的锁。两个事务都在等待对方释放资源,但都不愿意先释放自己持有的资源,因此都无法继续执行,形成了死锁。
#晒一晒我的offer##快手信息集散地#解决职场真实面试问题,分享同学真实成功案例,欢迎订阅关注!