21 线程池管理线程原理
这道题想考察什么?
是否了解线程池相关的理论知识
考察的知识点
- 线程中的基本概念,线程的生命周期
- 线程池的原理
- 常见的几种线程池的特点以及各自的应用场景
考生应该如何回答
在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
线程池的创建
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize :核心线程数量,默认情况下,线程池会一直维护corePoolSize个线程,让这些线程不会被回收。
- maximumPoolSize:线程池最大线程的数量;
- keepAliveTime:线程的最长闲置时间,若线程闲置超过此时间则回收;
- util:闲置时间单位;
- workQueue:等待队列,当线程池中执行的任务超过核心线程数后,新提交任务将加入此队列等待执行;
- threadFactory:创建线程的线程工厂
- handler:拒绝策略,在任务满了之后,如何处理继续添加的任务。
线程池的执行流程
在新建的线程池中,默认最开始里面是没有线程的。当然,可以使用prestartAllCoreThreads
方法,来提前把corePoolSize的核心线程。
向线程池中提交任务时,首先判断线程池中正在执行的线程数是否已经达到核心线程数:
- 未达到:创建新线程执行任务
- 达到:将任务添加进入任务队列
在向任务队列添加任务时,可能添加成功,也可能添加失败:
- 成功:等待其他线程执行完成,将会自动从队列中获取任务继续执行;
- 失败:判断当前线程池线程数是否达到最大线程数
若添加队列失败,在判断是否达到最大线程数是也可能存在两种结果:
- 达到:回调
RejectedExecutionHandler
拒绝策略,JDK提供了四种拒绝策略处理类:AbortPolicy(抛出一个异常,默认的),DiscardPolicy(直接丢弃任务),DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池),CallerRunsPolicy(交给线程池调用所在的线程进行处理) - 未达到:创建新线程执行任务
核心线程
默认情况下,线程池会一直维护核心线程,使其不被回收。
可以使用
allowCoreThreadTimeOut(true)
设置核心线程也会被闲置回收。
线程池运行线程执行任务:
while (task != null || (task = getTask()) != null) {
//......
task.run();
}
其中getTask
则会从队列中获取待执行任务,不断执行。如果当前队列中没有待执行任务,非核心线程会等待指定的keepAliveTime时间到达后正常退出线程,而核心线程会通过任务队列的take
方法阻塞,从而保证其持续运行。
Runnable r = timed ?
//非核心线程
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
//核心线程
workQueue.take();
22 线程池有几种实现方式,线程池的七大参数有哪些? (美团)
这道题想考察什么?
是否了解线程池的七大参数有哪些与真实场景使用,是否熟悉线程池的七大参数有哪些
考察的知识点
线程池的七大参数有哪些的概念在项目中使用与基本知识
考生应该如何回答
七大参数
-
corePoolSize
线程池中的常驻核心线程数
-
maximumPoolSize
线程池能够容纳同时执行的最大线程数,此值必须大于等于1
-
keepAliveTime
空闲线程的存活时间。
-
unit
keepAliveTime的单位
-
workQueue
任务队列,被提交但尚未被执行的任务
-
threadFactory
表示生成线程池中工作线程的线程工厂,用于创建线程一般默认即可
-
handler:
拒绝策略,表示当队列满了并且工作线程大于等于线程池最大线程数(maximumPoolSize)时如何处理
实现方式
使用JDK中自带的线程池可以通过创建ThreadPoolExecutor线程池对象,也能够通过Executors中定义的静态方法。其中Executors静态方法创建的线程池主要有以下类型:
1.newSingleThreadExecutor
创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
适用:一个任务一个任务执行的场景
2.newCachedThreadPool
当有新任务到来,则插入到SynchronousQueue中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁。
适用:执行很多短期异步的场景
3.newFixedThreadPool
创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列),但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
适用:长期执行的场景
4.NewScheduledThreadPool
创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列中,这是一种按照超时时间排序的队列结构
适用:周期性执行的场景
23 如何开启一个线程,开启大量线程会有什么问题,如何优化?(美团)
这道题想考察什么?
是否了解开启大量线程会有什么问题与真实场景使用,是否熟悉开启大量线程会有什么问题?
考察的知识点
开启大量线程会有什么问题的概念在项目中使用与基本知识
考生应该如何回答
如何开启一个线程
如何开启一个线程,再JDK中的说明为:
/**
* ...
* There are two ways to create a new thread of execution. One is to
* declare a class to be a subclass of <code>Thread</code>.
* The other way to create a thread is to declare a class that
* implements the <code>Runnable</code> interface.
* ....
*/
public class Thread implements Runnable{
}
Thread源码的类描述中有这样一段,翻译一下,只有两种方法去创建一个执行线程,一种是声明一个Thread的子类,另一种是创建一个类去实现Runnable接口。
继承Thread类
public class ThreadUnitTest {
@Test
public void testThread() {
//创建MyThread实例
MyThread myThread = new MyThread();
//调用线程start的方法,进入可执行状态
myThread.start();
}
//继承Thread类,重写内部run方法
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("test MyThread run");
}
}
}
实现Runnable接口
public class ThreadUnitTest {
@Test
public void testRunnable() {
//创建MyRunnable实例,这其实只是一个任务,并不是线程
MyRunnable myRunnable = new MyRunnable();
//交给线程去执行
new Thread(myRunnable).start();
}
//实现Runnable接口,并实现内部run方法
static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("test MyRunnable run");
}
}
}
实现Callable
其实实现Callback接口创建线程的方式,归根到底就是Runnable方式,只不过它是在Runnable的基础上又增加了一些能力,例如取消任务执行等。
public class ThreadUnitTest {
@Test
public void testCallable() {
//创建MyCallable实例,需要与FutureTask结合使用
MyCallable myCallable = new MyCallable();
//创建FutureTask,与Runnable一样,也只能算是个任务
FutureTask<String> futureTask = new FutureTask<>(myCallable);
//交给线程去执行
new Thread(futureTask).start();
try {
//get方法获取任务返回值,该方法是阻塞的
String result = futureTask.get();
System.out.println(result);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//实现Callable接口,并实现call方法,不同之处是该方法有返回值
static class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(10000);
return "test MyCallable run";
}
}
}
Callable的方式必须与FutureTask结合使用,我们看看FutureTask的继承关系:
//FutureTask实现了RunnableFuture接口
public class FutureTask<V> implements RunnableFuture<V> {
}
//RunnableFuture接口继承Runnable和Future接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
开启大量线程会引起什么问题
在Java中,调用Thread的start方法后,该线程即置为就绪状态,等待CPU的调度。这个流程里有两个关注点需要去理解。
start内部怎样开启线程的?看看start方法是怎么实现的。
// Thread类的start方法
public synchronized void start() {
// 一系列状态检查
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
//调用start0方法,真正启动java线程的地方
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
//start0方法是一个native方法
private native void start0();
JVM中,native方法与java方法存在一个映射关系,Java中的start0对应c层的JVM_StartThread方法,我们继续看一下:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;
bool throw_illegal_thread_state = false;
{
MutexLocker mu(Threads_lock);
// 判断Java线程是否已经启动,如果已经启动过,则会抛异常。
if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
throw_illegal_thread_state = true;
} else {
//如果没有启动过,走到这里else分支,去创建线程
//分配c++线程结构并创建native线程
jlong size =
java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
size_t sz = size > 0 ? (size_t) size : 0;
//注意这里new JavaThread
native_thread = new JavaThread(&thread_entry, sz);
if (native_thread->osthread() != NULL) {
native_thread->prepare(jthread);
}
}
}
......
Thread::start(native_thread);
走到这里发现,Java层已经过渡到native层,但远远还没结束:
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
Thread()
{
initialize();
_jni_attach_state = _not_attaching_via_jni;
set_entry_point(entry_point);
os::ThreadType thr_type = os::java_thread;
thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
os::java_thread;
//根据平台,调用create_thread,创建真正的内核线程
os::create_thread(this, thr_type, stack_sz);
}
bool os::create_thread(Thread* thread, ThreadType thr_type,
size_t req_stack_size) {
......
pthread_t tid;
//利用pthread_create()来创建线程
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
......
return true;
}
pthread_create方法,第三个参数表示启动这个线程后要执行的方法的入口,第四个参数表示要给这个方法传入的参数:
static void *thread_native_entry(Thread *thread) {
......
//thread_native_entry方法的最下面的run方法,这个thread就是上面传递下来的参数,也就是JavaThread
thread->run();
......
return 0;
}
终于开始执行run方法了:
//thread.cpp类
void JavaThread::run() {
......
//调用内部thread_main_inner
thread_main_inner();
}
void JavaThread::thread_main_inner() {
if (!this->has_pending_exception() &&
!java_lang_Thread::is_stillborn(this->threadObj())) {
{
ResourceMark rm(this);
this->set_native_thread_name(this->get_thread_name());
}
HandleMark hm(this);
//注意:内部通过JavaCalls模块,调用了Java线程要执行的run方法
this->entry_point()(this, this);
}
DTRACE_THREAD_PROBE(stop, this);
this->exit(false);
delete this;
}
一条U字型代码调用链至此结束:
-
Java中调用Thread的star方法,通过JNI方式,调用到native层。
-
native层,JVM通过pthread_create方法创建一个系统内核线程,并指定内核线程的初始运行地址,即一个方法指针。
-
在内核线程的初始运行方法中,利用JavaCalls模块,回调到java线程的run方法,开始java级别的线程执行。
线程如何调度
计算机的世界里,CPU会分为若干时间片,通过各种算法分配时间片来执行任务,有耳熟能详时间片轮转调度算法、短进程优先算法、优先级算法等。当一个任务的时间片用完,就会切换到另一个任务。在切换之前会保存上一个任务的状态,当下次再切换到该任务,就会加载这个状态, 这就是所谓的线程的上下文切换。很明显,上下文的切换是有开销的,包括很多方面,操作系统保存和恢复上下文的开销、线程调度器调度线程的开销和高速缓存重新加载的开销等。
经过上面两个理论基础的回顾,开启大量线程引起的问题,总结起来,就两个字——开销。
消耗时间:线程的创建和销毁都需要时间,当数量太大的时候,会影响效率。
消耗内存:创建更多的线程会消耗更多的内存,这是毋庸置疑的。线程频繁创建与销毁,还有可能引起内存抖动,频繁触发GC,最直接的表现就是卡顿。长而久之,内存资源占用过多或者内存碎片过多,系统甚至会出现OOM。
消耗CPU。在操作系统中,CPU都是遵循时间片轮转机制进行处理任务,线程数过多,必然会引起CPU频繁的进行线程上下文切换。这个代价是昂贵的,某些场景下甚至超过任务本身的消耗。
如何优化
线程的本质是为了执行任务,在计算机的世界里,任务分大致分为两类,CPU密集型任务和IO密集型任务。
CPU密集型任务,比如公式计算、资源解码等。这类任务要进行大量的计算,全都依赖CPU的运算能力,持久消耗CPU资源。所以针对这类任务,其实不应该开启大量线程。因为线程越多,花在线程切换的时间就越多,CPU执行效率就越低,一般CPU密集型任务同时进行的数量等于CPU的核心数,最多再加个1。
IO密集型任务,比如网络读写、文件读写等。这类任务不需要消耗太多的CPU资源,绝大部分时间是在IO操作上。所以针对这类任务,可以开启大量线程去提高CPU的执行效率,一般IO密集型任务同时进行的数量等于CPU的核心数的两倍。
另外,在无法避免,必须要开启大量线程的情况下,我们也可以使用线程池代替直接创建线程的做法进行优化。线程池的基本作用就是复用已有的线程,从而减少线程的创建,降低开销。在Java中,线程池的使用还是非常方便的,JDK中提供了现成的ThreadPoolExecutor类,我们只需要按照自己的需求进行相应的参数配置即可,这里提供一个示例。
/**
* 线程池使用
*/
public class ThreadPoolService {
/**
* 线程池变量
*/
private ThreadPoolExecutor mThreadPoolExecutor;
private static volatile ThreadPoolService sInstance = null;
/**
* 线程池中的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。
* 除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心 * 线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲 * 置的核心线程就会被终止。
* CPU密集型任务 N+1 IO密集型任务 2*N
*/
private final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() + 1;
/**
* 线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。包含核心线程数+非* * 核心线程数。
*/
private final int MAXIMUM_POOL_SIZE = Math.max(CORE_POOL_SIZE, 10);
/**
* 非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。
* 只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线 * 程产生效果。
*/
private final long KEEP_ALIVE_TIME = 2;
/**
* 用于指定keepAliveTime参数的时间单位。
*/
private final TimeUnit UNIT = TimeUnit.SECONDS;
/**
* 线程池中保存等待执行的任务的阻塞队列
* ArrayBlockingQueue 基于数组实现的有界的阻塞队列
* LinkedBlockingQueue 基于链表实现的阻塞队列
* SynchronousQueue 内部没有任何容量的阻塞队列。在它内部没有任何的缓存空间
* PriorityBlockingQueue 具有优先级的无限阻塞队列。
*/
private final BlockingQueue<Runnable> WORK_QUEUE = new LinkedBlockingDeque<>();
/**
* 线程工厂,为线程池提供新线程的创建。ThreadFactory是一个接口,里面只有一个newThread方法。 默认为DefaultThreadFactory类。
*/
private final ThreadFactory THREAD_FACTORY = Executors.defaultThreadFactory();
/**
* 拒绝策略,当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候 * ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。
* CallerRunsPolicy 只用调用者所在线程来运行任务。
* AbortPolicy 直接抛出RejectedExecutionException异常。
* DiscardPolicy 丢弃掉该任务,不进行处理。
* DiscardOldestPolicy 丢弃队列里最近的一个任务,并执行当前任务。
*/
private final RejectedExecutionHandler REJECTED_HANDLER = new ThreadPoolExecutor.AbortPolicy();
private ThreadPoolService() {
}
/**
* 单例
* @return
*/
public static ThreadPoolService getInstance() {
if (sInstance == null) {
synchronized (ThreadPoolService.class) {
if (sInstance == null) {
sInstance = new ThreadPoolService();
sInstance.initThreadPool();
}
}
}
return sInstance;
}
/**
* 初始化线程池
*/
private void initThreadPool() {
try {
mThreadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE_TIME,
UNIT,
WORK_QUEUE,
THREAD_FACTORY,
REJECTED_HANDLER);
} catch (Exception e) {
LogUtil.printStackTrace(e);
}
}
/**
* 向线程池提交任务,无返回值
*
* @param runnable
*/
public void post(Runnable runnable) {
mThreadPoolExecutor.execute(runnable);
}
/**
* 向线程池提交任务,有返回值
*
* @param callable
*/
public <T> Future<T> post(Callable<T> callable) {
RunnableFuture<T> task = new FutureTask<T>(callable);
mThreadPoolExecutor.execute(task);
return task;
}
}
24 pthread 了解吗?new 一个线程占用多少内存?(快手)
这道题想考察什么?
是否清楚创建线程的代价
考察的知识点
线程底层原理
线程资源消耗
考生应该如何回答
pthread
pthread一般是指 POSIX的线程标准,是一套定义了创建和操纵线程的API。 一般用于Unix-like系统,如Linux、Mac OS。
Java的跨平台基于虚拟机,由JVM屏蔽不同的操作系统的底层实现。在Java中创建线程,运行在Linux中(包括Android),实际上就是封装了pthread(《4.23 如何开启一个线程,开启大量线程会有什么问题,如何优化? 》)。
线程内存
在Java中每个线程需要分配线程内存,用来存储自身的线程变量。在JDK 1.4中每个线程是256K的内存,在JDK 1.5之后每个线程是1M的内存。
25 HandlerThread是什么?
这道题想考察什么?
是否了解HandlerThread与真实场景使用,是否熟悉HandlerThread
考察的知识点
HandlerThread的概念在项目中使用与基本知识
考生应该如何回答
HandlerThread原理
HandlerThread就是一个自带Handler的Thread,更确切的讲是一个内含有Looper的Thread,这点从HandlerThread的源码注释就可以得知。
/**
* Handy class for starting a new thread that has a looper. The looper can then be
* used to create handler classes. Note that start() must still be called.
*/
用于启动具有looper的新线程的方便类。 可以使用looper来创建处理程序类。 注意仍然必须调用start()来启动。
要想获取一个与之绑定的Handler可以通过内置的getThreadHandler()方法获得。
public Handler getThreadHandler() {
if (mHandler == null) {
mHandler = new Handler(getLooper());
}
return mHandler;
}
那么,当从调用handlerThread.start()方法那一刻开始,其内部发生了什么呢?Thread的start()方法注释如下。
/**
* Causes this thread to begin execution; the Java Virtual Machine
* calls the <code>run</code> method of this thread.
*/
导致此线程开始执行; Java虚拟机调用此线程的run方法。
看看HandlerThread的run()方法。
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
run()方法里没有什么陌生的东西,先是调用了Looper.prepare()方法创建一个Looper,然后加了一个同步锁获取当前线程的Looper对象赋值到mLooper上并唤醒所有线程,再然后设置当前线程优先级,再然后调用了onLooperPrepared()方法,最后调用Looper.loop()开启循环。
可能会有些好奇为什么在赋值mLooper时要加同步锁,后面会有解释。
看到这里,你可能会想,这跟我们自己继承Thread在run方法里开启一个Looper没什么两样吗,的确,但是官方也说了HandlerThread是一个方便类,方便我们在子线程使用Handler。
对了,上面run()方法里的onLooperPrepared()方法是干什么的?
/**
* Call back method that can be explicitly overridden if needed to execute some
* setup before Looper loops.
*/
protected void onLooperPrepared() {
}
如果需要在Looper循环之前执行某些设置,可以复写此方法。
emmm,那run()方法里加同步锁是为什么呢?
如果你不想通过HandlerThread为我们提供的getThreadHandler()方法来获取一个Handler,HandlerThread也提供了getLooper()方法为你提供Looper对象创建实现自己的Handler。
public Looper getLooper() {
if (!isAlive()) {
return null;
}
// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
}
return mLooper;
}
这下就明白为啥在run()方法同步锁里赋值mLooper完成后要在再唤醒所有线程了。因为此方法将阻塞线程,直到looper已初始化。
整个HandlerThread类也就不到200行代码,除了上面那些,它还封装了线程不使用时退出Looper的方法。
public boolean quit() {
Looper looper = getLooper();
if (looper != null) {
looper.quit();
return true;
}
return false;
}
public boolean quitSafely() {
Looper looper = getLooper();
if (looper != null) {
looper.quitSafely();
return true;
}
return false;
}
可以看出,HandlerThread真的是一个方便类啊,很方便。
使用场景
相比于普通的Thread,我为什么要使用HandlerThread?
对于普通的Thread,执行一遍就结束了,一个实例不能start多次,如果你不止一次启动同一个示例,那么将会抛出一个IllegalThreadStateException异常,这些在Thread的源码里都有注释写到。
/**
* It is never legal to start a thread more than once.
* 不止一次启动线程永远不合法
* In particular, a thread may not be restarted once it has completed execution.
* 特别是,一旦完成执行,线程可能无法重新启动。
* @exception IllegalThreadStateException if the thread was already started.
* 如果线程已经启动将抛出IllegalThreadStateException异常
*/
如果需要频繁的操作数据库、大文件、请求网络,就需要频繁的创建Thread示例调用start(),又或是需要执行一系列串行操作;而HandlerThread由于自带Handler,也就相当自带了一个任务队列,需要进行耗时操作的时候只要通过Handler执行send或post系列操作就行,不再需要频繁创建Thread,节省资源。
最后
此面试题会持续更新,请大家多多关注!!!
有需要这份面试题的朋友可以扫描下方二维码即可免费领取!!!
(扫码还可以享受ChatGPT机器人的服务哦!!!!)