以下所有整理内容都是我从第一次面试开始,将所有遇到的问题整合后的结果。所有的内容都是我在面试中真实遇到的问题,有bat这样的大厂,也有很多小厂。在面试超过20家之后,遇到的绝大多数问题都开始重复,这份资料给我的面试带来了非常多的便利。现在我将这份资料分享在这里,希望也能够给你带来一些帮助,加油吧少年!
1. 解释线程和进程以及优缺点
1.1 进程和线程解释
进程是指内存中运行的应用程序,每个进程都有自己独立的一块内存空间。比如在Windows系统中,一个运行的exe文件就是一个进程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。
线程的状态:
1.2 进程与线程的优缺点
1.2.1 进程的优缺点:
优点是进程拥有独立的空间地址,一个进程崩溃后,在保护模式下不会对其他进程产生影响。同时进程提供了多道编程,OS调度下可以并发执行,提高CPU利用率。缺点是同一时间内只能执行一件事,以及如果执行过程中堵塞,整个进程就会挂起
1.2.2 线程的优缺点:
创建的代价小、线程之间的切换需要OS做的操作较少、占用资源少、计算密集型应用可以将计算分到多线程中(如从1加到100)。缺点是虽然线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,
线程的状态:新建、就绪或可运行、运行、阻塞、死亡
1.3 多进程和多线程的比较
两者都可以使用并行机制提升系统的运行效率,区别在于运行时所占的内存分布不同,多线程是共用一套内存的代码块区间;而多进程是各用一套独立的内存区间。
多进程的优点是稳定性好,一个子进程崩溃了,不会影响主进程以及其余进程。基于这个特性,常常会用多进程来实现守护服务器的功能。多进程的不足是创建进程的代价非常大,因为操作系统要给每个进程分配固定的资源,并且操作系统对进程的总数会有一定的限制,若进程过多,操作系统调度都会存在问题,会造成假死状态。
多线程编程的优点是效率较高一些,适用于批处理任务等功能;不足之处在于,任何一个线程崩溃都可能造成整个进程的崩溃,因为它们共享了进程的内存资源池
对于计算密集型的任务,多进程效率会更高一下;而对于IO密集型的任务(比如文件操作,网络爬虫),采用多线程编程效率更高。
2. 线程池
2.1 线程池如何实现
2.1.1 线程池有两个主要作用:
(1)不同请求之间重复利用线程,无需频繁的创建和销毁线程,降低系统开销;
(2)控制线程数量上限,避免创建过多的线程耗尽进程内存空间,同时减少线程上下文切换次数。
2.1.2 线程池由两个核心数据结构组成:
(1)**线程集合(workers):**存放执行任务的线程,是一个HashSet;
(2)**任务等待队列(workQueue):**存放等待线程池调度执行的任务,是一个阻塞式队列BlockingQueue;
2.1.3 线程池有几个核心参数(最简单线程池):
参数 | 用途 | ||
---|---|---|---|
corePoolSize | 核心线程数量:线程池中最少持有的线程数量,线程池中的线程数量没有达到这个数量时,会创建线程补到这个数量,保证线程池中有足够的线程执行任务 | ||
maximumPoolSize | 最大线程数量:线程池中最多持有的线程数量,线程池中的线程数量达到这个数量时,线程池不能再创建线程,确保线程池数量过多耗尽系统资源 | ||
keepAliveTime | 空闲线程最大存活时间:线程池中的线程数量超过corePoolSize时线程的最大空闲时间,空闲时间超过这个时间的线程会回收直到线程数量达到corePoolSize(之后接一个TimeUnit单位) | ||
BlockingQueue | 线程池中的任务队列:维护着等待执行的Runnable对象 当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务。 |
2.1.4 任务执行流程
任务由execute方法提交到线程池中调度,在提交任务时会有下面几种场景:
(1)线程池中线程数量小于corePoolSize,此时任务不会进等待队列,线程池直接创建一个线程Worker执行提交的任务;
(2)线程池中线程数量不小于corePoolSize并且等待队列未满,任务直接添加到等待队列,等待线程池调度执行;
(3)线程池中线程数量不小于corePoolSize但是等待队列已满且线程数量小于maximumPoolSize,线程池会进行扩容新创建一个线程Worker执行提交的任务,新创建的Worker会被添加到线程集合workers中;
(4)等待队列已满并且线程数量已达到maximumPoolSize,这种情况下线程池无法继续执行任务会拒绝任务,执行一个指定的拒接策略。
(5)线程池已关闭,拒绝任务,执行一个指定的拒接策略。
2.2 请求过多时线程池的拒绝机制
当线程集合和等待队列都满时线程无法调度任务,这时线程池会执行一个默认的或使用者指定的拒绝策略。
JDK内置的拒绝策略主要有下面几种:
1)调用线程执行(CallerRunsPolicy),任务被线程池拒绝后,任务会被调用线程执行;
2)终止执行(AbortPolicy),任务被拒绝时,抛出RejectedExecutionException异常报错
3)丢弃任务(DiscardPolicy),任务被直接丢弃,不会抛异常报错;
4)丢失老任务(DiscardOldestPolicy),把等待队列中最老的任务删除,删除后重新提交当前任务。
除了这些内置的拒绝策略,还可以实现RejectedExecutionHandler接口自定义拒绝策略
2.3 关闭线程池
关闭线程池时有两个关键步骤:
(1)修改线程池状态到SHUTDOWN,这时新提交到线程池的任务都会被直接拒绝;
(2)中断线程池中的所有线程,中断任务执行回收线程集合中所有线程。
2.4 按时调度线程池
JDK还内置了延时/定时调度任务的线程池,能够延时/定时执行提交的任务,它和普通线程池实现上的区别是,任务队列使用了定制的阻塞队列
**DelayedWorkQueue,该队列会对添加到队列中的任务按时间排序,**在从队列中拉取任务时,只有队头任务指定的时间超过了延时时间时才会有任务出队,否则会一直等待。
3. 如何保证线程同步
- 通过Object的wait和notify/notifyAll
- 通过Condition的waiat和signal
- 通过一个阻塞队列(ArrayBlockingQueue,放满了就阻塞)
- 通过两个阻塞队列(一个存取一个控制次序)
- 通过SyschronousQueue(没有数据缓冲的BlockingQueue)
- 通过线程池的Callback回调
- 通过同步辅助类CountDownLatch
- 通过同步辅助类CyclicBarrier
有synchronized关键字修饰的方法。 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
可以是同步方法,同步代码块,同步静态方法会锁整个类
SynchronousQueue不同于一般的数据等线程,而是线程等待数据,他是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。
4. 在Java中Executor和Executors的区别?
Executor 接口对象能执行我们的线程任务;
Executors 工具类的不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
ExecutorService 接口继承了Executor接口并进行了扩展,提供了更多的方法,我们能够获得任务执行的状态并且可以获取任务的返回值。
5. 为什么使用Executor框架比使用应用创建和管理线程好?
(1)每次执行任务创建线程 new Thread()比较消耗性能,创建一个线程是比较耗 时、耗资源的。
(2)调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的 创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪,还有线程之间的频繁交替也会消耗很多系统资源。
(3)直接使用 new Thread() 启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
使用 Executor 线程池框架的优点
(1)能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开销。
(2)可有效控制最大并发线程数,提高系统资源使用率,同时避免过多资源竞争。
(3)框架中已经有定时、定期、单线程、并发数控制等功能。
6. 在线程中怎么处理不可捕捉异常?
实现UncaughtExceptionHandler这个接口来捕获抛出的异常。
ublic class ThreadExceptionMain {
public static void main(String[] args) {
Thread thread = new Thread(new ThreadExceptionRun());
thread.setUncaughtExceptionHandler(new ThreadException());// 异常类实现接口
thread.start();
}
}
UncaughtExceptionHandler是JDK5中的新接口,允许我们在每个线程上面都附加一个异常处理器,Thread.UncaughtExceptionHandler.uncaughtException()方法会在线程因未捕捉的异常而在临近死亡时被调用。
7. 生产环境中线程池如何配置线程数?
要看CPU核 主频 内存大小。 是否是跑WEB容器的还是 非WEB容器
同时运行的其他应用和服务有哪些。
这些都是变量。 都会影响 线程数量的配置。 线程的任务是IO密集的还是内存密集的 还是CPU密集的。也会影响。
8. 提交任务时execute和submit方法的区别?
(1)方法execute()没有返回值,而submit()方法可以有返回值(通过Callable和Future接口)
(2)方法execute()在默认情况下异常直接抛出(即打印堆栈信息),不能捕获,但是可以通过自定义ThreadFactory的方式进行捕获(通过setUncaughtExceptionHandler方法设置),而submit()方法在默认的情况下可以捕获异常。
(3)方法execute()提交的未执行的任务可以通过remove(Runnable)方法删除,而submit()提交的任务即使还未执行也不能通过remove(Runnable)方法删除。
(4)execute只能接受Runnable类型的任务。submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null。
(5)execute中抛出异常。
① execute中的是Runnable接口的实现,所以只能使用try、catch来捕获CheckedException,通过实现UncaughtExceptionHande接口处理UncheckedException, 即和普通线程的处理方式完全一致
②submit中抛出异常:不管提交的是Runnable还是Callable类型的任务,如果不对返回值Future调用get()方法,都会吃掉异常