用10086客服热线理解Java高级多线程之线程池


 尼采说过,人类无法理解没有经历过的事情。所以很多概念不能去强行地理解和记忆,需要结合实际生活中的案例。

客服热线案例

 对于线程池的理解可以类比成用户给运行商的客服打电话,假设某运营商客服作息500人,现有500用户正与客服一对一交流,那么第501个用户即海绵宝宝就会听到语音等待:“您好!现在作息繁忙…”,海绵宝宝只能等待,如果说以上500个用户有一个挂断电话,海绵宝宝都可以成功连上人工客服;
 用户电话打进来对客服来说都是一项任务,而每个客服可以完成多个任务,而不是仅仅500个任务,比如每个客服每天接入100个电话,那么整个客服后台就是完成了5万个任务;

在这里插入图片描述

类比:这里客服后台可以看成是线程池,500个客服可以看成是500个线程,500个用户就是Task,而接通电话这件事情就是线程的执行;第501个拨打客服热线的用户相当于501个Task,超过最大值的线程可以排队,但他们要等到其他线程完成后才启动;这就实现了对线程的复用;

类比这个现实案例,可以发现存在以下问题:

  • 线程是宝贵的内存资源、单个的线程约占1MB空间。过多分配容易造成内存溢出。
  • 频繁的创建及销毁线程会增加虚拟机回收频率、资源的开销,造成程序性能下降

引入线程池

1.线程的概念

以下内容摘自百度百科

 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

2.线程池的作用:

  • 将任务提交给线程池。由线程池分配线程、运行任务,并在当前任务结束后复用线程。
  • 线程容器。可设定线程分配数量的上限。
  • 将预先创建好的线程对象存入池中,并重用线程池中的线程对象。
  • 避免频繁的创建和销毁线程。

获取线程池

1.常用的线程池接口和类

  • Executor : 线程池的顶级接口
方法 描述
void execute(Runnable command) 在将来的某个时间执行给定的命令。
  • ExecutorService:
     线程池接口,继承了Executor ,并增加和优化了父接口的一些行为,可通过submit(Runnable task)提交任务代码。
方法 描述
Future submit(Callable task) 提交值返回任务以执行,并返回代表任务待处理结果的Future。
Future<?> submit(Runnable task) 提交一个可运行的任务执行,并返回一个表示该任务的未来。
Future submit(Runnable task, T result) 提交一个可运行的任务执行,并返回一个表示该任务的未
  • Executors工厂类:

 通过下面的方法可以获得一个固定线程数量的线程池。参数即线程池中线程的数量

方法 描述
static ExecutorService newFixedThreadPool(int nThreads) 创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。

创建方式

//线程池引用 ===> Executors工具类创建线程
        ExecutorService executorService = Executors.newFixedThreadPool(2);

 也可以通过newCachedThreadPool() 获取动态数量的线程池,如果不够则自动创建新的,没有上限;

JavaApi:

创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程。 这些池通常会提高执行许多短暂异步任务的程序的性能。 调用execute将重用以前构造的线程(如果可用)。 如果没有可用的线程,将创建一个新的线程并将其添加到该池中。 未使用六十秒的线程将被终止并从缓存中删除。 因此,长时间保持闲置的池将不会消耗任何资源。 请注意,可以使用ThreadPoolExecutor构造函数创建具有相似属性但不同详细信息的池(例如,超时参数)。

方法 描述
static ExecutorService newCachedThreadPool() 创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程。

2.代码案例

 现有一个任务,实现Runnable接口,循环打印5次,交有一个线程池executorService去分别执行任务,且线程池的最大线程数为2 ;

public class TestThreadPool {
    public static void main(String[] args){

        //线程池引用 =  Executors工具类创建线程
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        Runnable runnable = new MyTask();
        executorService.submit(runnable);
    }
}
class MyTask implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i <5; i++) {
            System.out.println(Thread.currentThread().getName()+" MyTask: "+i);
        }
    }
}

打印结果:
在这里插入图片描述
如果一个任务被提交多次的?比如提交给线程池的任务大于线程池线程的最大上限,会不会
出现线程的复用呢?
测试方法:

//线程池引用 =  Executors工具类创建线程
 ExecutorService executorService = Executors.newFixedThreadPool(2);
 Runnable runnable = new MyTask();
 executorService.submit(runnable);
 executorService.submit(runnable);
 executorService.submit(runnable);

 显然现在有两个线程却提交了三个任务,因此,必然有一个线程要被复用。

打印结果:
在这里插入图片描述
 从打印结果来看,线程1被复用了两次,避免了线程频繁的创建和销毁,达到了优化的目的;

Callable接口

1.概念简述

 前面在介绍Executor的时候,列出了以下三个方法,其中第一个方法的参数是Callable

方法 描述
Future submit(Callable task) 提交值返回任务以执行,并返回代表任务待处理结果的Future。
Future<?> submit(Runnable task) 提交一个可运行的任务执行,并返回一个表示该任务的未来。
Future submit(Runnable task, T result) 提交一个可运行的任务执行,并返回一个表示该任务的未
@FunctionalInterface
public interface Callable<V>

 返回结果并可能引发异常的任务。 实现者定义一个没有参数的单一方法,称为call 。
Callable接口类似于Runnable ,因为它们都是为其实例可能由另一个线程执行的类设计的。 然而,A Runnable不返回结果,也不能抛出被检查的异常。

2.应用场景

 假如我们需要计算1+2+3+......+1000000的结果,这个过程我们需要交付给多个线程去分别执行,如Thread1执行前10万数字之和,如Thread2执行10万~20万数字之和…,但是其中涉及一个问题,就是Thread1执行执行的结果必须有返回值,不然Thread2不能根据1的结果继续执行;显然,此时就不能使用类实现Runnable接口覆盖run方法的形式来解决了,因为run返回的是void;

class MyTask implements Runnable{
    @Override
    public void run() {
      //1+2+...+100000
    }
}

这就用到了Callable,因为它最大的特点就是:返回结果并可能引发异常的任务。
在这里插入图片描述

3.方法

方法 描述
V call() 计算一个结果,如果不能这样做,就会抛出一个异常。
  • JDK5加入,与Runnable接口类似,实现之后代表一个线程任务。
  • Callable具有泛型返回值、可以声明异常。
public interface Callable<V>{
	public V call() throw Exception;
}

4.应用方法

public class TestCallable {
    public static void main(String[] args){
        //创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Callable task = new Task();
        executorService.submit(task);
        executorService.submit(task);
        executorService.submit(task);
    }
}
class Task implements Callable<String>{
    @Override
    public String call() throws Exception {
        for (int i = 0; i <5 ; i++) {
            System.out.println(Thread.currentThread().getName()+" : "+i);
        }
        return null;
    }
}

打印结果:
在这里插入图片描述
 显然现在有两个线程却提交了三个任务,因此,线程2复用了。

Future 接口

1.引入

&emsp;会到我们开始引入的案例:计算1+2+3+......+1000000的结果,这个过程我们需要交付给多个线程去分别执行,Callable可以返回类型,但是怎么将各个线程的返回类型一次相加呢?这里就需要引入Future接口,该对象里面封装了call()方法的返回值

JavaApi:

A Future计算的结果。 提供方法来检查计算是否完成,等待其完成,并检索计算结果。 结果只能在计算完成后使用方法get进行检索,如有必要,阻塞,直到准备就绪。 取消由cancel方法执行。 提供其他方法来确定任务是否正常完成或被取消。 计算完成后,不能取消计算。 如果您想使用Future ,以便不可撤销,但不提供可用的结果,则可以声明Future<?>表格的类型,并返回null作为基础任务的结果。

2.方法

方法 描述
boolean cancel(boolean mayInterruptIfRunning) 尝试取消执行此任务。
V get() 等待计算完成,然后检索其结果。
V get(long timeout, TimeUnit unit) 如果需要等待最多在给定的时间计算完成,然后检索其结果(如果可用)。
boolean isCancelled() 如果此任务在正常完成之前被取消,则返回 true 。
boolean isDone() 返回 true如果任务已完成。

3.解决分任务执行1+2+…+100的问题

public class TestCallable {
	public static void main(String[] args) throws InterruptedException, ExecutionException {
		System.out.println("程序开始");
		//创建线程池
		ExecutorService es = Executors.newFixedThreadPool(3);
		
		Callable<Integer> task1 = new MyTask1();
		Callable<Integer> task2 = new MyTask2();
		//接收两个任务的返回值
		Future<Integer> f1 = es.submit(task1);
		Future<Integer> f2 = es.submit(task2);
		//检索计算结果并返回给变量
		Integer result1 = f1.get();//无限期等待
		Integer result2 = f2.get();//无限期等待
		//输出结果
		System.out.println("result1:"+result1 + " + result2:"+result2 +" = " +(result1+result2));		
	}
}
//线程1计算1+2+....+50
class MyTask1 implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
	  	Thread.sleep(5000);
		Integer sum = 0;
		for (int i = 1; i <= 50; i++) {
			sum += i;
		}
		return sum;
	}
}
//线程2计算51—+52+.....+100
class MyTask2 implements Callable<Integer>{
	@Override
	public Integer call() throws Exception {
	    Thread.sleep(5000);
		Integer sum = 0;
		for (int i = 51; i <= 100; i++) {
			sum += i;
		}
		return sum;
	}
}

打印结果(这是动图):
在这里插入图片描述
 从打印结果来看V get()是以阻塞的形式等待Future中的异步(多线程并发)处理结果(call()的返回值);通俗的来说,这里让两个线程休眠了5秒,因此在两个线程限期等待的过程中,V get()是以阻塞的形式等待线程执行完毕,完后才能拿到Future的返回值;


 其实,在这里已经很接近分布式的感觉啦~
在这里插入图片描述

发布了85 篇原创文章 · 获赞 196 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq_44717317/article/details/104928731