这道题想考察什么?
- 是否了解线程开启的方式?
- 开启大量线程会引起什么问题?为什么?怎么优化?
考察的知识点
- 线程的开启方式
- 开启大量线程的问题
- 线程池
考生应该如何回答
1、首先,关于如何开启一个线程,大多数人可能都会说3种,Thread、Runnable、Callback嘛!但事实却不是这样的。看JDK里怎么说的。
1 | /** |
Thread源码的类描述中有这样一段,翻译一下,只有两种方法去创建一个执行线程,一种是声明一个Thread的子类,另一种是创建一个类去实现Runnable接口。惊不惊讶,并没有提到Callback。
- 继承Thread类
1 | public class ThreadUnitTest { |
- 实现Runnable接口
1 | public class ThreadUnitTest { |
- 还是看看Callable是怎么回事吧。
1 | public class ThreadUnitTest { |
Callable的方式必须与FutureTask结合使用,我们看看FutureTask的继承关系。
1 | //FutureTask实现了RunnableFuture接口 |
真相大白了,其实实现Callback接口创建线程的方式,归根到底就是Runnable方式,只不过它是在Runnable的基础上又增加了一些能力,例如取消任务执行等。
2、开启线程的几种方式算是答上了,那开启大量线程,或者说频繁开启线程到底会引起什么问题呢?众所周知,在Java中,调用Thread的start方法后,该线程即置为就绪状态,等待CPU的调度。这个流程里有两个关注点需要去理解。
- start内部怎样开启线程的?看看start方法是怎么实现的。
1 | // Thread类的start方法 |
JVM中,native方法与java方法存在一个映射关系,Java中的start0对应c层的JVM_StartThread方法,我们继续看一下。
1 | JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)) |
走到这里发现,Java层已经过渡到native层,但远远还没结束。
1 | JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) : |
pthread_create方法,第三个参数表示启动这个线程后要执行的方法的入口,第四个参数表示要给这个方法传入的参数。
1 | static void *thread_native_entry(Thread *thread) { |
终于开始执行run方法了!!!
1 | //thread.cpp类 |
一条U字型代码调用链总算结束了,高呼一声,原来start之后做了这么多事情呀,稍微总结一下。
1) Java中调用Thread的star方法,通过JNI方式,调用到native层。
2) native层,JVM通过pthread_create方法创建一个系统内核线程,并指定内核线程的初始运行地址,即一个方法指针。
3) 在内核线程的初始运行方法中,利用JavaCalls模块,回调到java线程的run方法,开始java级别的线程执行。
线程开启后CPU调度会发生什么?
计算机的世界里,CPU会分为若干时间片,通过各种算法分配时间片来执行任务,有耳熟能详时间片轮转调度算法、短进程优先算法、优先级算法等。当一个任务的时间片用完,就会切换到另一个任务。在切换之前会保存上一个任务的状态,当下次再切换到该任务,就会加载这个状态, 这就是所谓的线程的上下文切换。很明显,上下文的切换是有开销的,包括很多方面,操作系统保存和恢复上下文的开销、线程调度器调度线程的开销和高速缓存重新加载的开销等。
经过上面两个理论基础的回顾,开启大量线程引起的问题,总结起来,就两个字——开销。
- 消耗时间。线程的创建和销毁都需要时间,当数量太大的时候,会影响效率。
- 消耗内存。创建更多的线程会消耗更多的内存,这是毋庸置疑的。线程频繁创建与销毁,还有可能引起内存抖动,频繁触发GC,最直接的表现就是卡顿。长而久之,内存资源占用过多或者内存碎片过多,系统甚至会出现OOM。
- 消耗CPU。在操作系统中,CPU都是遵循时间片轮转机制进行处理任务,线程数过多,必然会引起CPU频繁的进行线程上下文切换。这个代价是昂贵的,某些场景下甚至超过任务本身的消耗。
3、针对上面提及到的问题,我们自然需要进行优化。线程的本质是为了执行任务,在计算机的世界里,任务分大致分为两类,CPU密集型任务和IO密集型任务。
- CPU密集型任务,比如公式计算、资源解码等。这类任务要进行大量的计算,全都依赖CPU的运算能力,持久消耗CPU资源。所以针对这类任务,其实不应该开启大量线程。因为线程越多,花在线程切换的时间就越多,CPU执行效率就越低,一般CPU密集型任务同时进行的数量等于CPU的核心数,最多再加个1。
- IO密集型任务,比如网络读写、文件读写等。这类任务不需要消耗太多的CPU资源,绝大部分时间是在IO操作上。所以针对这类任务,可以开启大量线程去提高CPU的执行效率,一般IO密集型任务同时进行的数量等于CPU的核心数的两倍。
另外,在无法避免,必须要开启大量线程的情况下,我们也可以使用线程池代替直接创建线程的做法进行优化。线程池的基本作用就是复用已有的线程,从而减少线程的创建,降低开销。在Java中,线程池的使用还是非常方便的,JDK中提供了现成的ThreadPoolExecutor类,我们只需要按照自己的需求进行相应的参数配置即可,这里提供一个示例。
1 | /** |
备注:转载至知乎