1. 创建线程的几种方式:
- 继承
Thread
类并重写run
方法,实现简单但不可以继承其他类。
Thread thread_1 = new Thread();
thread_1.start();
- 实现
Runnable
接口并重写run
方法,避免了单继承的局限性,可以实现解耦。
Thread thread_2 = new Thread(new Runnable() {
@Override
public void run(){
}
});
- 实现
Callable
接口并重写call
方法,可以获取线程执行结果的返回值并抛出异常。
FutureTask<String> task = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
return null;
}
});
Thread thread_3 = new Thread(task);
thread_3.start();
- 通过线程池创建(使用
java.util.concurrent.Excutor接口
)
ExcutorService es = Excutors.newFixedThreadPool(1);
es.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
es.shutdown();
2. 线程状态及其切换
线程的状态分为以下几种:
- New 新建
指的是线程被创建但未启动的阶段
- Runnable 可运行
指的是线程可能正在运行,也可能正在等待CPU时间片
- Blocking 阻塞
正在等待获取一个排它锁,当其余线程释放了锁就会结束此状态
- Waiting 无限期等待
等待其他线程显式地唤醒,否则不会被分配CPU时间片
- TimedWaiting 有限期等待
无需等待其余线程显式唤醒,在一定时间后会被系统自动唤醒
- Terminated 死亡
可能是线程结束任务之后自己结束,也可能是因产生了异常而结束
3. start()与run()的区别
- 通过
start()
来启动线程可以实现多线程运行,无需等待线程中run方法体中的代码执行完毕就可以继续执行后续的代码,但在使用start()方法来启动线程时,线程处于的是就绪态 run()
方法被成为线程体,其中包含了要执行的线程的具体内容,通过run()方法启动线程时,线程会进入运行态,执行run()方法中的内容。当run()方法执行结束后,线程终止,CPU再去调度其余线程
4. Java中的线程调度算法
采用的是抢占式。当一个线程执行结束后,操作系统会根据线程的优先级、饥饿情况等数据算出一个总的优先级并分配时间片给优先级最高的线程执行
5. 为什么要采用线程池
线程的创建与销毁是一个极其销毁资源的过程,而Java线程依赖于内核线程,创建线程需要进行操作系统状态的切换,为了避免过度的资源浪费,需要想办法重用线程执行多个任务,也就是线程池。
线程池主要控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动任务,如果线程数量超过了最大线程数,多余的线程会排队等候,直到有线程空闲。其主要的特点是:线程复用、控制最大并发数、管理线程。
采用线程池的优点是:
- 重用存在的线程,减少了线程创建与销毁的开销,提高性能
- 提高响应速度,任务到达时无需等待线程创建就可以立即执行
- 提高线程的可管理性,统一对线程进行分配、调优与监控
6. 线程池的工作原理/流程
当线程池被创建时,其中没有任何线程,任务队列作为参数传入,此时即使任务队列中有任务线程池也不会立即执行。当调用execute()
方法添加一个任务时,线程池会进行判断,如果当前正在运行的线程数小于corePoolSize
,则会立即创建一个核心线程执行任务;如果当前正在运行的线程数大于等于corePoolSize
,则任务会被置入等待队列;如果等待队列已满,且正在运行的线程数小于maximumPoolSize
,则会创建一个非核心线程立即执行任务;如果等待队列已满且正在运行的线程数大于等于maximumPoolSize
,则线程池会抛出RejectExecutionException
。当一个线程完成任务时,它会从等待队列中取出下一个任务来执行。当一个线程持续处于空闲状态的时间达到了keepAliveTime
时,如果当前正在运行的线程数大于corePoolSize
,则该线程会被立即销毁,直到线程池的大小缩减到maximumPoolSize
。
7. 线程池的重要参数
代码语言:javascript复制public ThreadPoolExecutor( int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 线程池的核心线程数
- maximumPoolSize 线程池包含核心线程在内的最大线程数
- keepAliveTime 非核心线程的最大空闲时间
- unit keepAliveTime的单位
- workQueue 线程池所采用的缓冲队列,用于存放被提交但尚未执行的线程
- threadFactory 线程池的工厂,用于创建线程
- handler 线程池的拒绝策略
8. 创建线程池的几种方式
newSingleThreadExecutor
:只会创建一个线程用于执行任务的线程池newFixedThreadPool
:可以重用固定线程数的线程池newCachedThreadPool
:可以根据需要调整线程数量的线程池newScheduledThreadPool
:继承自ThreadPoolExecutor
,等待给定延迟时间后执行任务
注:应当尽量避免使用Executors
创建线程池,而是采用ThreadPoolExecutor
的方式创建。Executors
返回的线程池对象允许的队列长度是Integer.MAX_VALUE
,可能堆积大量请求,导致OOM。
推荐方式:
new ThreadPoolExecutor
- Spring中的
ThreadPoolTaskExecutor
9. 线程池的拒绝策略
- ThreadPoolExecutor.AbortPolicy:丢弃当前任务提交的线程,同时终止当前任务的执行并抛出
RejectedExecutionException
- ThreadPoolExecutor.CallerRunsPolicy:只要线程池没有关闭,就将被提交的任务转交给提交该任务的线程执行
- ThreadPoolExecutor.DiscardPolicy:直接丢弃被提交的任务,同时不会有其余任何的处理
- ThreadPoolExecutor.DiscardOldestPolicy:将等待队列最前端,也就是优先级最高的任务丢弃,然后将该任务插入到等待队列的队尾
10. 线程池的状态
- RUNNING 当前线程池可以接收新任务,可以对已提交的任务进行处理。线程池被创建后的初始状态就是RUNNING
- SHUTDOWN 当前线程池拒绝接收新任务,但仍然可以处理已提交的任务。状态切换:调用
shutdown()
接口 - STOP 当前线程池拒绝接收新任务,不处理已添加的任务同时停止正在处理的所有任务。状态切换:调用
shutdownNow()
接口 - TIDYING 所有任务已终止,ctl记录的任务数量为0,线程池就会处于TIDYING状态。此时,会执行钩子函数
terminated()
。状态切换:线程池处于SHUTDOWN状态,阻塞队列为空且线程池中无正在执行的任务;或者线程池处于STOP状态,正在执行的任务为空。两种情况都会使线程池变为TIDYING状态 - TERMINATED 线程池彻底终止。状态切换:线程池处于TIDYING状态,并且已经执行完了钩子函数
terminated()
11. 线程池被创建后其中是否有线程?如果没有,如何进行预热?
线程池被创建后如果没有任务传递就不会有线程存在。预热的方法分为两种:
- 全部启动:调用
prestartAllCoreThreads()
方法 - 启动一个:调用
prestartCoreThread()
方法
12. 核心线程与非核心线程的销毁机制
- 对于核心线程而言,其默认不回被销毁,但可以通过调用
allowCoreThreadTimedOut(boolean value)
方法来指定核心线程可以按照非核心线程的方法被销毁 - 对而非核心线程而言,当其持续空闲时间达到了
keepAliveTime
时就会被自动销毁
13. 线程池抛出异常后,会怎么处理?
当线程池中线程能够在执行任务时出现了未被捕获的异常,线程池会将提交该任务的线程进行销毁,然后创建一个新的线程加入到线程池中,也可以通过ThreadFactory自定义线程来捕获异常,但无论是否捕获或者处理异常,提交抛出异常任务的线程都会被销毁
14. submit与execute的区别
- submit的参数可以是
Runnable
与Callable
,而execute的参数只能是Runnable
- submit有返回值,而execute没有返回值
- submit还可以对异常进行处理
15. shutdown()与shutdownNow()的区别
- shutdown() 关闭线程池,线程池状态变为SHUTDOWN。此时线程池不再接收新任务但仍可以处理已提交的任务
- shutdownNow() 关闭线程池,线程池的状态变为STOP。此时线程池不再接收新任务,终止正在运行的任务,停止处理等待队列中的任务并返回正在等待执行的List
16. 线程池如何重用线程?
- 当Thread的run方法执行完一个任务后,会循环的从等待队列中取出任务来执行,这样线程就不会被立即销毁
- 当工作线程数小于核心线程数,空闲的核心线程尝试从等待队列中获取任务时,队列中Runnbale的任务数量为0,则该核心线程会被阻塞,从而阻止线程的回收
17. synchronized的实现原理
在java中,每一个对象都隐式的包含有一个montor
对象,加锁的过程其实就是竞争monitor
的过程。当线程进入到字节码文件中的monitorenter
指令时,线程将持有该monitor
对象,只有在执行出monitorexit
时才会释放掉monitor
对象,而当其余线程在没有获取到monitor
对象时会被阻塞
18. synchronized的作用范围
- 作用于方法,会对方法中的对象实例加锁
- 作用于静态方法,会锁住整个class实例,相当与一个类的全局锁,即会锁住所有调用该方法的线程
- 作用于对象实例,会锁住所有以该对象为锁的代码块
19. synchronized与volatile的区别
- volatile关键字是线程同步的轻量级实现,因而volatile的性能要优于synchronized
- volatile只能用于变量而synchronized还可以修饰方法与代码块
- volatile可以保证变量在多线程之间的可见星,synchronized还可以保证多线程访问资源时的原子性
20. synchronized与lock的区别
- synchronized是java中的关键字,是jvm级别的;lock是一个接口,因而是api级别的
- synchronized会自动释放掉线程所持有的锁;而lock必须调用
unlock()
方法才能释放 - synchronized会有死锁的问题;而lock中有trylock方法,故不会产生死锁
- synchronized无法判断锁的状态;而lock可以
- synchronized可重入,不可中断,是非公平锁;而lock可重入,可中断,也可以实现公平锁
- synchronized适用于少量同步的情况;lock适用于大量同步阶段