文章目录
面试专题-java基础篇
1. 什么是面向对象?谈谈你对面试对象的理解
对比面向过程,是两种不同的处理问题的角度
面向过程更注重事情的每一个步骤及 顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么
比如:洗衣机洗衣服
面向过程会将任务拆解成一系列的步骤 (函数),1 打开洗衣机—>2 放衣---->放洗衣粉---->清洗----->烘干
面向对象会拆出人和洗衣机两个对象: 人:打开洗衣机放衣服放洗衣粉 洗衣机:清洗烘干
从以上例子能看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护
面向对象三大特性
封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项 内部细节对外部调用透明,外部调用无需修改或者关心内部实现
1、javabean的属性私有,提供getset对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决定。而不能由外部胡乱修改,该name有自己的命名规则,明显不能由外部直接赋值
2、ORM框架:操作数据库,我们不需要关心链接是如何建立的、Sq|是如何执行的,只需要引入mybatis,调方法即可
继承:继承基类的方法,并做出自己的改变和/或扩展, 子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的
多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。 父类引用指向子类对象,同时父类无法调用子类特有功能
2. 深拷贝与浅拷贝的理解
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
浅拷贝是指,在拷贝一个对象时,对对象的基本数据类型的成员变量进行拷贝,但对引用类型的成员变量只进行引用的传递,并没有创建一个新的对象,当对引用类型的内容修改会影响被拷贝的对象。
深拷贝是指,在拷贝一个对象时,除了对基本数据类型的成员变量进行拷贝,对引用类型的成员变量进行拷贝时,创建一个新的对象来保存引用类型的成员变量。
3. String、 StringBuffer StringBuilder的区别
-
String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的
-
StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高
4. 接口与抽象类有什么区别
- 定义:
- 接口:接口是一种抽象数据类型,它只定义了一组方法的签名(即方法名称、返回类型和参数列表),但没有方法的实现。接口中的方法默认为抽象方法,不包含具体的代码。
- 抽象类:抽象类是一个类,可以包含抽象方法(即没有方法体的方法),同时也可以包含具体方法。抽象类可以有实例变量,并且可以被继承。
- 多继承:
- 接口:Java支持多继承接口,一个类可以实现多个接口。这使得类能够获得多个不同接口的方法。
- 抽象类:Java不支持多重继承的类,一个类只能继承一个抽象类。
- 构造方法:
- 接口:接口不能包含构造方法,因为它们不能实例化。
- 抽象类:抽象类可以包含构造方法,可以用于初始化对象状态。
- 字段(属性):
- 接口:接口中的字段默认为常量(
public static final
),必须初始化,并且只能包含常量。 - 抽象类:抽象类可以包含实例字段,可以有不同的访问修饰符,可以被子类继承。
- 接口:接口中的字段默认为常量(
- 实现:
- 接口:类通过实现接口来获得接口中定义的方法。类必须提供方法的实现。
- 抽象类:子类通过继承抽象类来获得方法。子类可以选择实现或覆盖抽象方法。
- 用途:
- 接口:适用于定义契约,强制实现多态性,允许类遵循多个不同的契约。
- 抽象类:适用于定义类的基本结构,提供一些通用的方法和数据,允许代码重用。
5. wait与sleep有什么区别?
一个共同点,三个不同点
共同点
wait() 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态
不同点
方法归属不同
sleep(long) 是 Thread 的静态方法 wait()都是 Object 的成员方法,每个对象都有
醒来时机不同
执行 sleep(long) 的线程会在等待相应毫秒后醒来 wait() 可以被 notify 唤醒,wait() 如果不唤醒就一直等下去
它们都可以被打断唤醒
锁特性不同(重点)
wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)
6. 线程池相关问题
6.1. 对线程池的理解
线程池是一种池化技术,其实是一种资源复用思想的利用 常见的比如像线程池,连接池 内存池 对象池等这些
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最 大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
主要特点:线程复用;控制最大并发数:管理线程。
第一:降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
第二:提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进 行统一的分配,调优和监控
6.2. 线程池底层工作原理
第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程
第二步:调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务
第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存
第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务
第五步:如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常
6.3. 如何自定义线程池参数
corePoolSize | 核心线程数目 - 池中会保留的最多线程数 |
---|---|
maximumPoolSize | 最大线程数目 - 核心线程+救急线程的最大数 |
keepAliveTime | 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放 |
unit | 时间单位 - 救急线程的生存时间单位,如秒、毫秒等 |
workQueue | 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务 |
threadFactory | 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等 |
handler | 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略 |
6.4. 阻塞队列
常用的实现类
LinkedBlackingQueue 可以利用碎片化空间提升内存的利用率
ArrayBlackingQueue 空间连续
该参数是实际开发中重点需要调优和动态调整的参数, 队列的大小可以由项目所占内存大小和对应接口的吞吐量决定
6.5. 如何配置合理的线程数
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:一般公式:CPU核数+1个线程的线程池
IO 密集型
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2 I0密集型,即该任务需要大量的I0,即大量的阻塞。在单线程上运行I0密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
I0密集型时,cpu核数 / (1-0.9),核数为4的话,一般设置 40
6.6. 线程池拒绝策略有哪些?
拒绝策略分类 | 含义 | 使用场景 |
---|---|---|
AbortPolicy | 丢弃任务并抛出异常 RejectedExecutionException | 我们项目中关于线程池的定义,使用的就是默认的如果这种需求是关键的业务,eg:商品详情/购物车/首页 |
DiscardPolicy | 安静的丢弃任务但是不抛出异常 | 设计的时候,一些无关紧要的业务可以采用此策略Eg:单纯的展示某一项数据的情况 文章的浏览量/点赞个数 |
DiscardOldestPolicy | 丢弃队列最前面的任务,然后重新提交被拒绝的任务 | 喜新厌旧使用场景不多,可根据特定场景使用 |
CallerRunsPolicy | 由调用线程处理该任务 | 使用场景非常少 |
7. synchronized和Lock锁
8. 死锁问题
8.1 什么是死锁?
死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
8.2. 诱发死锁产生的原因有哪些?
互斥条件
占有且等待
不可抢占
循环等待
8.3 遇到死锁问题如何解决?
死锁一旦发生,其实基本上就很难人为干预解决他,所有我们只能尽可能的规避他 上面提到的四个条件,只要同时满足
了就会触发死锁 所有我们只需打破其中任意一条,死锁自然也就不会存在了 举例说明:
第一条: 互斥条件 这个作为锁所必须的条件,无法干预
第二条: 可以一次性申请所需的所有资源 此时就不存在等待的问题
第三条: 当其中一个线程再去申请资源的时候,如果申请不到,
死锁一旦发生,其实基本上就很难人为干预解决他,所有我们只能尽可能的规避他 上面提到的四个条件,只要同时满足
了就会触发死锁 所有我们只需打破其中任意一条,死锁自然也就不会存在了 举例说明:
第一条: 互斥条件 这个作为锁所必须的条件,无法干预
第二条: 可以一次性申请所需的所有资源 此时就不存在等待的问题
第三条: 当其中一个线程再去申请资源的时候,如果申请不到,
第四条: 给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。采用层次分配策略,将系统中所有的资源排列到不同层次中