正文
创建一个线程是比较耗时间的。需要请求操作系统、分配栈空间、初始化等工作。
大家都知道的,操作系统基本概念,不再赘述。值得注意的是,WAITING状态的线程(多见于I/O等待)几乎不会被调度,因此并不导致过多的上下文切换。
大量线程频繁切换,势必要访问不同的数据,打乱了空间局部性,导致CPU cache miss增加,需要经常访问更慢的内存,会明显影响CPU密集型程序的性能,这点大家恐怕没想到吧。
线程会增加内存占用,线程的栈空间通常占1MB,1000个就是1GB。而且在栈上引用了很多对象,暂时不能回收,你说有多少个GB?
一些有限的资源,如锁、数据库连接、文件句柄等,当线程被挂起或阻塞,就暂时无人可用了,浪费!还有死锁风险!
那么分配多少线程好呢?
-
对于I/O密集型程序:一个经验数值是两倍于数据库连接数,例如你有30个数据库连接,就开60个线程;我还有个经验数值是500以下,超过500就慢一些,如果调用栈特别深,这个数值还要下调。
-
对于CPU密集型程序:我的经验数值是略多于CPU核心数 (理论上是等于,但你难免有几个阻塞操作)。除了核心数,还要考虑CPU cache的大小,最好实际测试一下。举个例子,某司内部的自动重构程序在Intel i7 3630QM CPU上测试,3~4个线程效果最好。
传统的网络程序是每个会话占用一个连接、一个线程。I/O多路复用(I/O multiplexing:多个会话共用一个连接)是应C10K问题而生的,C10K就是1万个连接。1万个连接是很耗系统资源的,何况还有1万个线程。从上文的分析可知,C1K的时候就可以开始运用I/O多路复用了。
Thread Pool
预留一些可反复使用的线程在一个池里,反复地接受任务。线程数量可能是固定的,也可能是一定范围内变动的,依所选择的线程池的实现而定。
这个模型是极其常用的,例如Tomcat就是用线程池来处理请求的。
注意——尽量不要阻塞任务线程;若实在无法避免,多开一些线程——每阻塞一个线程,线程池就少一个可用的线程。
Java典型的线程池有Executors.newFixedThreadPool Executors.newFixedThreadPool Executors.newFixedThreadPool Executors.newScheduledThreadPool等等,也可以直接new ThreadPoolExecutor(可指定线程数的上限和下限)。
Scala没有增加新的线程池种类,但有个blocking方法能告诉线程池某个调用会阻塞,需要临时增加1个线程。
Future
Future是一个未来将会有值的对象,相当于一个占位符(提货凭证!)。
将任务投入线程池执行时,可为任务绑定一个Future,凭此Future即可在未来取得任务执行结果。未来是什么时候呢?要通过检查Future内部的状态来获知——任务完成时会修改这个状态,将执行结果存进去。
最初的代码示例可改写为:
// 两个future是并行的
val f1 = Future { get("http://server1") }
val f2 = Future { get("http://server2") }
compute(f1.get(), f2.get())
高级模型