线程池它就是一个池子(就像是养鱼的池子),可以养一定数量的鱼,可以重复使用!学习Java中的线程池,就是学习Java用了什么工具(API)和方法(设计模式)来搞出可以“养鱼的池子”。

本文作为入门级的线程池教程,主要介绍第一个线程池的一般写法,也就是“Hello,world”的水平,快速入门!


Table of Contents

什么是线程池

线程池体系

第一个线程池


什么是线程池

背景:

  1. 如果反复创建销毁线程,也会有一定的系统资源开销;
  2. 线程池,其实就是一个容纳多个线程的容器;Java初始时提供一定数量的、可重复使用的线程;
  3. 省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
  4. 此外,当我们线程创建过多时,容易引发内存溢出,因此我们就有必要使用线程池的技术了。

为什么要使用线程池?

  1. 在java中,如果每个请求到达就创建一个新线程,开销是相当大的。
  2. 在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。
  3. 如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。
  4. 为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。
  5. 线程池主要用来解决线程生命周期开销问题和资源不足问题。
  6. 通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。
  7. 这样,就可以立即为请求服务,使用应用程序响应更快。
  8. 另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。

《Java 并发编程的艺术》提到的来说一下使用线程池的好处:

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

注意:

  1. 程序一开始的时候,创建多个线程对象,存储到集合中;当需要线程的时候,从几回合中获取线程出来;
  2. 从JDK1.5开始,程序员不再需要自己开发线程池,而是使用内置线程池技术;

线程池体系

JDK给我们提供了Excutor框架来使用线程池,它是线程池的基础。Executor提供了一种将“任务提交”与“任务执行”分离开来的机制(解耦)。

Java线程最多开几个_java


第一个线程池

线程池的使用流程:

  1. 调用Executors类的静态方法创建线程池;
  2. 调用submit提交Runnable或Callable对象;
  3. 保存好返回的Future对象,以便得到结果或者取消任务;
  4. 当不想再提交任何任务时,调用shutdown

向线程池中提交Runnable接口的线程

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadDemo {
	public static void main(String[] args) {
		
		//第一步:调用Executors类的静态方法创建线程池;
		ExecutorService es = Executors.newFixedThreadPool(5); //创建一个固定大小的线程池
		
		//第二步-1:Runnable或Callable对象
		MyTask task = new MyTask();
		
		//第二步-2:提交Runnable或Callable对象到线程池中
		for(int i=0; i<10; i++) {
			es.submit(task); //连续提交10个线程到线程池中
		}
		es.shutdown();
	}
}
class MyTask implements Runnable {
	@Override
	public void run() {
		System.out.println("时间:"+System.currentTimeMillis()+";线程执行ID:"+
					Thread.currentThread().getName());
		try {
			Thread.sleep(1000); //休眠1s
		} catch(InterruptedException e) {
			e.printStackTrace();
		}
	}
}

 程序输出:

  1. 时间:1585382666365;线程执行ID:pool-1-thread-1
  2. 时间:1585382666365;线程执行ID:pool-1-thread-4
  3. 时间:1585382666365;线程执行ID:pool-1-thread-5
  4. 时间:1585382666365;线程执行ID:pool-1-thread-2
  5. 时间:1585382666365;线程执行ID:pool-1-thread-3

 

  1. 时间:1585382667366;线程执行ID:pool-1-thread-5
  2. 时间:1585382667367;线程执行ID:pool-1-thread-1
  3. 时间:1585382667367;线程执行ID:pool-1-thread-2
  4. 时间:1585382667367;线程执行ID:pool-1-thread-3
  5. 时间:1585382667367;线程执行ID:pool-1-thread-4

分析:

  1. 根据输出:前5个任务和后5个任务的执行时间正好相差1s!
  2. 并且,前5个和后5个任务的线程ID都是完全一致的!说明前5个线程是同一个整体,后5个线程又是另一个整体!

 

向线程池中提交Callable接口的线程

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ThreadDemo {
	public static void main(String[] args) {
		
		//第一步:调用Executors类的静态方法创建线程池;
		ExecutorService es = Executors.newFixedThreadPool(2); //创建一个固定大小的线程池
		
		//第二步-1:创建Runnable或Callable对象
		MyTask task = new MyTask();
		
		//第二步-2:提交Runnable或Callable对象到线程池中
		//第三步:用Future保存好每个任务,以便对任务的操作
		Future<String> re01 = es.submit(task);
		Future<String> re02 = es.submit(task);
		
		try {
			System.out.println(re01.get()+re02.get()); //对任务进行操作!
		} catch (InterruptedException | ExecutionException e) {
			e.printStackTrace();
		}
		
		//第四步:关闭线程池
		es.shutdown();
	}
}

class MyTask implements Callable<String> {
	public String call() {
		String info = Thread.currentThread().getName();
		return info+"执行了....";
	}
}

Callable接口和Runnable接口一样,都是Java中多线程的实现方式。其中Callable接口实现的线程可以抛出异常,也可以有返回值,而Runnable接口实现的线程则不可以(不可抛异常,不可有返回值)!