java并发编程——线程池和Executor介绍

孤人 提交于 2019-12-04 20:26:02
  • 第一部分:概述

早期的应用程序大多是单线程串行执行的,虽然程序的任务边界清晰有序,但是执行的效率却很低,尤其是执行花费时间较长的操作,会导致大量的等待和堆积。为了提高程序的执行效率和吞吐量,我们很自然的会想到多线程,即为每个任务都新建一个独立的线程,这样就极大地提高了程序的执行效率。但事实上多线程也会带来很多问题。比如大量的创建线程,这本身就会消耗很多的资源,尤其是内存,当创建的线程数量超过服务器能够承受的极限时,内存溢出是在所难免的;比如还有其他稳定性问题,以及多线程造成的程序调用和管理的混乱。所以,综上所述,我们需要一个介于两者之间的工具,既可以创建大量的线程来提高程序的并发性和吞吐量,同时又可以有序的管理这些线程,能够可控。而Executor接口和线程池技术就是在这种背景下应运而生的。下面分别将这两种常用的并发技术做以介绍和总结。

  • 第二部分:Executor和ExecutorService接口介绍

java.util.coucurrent包下面为我们提供了丰富的并发工具,Executor和ExecutorService接口就是其中比较重要的两个。

  • Executor接口介绍

public interface Executor{
    void execute(Runnable command);
}

以上是Executor接口的代码,可以看出这个借口非常简单,就只有一个execute()方法。但他却为强大的异步任务执行提供了基础,它支持不同类型的任务执行策略。Executor框架基于生产者消费者模型,它提供了一个标准的方法将任务的提交和任务的执行过程解耦,并用Runnable来表示任务。下面来看一个基于Executor的简单服务器实现:

public class ExecutorWebServer {
    private static final int LIMIT = 50;
    private static final Executor exe = Executors.newFixedThreadPool(LIMIT);
    
	public static void main(String[] args) throws IOException {
		// TODO Auto-generated method stub
        ServerSocket server = new ServerSocket(80);
        while(true){
        	Socket socket = server.accept();
        	Runnable task = new Runnable(){
        		public void run(){
        			doSomeThing(socket);
        		}
        	};
        	exe.execute(task);
        }
	}
}

通过上面列子我们就很好的把任务的提交和任务的执行分开来,这就是Executor框架最大的优势。

  • ExecutorService接口介绍

上面我们讲了如何创建一个Executor,但是并没有讲如何关闭它。既然Executor是为应用程序服务的,因而他们应该是可关闭的,并将关闭操作中受影响的任务状态反馈给应用程序。为了解决执行服务的生命周期问题,ExecutorService扩展了Executor接口,增加了管理生命周期的方法:

public interface ExecutorService Extends Executor{
    void shutdown();
    boolean isShutDown();
    boolean isTerminated();
    ……
}

ExecutorService的生命周期分为三种,运行,关闭和已终止。下面看这个简单的实例:

 class NetworkService implements Runnable {
    private final ServerSocket serverSocket;
    private final ExecutorService pool;

    public NetworkService(int port, int poolSize)
        throws IOException {
      serverSocket = new ServerSocket(port);
      pool = Executors.newFixedThreadPool(poolSize);
    }
 
    public void run() { // run the service
      try {
        for (;;) {
          pool.execute(new Handler(serverSocket.accept()));
        }
      } catch (IOException ex) {
        pool.shutdown();
      }
    }
  }

  class Handler implements Runnable {
    private final Socket socket;
    Handler(Socket socket) { this.socket = socket; }
    public void run() {
      // read and service request on socket
    }
 }
  • 第三部分:线程池介绍及使用

  • 几种类型的线程池

可以通过Executors类的几个静态方法来创建线程池。包含以下几类线程池:
newFixedThreadPool创建固定长度的线程池。每提交一个任务时就新建一个线程,直到达到线程池的最大数量。

newCachedThreadPool创建一个可缓存的线程池,线程的数量不受限制,但是在以前线程可用时将重用他们。

newSingleThreadExecutor创建一个使用单个 worker 线程的 ,以无界队列方式来运行该线程。

newScheduleThreadPool创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。类似于一个定时器。

  • 设置线程池大小

线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。在代码中最好不要固定线程池的大小,而要通过某种灵活机制来配置。线程池大小的配置只要避免“过大”和”过小“两种极端情况即可。线程池过大会导致大量的线程竞争CPU和内存,最终导致资源耗尽;线程池过小时又会导致资源浪费,所以设置一个合适的线程池大小非常重要。通常有一个简单的设置线程池大小的公式供我们参考使用:

N(Threads) = N(cpu) * U(cpu) * (1 + w/c)

N(cpu)代表cpu的个数,U(cpu)代表cpu利用率, w/c表示等待时间与计算时间的比值

  • 配置线程池

ThreadPoolExecutor为Executor和ExecutorService接口提供了基本实现。下面我们来看一下ThreadPoolExecutor类的构造函数:该类一共有四个构造函数,其中最基础的一个构造函数如下:

ThreadPoolExecutor(int corePoolSize, 
                   int maximumPoolSize, 
                   long keepAliveTime, 
                   TimeUnit unit, 
                   BlockingQueue<Runnable> workQueue) 

corePoolSize表示的是核心池的大小,第二个参数表示线程池最大的线程数,第三个参数表示存活时间,第四个参数表示给定单元粒度的时间段,第五个参数表示的是工作队列。

  • 扩展线程池

ThreadPoolExecutor是可以扩展的,它提供了几个可以在子类中改写的方法:beforeExecutor,afterExecutor,terminated,这些方法可用于扩展ThreadPoolExecutor的行为。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!