一 面试题
1.1 java中的Lock
👨💻面试官:java底层中锁是怎么实现的lock -----AQS
🙋 我 :这个我之前看过一点源码,Lock是一个类,在java.util.concurrent.locks
包下面,通过一个AQS的框架实现。AQS我之前读过一点注释,它的
核心思想主要是:如果一个的资源被请求,如果是空闲的就把当前的请求资源的线程设置为有效的工作线程,并且将共享资源锁定,如果共享资源被占用,就把这个线程加入一个CLH
双向队列中,等这个资源被释放。
判断资源是否空闲,在源码中用int类型的变量state
表示同步状态,除了有get和set方法对它进行修改,还有一个CAS(compareAndSetState(int expect, int update) )
方法负责原子的对state进行修改,
常用的ReentrantLock
和jvm中的synchronized
一样采用的是资源独占的方式,一次只能让一个线程执行。但是jvm的同步锁是非公平锁,在ReentrantLock
中定义了一个类Sync
继承AQS,然后定义了两个类NonfairSync
和FairSync
继承Sync
,然后可以自己选择是用非公平锁还是公平锁。
1.2 线程池的创建方式
👨💻面试官:线程池的创建方式,自己一般用哪种
🙋 我 :
我一般使用ThreadPoolExecutor
构造函数自定义参数创建,有几个重要参数
corePoolSize
:核心线程数,最小可以同时运行的线程数量maximumPoolSize
:最大线程数,当等待队列满时,将核心线程数变为最大线程数workQueue
:等待队列容量keepAliveTime
:等待时间
使用.execute(worker)
方法把任务提交到线程池。执行过程:
比如要执行10个任务,配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只有5的任务同时执行,剩下5的任务会放到等待队列中,如果当5个任务有任务被执行完了,线程池就会去拿新的任务执行。
四种拒绝策略
1.3 垃圾回收机制
👨💻面试官:说说垃圾回收机制吧
🙋 我 :
现在一般都采用分代收集算法,在java堆中一般分为新生代和老年代,新生代中分为三个区:Eden区、From和To区
内存分配与回收策略
-
优先在eden区分配:新对象的分配会优先在新生代的
Eden
区进行,当空间快满时,进行第一次Minor GC
,Eden区的存活对象会移动到Survivor From
中,第二次Minor GC
将Eden
和From
区中的存活对象移动到Survivor To
区中,From
和To
进行角色互换,始终保证To区
是空的。 -
**大对象直接进入老年代:**如果创建的对象较大,如分配一个字符串、数组,就直接把这个大对象直接放到老年代,不用经过新生代。
因为在新生代的GC中,如果大对象存活,我们还需要将它复制,为它分配内存,这样复制来复制去效率就会降低。
-
空间分配担保:确保在
Minor GC
之前老年代本身还有容纳新生代所有对象的剩余空间。如果发现之前young GC
的平均晋升大小比目前的老年代剩余的空间大,就会触发full GC
然后因为新生代中,每次收集都会有大量对象死去,所以用的是”标记-复制“算法,只用付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以必须用“标记-清除”或“标记-整理”算法进行垃圾收集。
二 详解
2.1 线程池的好处
- 降低资源消耗。它可以重复利用已创建的线程
- **提高响应速度。**当有任务来的时候,不需要等线程创建就可以执行
- 提高线程的可管理性
2.1 线程池的饱和策略
如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor
定义一些策略:
ThreadPoolExecutor.AbortPolicy
: 抛出RejectedExecutionException
来拒绝新任务的处理。ThreadPoolExecutor.CallerRunsPolicy
: 调用执行自己的线程运行任务,也就是直接在调用execute
方法的线程中运行(run
)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。ThreadPoolExecutor.DiscardPolicy
: 不处理新任务,直接丢弃掉。ThreadPoolExecutor.DiscardOldestPolicy
: 此策略将丢弃最早的未处理的任务请求。
举个例子: Spring 通过 ThreadPoolTaskExecutor
或者我们直接通过 ThreadPoolExecutor
的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler
饱和策略的话来配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy
。在默认情况下,ThreadPoolExecutor
将抛出 RejectedExecutionException
来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy
。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 ThreadPoolExecutor
的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)
2.2 AQS原理(结合源码)
ReentrantLock:
所有Lock接口的操作都委派到一个Sync类上,这个类继承了AbstractQueuedSynchronizer:
abstract static class Sync extends AbstractQueuedSynchronizer
Sync又有两个子类:
final static class NonfairSync extends Sync //非公平锁
final static class FairSync extends Sync //公平锁
默认情况下为非公平锁。
AQS原理
核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
int定义了一个变量state表示同步状态,使用CAS(compareAndSetState)对该同步状态进行原子操作实现对值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
state通过protected类型的getState(),setState(),compareAndSetState()
//返回同步状态的当前值
protected final int getState() {
return state;
}
//设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS有两种定义资源共享的方式。
2.3 判断对象是否存活
- 引用计数法
- 利用空间的一部分来进行计数
- 如果有地方引用这个对象,计数器就加一
- 引用失效就减一,计数为0时就回收这个对象
- 缺点:很难解决对象之间循环引用问题
- 可达性分析算法
- 从一个称为"GC Roots"的对象作为起始结点,从这个结点开始根据引用关系向下搜索,搜索走过的路径称为引用链,引用链到达不了的节点说明该对象不可用,需要被回收
- 可以作为
GC Roots
的对象有:- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象