什么是线程池
学习编程的小伙伴们会经常听到“线程池”、“连接池”这类的词语,可是到底“池”是什么意思呢?我讲个故事大家就理解了:在很久很久以前有一家银行,一年之中只有一个客户来办理业务,随着时间的推移,办理业务的人数每年都增加五千。20年之后这家银行办理业务的人次已经到十万。最开始只有一个客户的时候银行只需要雇佣一个按办理业务次数计工资的临时工就行了,办完业务就解雇。随着办理业务的人不断增多,银行老板发现继续雇佣按次计费的员工太麻烦了,每天都在招人,又每天都解雇人。所以老板就想出了一个办法,雇佣几个员工一直在办事大厅待命,有顾客来的时候,柜员就给顾客办业务,办完一个之后再继续为下一个顾客办业务,如果没有下一个顾客就继续待命。就这样这个“聪明”的老板发明了“柜员池”。
线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。
在Java中使用线程池
线程池的种类
Java中有三个比较常用的线程池,分别是FixedThreadPool,SingleThreadExecutor,CachedThreadPool。
1) FixedThreadPool
这是一个线程数固定的线程池,当这个线程池被创建的时候,池里的线程数就已经固定了。当需要运行的线程数量大体上变化不大时,适合使用这种线程池。固定数量还有一个好处,它可以一次性支付高昂的创建线程的开销,之后再使用的时候就不再需要这种开销。
2) SingleThreadExecutor
这是一个线程数量为1的线程池,所有提交的这个线程池的任务都会按照提交的先后顺序排队执行。单个线程执行有个好处:由于任务之间没有并发执行,因此提交到线程池种的任务之间不会相互干扰。程序执行的结果更具有确定性。
3) CachedThreadPool
一看到Cache就知道这是一个和缓存有关的线程池,每次有任务提交到线程池的时候,如果池中没有空闲的线程,线程池就会为这个任务创建一个线程,如果有空闲的线程,就会使用已有的空闲线程执行任务。有的人可能会有个疑惑:这样线程不就越来越多了吗?其实不是的,这个线程池还有一个销毁机制,如果一个线程60秒之内没有被使用过,这个线程就会被销毁,这样就节省了很多资源。CachedThreadPool是一个比较通用的线程池,它在多数情况下都能表现出优良的性能。以后编码的时候,遇事不决,用缓存(线程池)。
线程池的使用
上面说了半天也没说这东西到底咋用,有的小伙伴已经等不及了。其实Java的线程池使用起来很简单。只需要一个接口和一个类就行:
ExecutorService是所有的线程池都实现了的接口,用这个接口可以定义句柄。
Executors类用于创建不同种类的线程池,Executors.new***()方法就可以返回一个线程池的对象,***表示线程池的名字。
具体代码如下:
public class CreateThreadPool{
public static void main(String[] args) {
//定义一个有5个线程的线程池
ExecutorService fixedTP = Executors.newFixedThreadPool(5);
ExecutorService singleTE = Executors.newSingleThreadExecutor();
ExecutorService cachedTP = Executors.newCachedThreadPool();
}
}
不同的线程池之间只有定义的方式略有区别,使用方法是完全相同的。下面以缓存线程池为例来介绍线程池的使用方法:
class ExampleThread implements Runnable {
@Override
public void run() {
System.out.println("Do something");
}
}
public class UseThreadPool {
public static void main(String[] args) {
ExecutorService cachedTP = Executors.newCachedThreadPool();
cachedTP.execute(new ExampleThread());
cachedTP.shutdown();
}
}
ExampleThread实现了Runnable接口,这次我们没有使用Thread类创建线程,而是使用的线程池,直接调用线程池的execute()方法并将ExampleThread的对象传进去就可以创建线程了,execute()方法可以接收任何实现Runnable接口的类的对象。使用线程池的时候别忘了调用shutdown()方法关闭线程池。继承自Thread类的对象也可以用同样的方式创建线程,因为Thread类也实现了Runnable接口。
上次我们还讲到了可以通过实现Callable接口创建线程,这个线程就不能使用execute()方法了。线程池还有另一个方法:submit()方法接收实现Callable接口的的对象,同时还有一个重载的方法,重载方法接收实现Runnable方法的类。也就是说不管是实现了Runnable还是Callable的类都可以作为它的参数。submit()方法还有一个Future类型的返回值,Future用于获取线程的返回值,Future是一个有泛型的类,泛型的类型与Callable的泛型相同。演示代码如下:
class ExampleThread implements Callable<String> {
@Override
public String call() {
return "Some Value";
}
}
public class UseThreadPool {
public static void main(String[] args) {
ExecutorService cachedTP = Executors.newCachedThreadPool();
Future<String> future = cachedTP.submit(new ExampleThread());
try {
String value = future.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
cachedTP.shutdownNow();
}
}
future.get()方法,这个方法会阻塞当前线程,一直等到线程池里相应的线程执行结束的时候当前线程才会解除阻塞。因此,这个get()方法也是只有等到不得不用返回值的时候才会调用,否则会影响程序运行的效率。
shutdown()方法还有一个孪生兄弟shutdownNow(),两者区别如下:
shutdown():调用这个方法之后就线程池就不会再接收其它任务了。
shutdownNow():调用这个方法会向所有的线程发一个中断信号(如果线程选择忽略,这个线程还是会继续执行),并且不再执行排队中的任务,将排队中的任务作为返回值返回(List
总结
线程池是一个典型的“用空间换时间”的应用案例,在线程池中始终维护一定数量的线程,这样不必每次都创建新的线程,代价是线程即使空闲的时候也要占用内存资源。当需要频繁创建和销毁线程的时候,使用线程池可以显著提高系统的运行效率,在线程池的不同种类中,缓存线程池在通常情况下都是性能最好的。在提交任务到线程池的时候,通常实现Runnable接口的使用execute()方法提交,实现Callable接口的使用submit()方法提交。最后,使用线程池后别忘了shutdown()。公众号:今日说码。关注我的公众号,可查看连载文章。遇到不理解的问题,直接在公众号留言即可。