第六章 任务执行

本文探讨了在Java中如何执行任务,从串行执行任务的不足,到显式创建线程的改进,再到无限制创建线程的问题。接着介绍了基于Executor的框架,包括Executor的执行策略、线程池的优势以及生命周期。文章还讨论了如何找出并行性,如使用CompletionService实现页面渲染器的并发优化,并展示了如何为任务设置时限,以及在旅行预定门户网站的场景中应用并发策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

6.1在线程中执行任务

1.当围绕“任务执行”来设计应用程序时,第一步就是要找出清晰的任务边界。大多数服务器应用程序都提供了以独立的客户请求为边界这种方式。
2.在正常的负载下,服务器应该表现出良好的吞吐量和快速相应。在负荷过载时,性能应该是逐渐降低,而不是直接失败。

6.1.1串行地执行任务

class SingleThreadWebServer{
	public static void main(String[] args) throws IOException{
			ServerSocket server = new ServerSocket(80);
			while(true){
				Socket connection = socket.accept();
				//处理请求
			}
	} 
}

缺点: 1.每次只能处理一个请求,处理套接字i/o或者处理文件i/o时,会发生阻塞。
2.服务器的资源利用率非常低,单线程在等待i/o操作完成时,cpu处于空闲状态。

6.1.2显式地为任务创建线程

public class ThreadPerTaskWebServer {

    public static void main(String[] args)throws IOException {
        ServerSocket serverSocket = new ServerSocket(80);

        while (true){
            final Socket connection = serverSocket.accept();
            Runnable task = new Runnable() {
                @Override
                public void run() {
					//handle request
                }
            };
            new Thread(task).start();
        }
    }
}

主线程交替执行“接受外部连接”与“分发请求”等操作。对于每个请求,主循环将创建一个新线程来处理请求。
改进:

  • 任务处理从主线程中分离,主线程可以在前面的请求处理完成之前接受新的请求,提高响应速度。
  • 任务可以并行处理,从而能同时服务多个请求。吞吐量提高。
  • 任务处理代码必须是线程安全的,因为当有多个任务时会并发地调用这段代码。

6.1.3无限制创建线程的不足

为每个任务分配一个线程这种方式存在一定的缺陷,尤其是需要创建大量线程时:

  • 线程生命周期的开销非常高。如果请求的到达率非常高且请求的处理过程是轻量级的,这种情况下创建一个线程就会消耗大量的计算资源。
  • 资源消耗。活跃的线程数量会消耗系统资源,尤其是内存。如果已经有足够多的线程使CPU保持忙碌状态,继续创建线程会使性能降低。
  • 稳定性。在可创建线程的数量上存在一个限制。破坏了这些限制,可能会抛出OutOfMemoryError异常。

6.2Executor框架

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

1.Executor可以将任务的提交过程与执行过程解耦开来,用Runnable表示任务。
2.Executor基于生产者-消费者模式。提交任务的操作相当于生产者,执行任务的线程则相当于消费者。

6.2.1 基于Executor的web服务器

public class TaskExecutionWebServer {
    private static final int NTHREADS = 100;
    private static final Executor exec = Executors.newFixedThreadPool(NTHREADS);

    public static void main(String[] args)throws IOException {
        ServerSocket serverSocket = new ServerSocket(80);
        while (true){
            final Socket connection = serverSocket.accept();
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    //handleRequest(connection)
                }
            };
            exec.execute(task);
        }
    }
}

6.2.2执行策略

  • 在什么(what)线程中执行任务?
  • 任务按照什么(what)顺序执行?(FIFO,LIFO,优先级)
  • 有多少(how many)个任务能并发执行?
  • 在队列中有多少(how many)个任务在等待执行?
  • 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个(which)任务?如何(how)通知应用程序有任务被拒绝?

6.2.3线程池

1.使用线程池的优势:

  • 重用现有的线程代替创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销。
  • 请求到达时,工作线程通常已经存在,因此不会因为由于等待创建线程而延迟任务的执行,从而提高了响应性。
  • 通过适当调整线程池的大小,可以创建合适数量的线程使cpu保持忙碌状态,还可以防止线程创建过多相互竞争资源导致应用程序耗尽内存或失败。

2.通过调用Executors中的静态工厂方法来创建线程池:

  • newFixedThreadPool:固定长度的线程池
  • newCachedThreadPool:创建一个可缓存的线程池,如果线程池的规模超过了当前的需求,那么回收当前的空闲线程;当需求增加时,可以添加新的线程,线程池的规模不存在任何限制。
  • newSingleThreadPool:单线程的Executor, 创建单个线程来执行任务,如果发生异常导致线程结束,则创建一个新的。同时,它还能保证任务在队列中按照顺序串行执行。

6.2.4Executor的生命周期

1.Executor扩展了ExecutorService接口,添加了一些用于生命周期管理的方法。
在这里插入图片描述
2.ExecutorService生命周期3种状态:

  • 运行:在初始创建时处于运行状态
  • 关闭:shutdown方法执行平缓的关闭过程:不再接受新的任务,同时等待已经提交的任务执行完成,包括未开始执行的任务。shutdownNow方法执行粗暴的关闭过程:它将尝试取消所有运行种的任务,并且不再启动队列中尚未开始执行的任务。
  • 终止:ExecutorService关闭后的提交任务将由“拒绝执行处理器”来处理,它会抛出异常,或者使得execute方法抛出一个未检查的RejectedExecutionException。等所有任务完成后,ExecutorService进入终止状态。
 public void shutdown() {
       final ReentrantLock mainLock = this.mainLock;
       mainLock.lock();
       try {
           checkShutdownAccess();
           advanceRunState(SHUTDOWN);
           interruptIdleWorkers();
           onShutdown(); // hook for ScheduledThreadPoolExecutor
       } finally {
           mainLock.unlock();
       }
       //尝试进入终止状态
       tryTerminate();
   }

支持关闭操作的web服务器:

public class LifecycleWebServer{
	private final ExecutorService exec = Executors.newCachedThreadPool();
    public void start()throws IOException{
   		ServerSocket server = new ServerSocket(80);
   		while(!exec.isShutdown()){
   				try{
   						final Socket socket = server.accept();
   						exec.execute(new Runnable(){
   							public void run(){
   											handleRequest(socket);
   									}
   						});
   				}catch(RejectedExecutionException){
   						if(!exec.isShutdown())
   									...
   				}
   		}
}
	public void stop(){
		exec.shutdown();
	}
	public void handleRequest(Socket socket){
			Request request = readRequest(socket);
			if(isShutDownRequest(socket){
				stop();
			}else
				dispatchRequest(req);
	}
}

6.2.5延迟任务与周期任务

1.推荐使用ScheduledThreadPoolExecutor来执行延迟任务与周期任务。
2.Timer类的问题:

  • 如果某个任务的执行时间过长,那么将破坏其他TimerTask的定时精准性。比如一个周期任务每10ms执行一次,而另一个任务执行40ms,那么这个任务或者在40ms任务执行完毕之后被连续调用4次,或者被彻底丢弃。
  • Timer类不捕获异常。
    3.可以使用DelayQueue构建调度服务。

错误的Timer行为:

6.3找出可利用的并行性

6.3.1串行的页面渲染器

先绘制文本元素,同时为图像预留出矩形的占位空间,在处理完了第一遍文本后,程序再开始下载图像,并将它们绘制到相应的占位空间中。

public class SingleThreadRenderer {
    void renderer(CharSequence source){
		//1.绘制文本元素
        renderText(source);
        //2.下载图像
        List<ImageData> imageData = new ArrayList<>();
        for (ImageInfo imageInfo:imageData){
            imageData.add(imageInfo.downloadImage());

        }
        //3.绘制图像
        for (ImageData data:imageData){
            renderImage(data);
        }
    }
}

6.3.2携带结果的任务CallableFuture

1.Runnable不能返回一个值或者抛出一个受检查的异常。
2.许多实际任务都存在延迟的计算:执行数据库查询,从网络上获取资源,或者计算某个复杂的功能。
3.Callable:它认为主入口点(即Call)将返回一个值,并可能抛出异常。
4.RunnableCallable描述的都是抽象的计算任务。这些任务都有一个明确的起点,并且最终都会结束。
5.Executor执行的任务有4个生命周期阶段:创建,提交,开始,完成。已提交但尚未开始的任务可以取消,已经开始执行的任务,只有在响应中断时,才能提交。
6.Future表示一个任务的生命周期。提供了相应的方法来判断是否已经完成或者取消,以及获取任务的结果和取消任务等。
7.get方法取决于任务的状态。如果已经完成,那么会立即返回或者抛出异常;如果任务还未完成,那么get会阻塞并一直等待任务完成。如果任务抛出了异常,get将该异常封装为ExecutionException并重新抛出。如果任务被取消,那么get将抛出CancelException。如果get抛出了ExecutionException,那么可以通过getCause来获得被封装的初始异常。
8.ExecutorService中的所有submit()方法都将返回一个Future,从而将一个Runnable或者Callable提交给Executor,得到一个Future,还可以指定某个Runnable或者Callable实例化一个FutureTask。它实现了Runnable,可以直接调用其run方法,或者提交给Executor

6.3.3使用Future实现页面渲染器

1.为了提高页面渲染器的并发性,将其分为两个任务,一个是渲染所有文本(CPU密集型),另一个是下载所有的图像(I/O密集型)。

public class FutureRenderer {

    private final ExecutorService executorService = Executors.newCachedThreadPool();

    void renderPage(CharSequence source){
        final List<ImageInfo> imageInfos = scanForImageInfo(source);
        //创建一个Callable来下载所有图像
        Callable<List<ImageData>> task = new Callable<List<ImageData>>() {
            @Override
            public List<ImageData> call() throws Exception {
                List<ImageData> result = new ArrayList<>();
                for (ImageInfo imageInfo:imageInfos)
                    result.add(imageInfo.downloadImage());
                return result;
            }
        };
        //提交到executorService
        Future<List<ImageData>> future = executorService.submit(task);
        renderText(source);

        try {
            List<ImageData> imageData = future.get();
            for (ImageData data:imageData){
                renderImage(data);
            }
        }catch (InterruptedException e){
            //重新设置线程的中断状态
            Thread.currentThread().interrupt();
            //由于不需要结果,取消任务
            future.cancel(true);
        }catch (ExecutionException e){
            throw launderThrowable(e.getCause());
        }
    }
}

Future.get的异常处理代码将处理两个可能的问题:任务遇到一个Exception,或者调用get的线程在获得结果之前被中断。
FutureRenderer使得渲染文本与下载图像数据的任务并发执行,当所有图像下载完毕后,会显示到页面上。

6.3.4在异构任务并行化中存在的局限

1.FutureRenderer使用了两个任务,一个负责文本渲染,一个负责图像下载。如果渲染文本的速度远远大于下载图像的速度。那么最终性能与串行执行时的性能差别不大,而代码结构却变得更复杂了。
2.只有大量相互独立且同构的任务可以并发处理时,才能体现出并发带来的真正的性能提升。

6.3.5CompletionService:Executor与BlockingQueue

1.CompletionServiceExecutorBlockingQueue的功能融合在一起。
在这里插入图片描述
2.其在构造函数中创建一个BlockingQueue来保存计算完成的结果。
构造函数

3.当提交一个任务时,该任务首先将被包装为一个QueueingFuture

  public Future<V> submit(Callable<V> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task);
        //将其包装为一个QueueingFuture
        executor.execute(new QueueingFuture(f));
        return f;
    }

QueueingFuture.done()方法将其加入到BlockingQueue中。

6.3.6示例:使用CompletionService实现页面渲染器

使用CompletionService为每幅图像的下载都创建一个独立任务,并在线程池中运行他们,减少下载图像的总时间。下载完成后从CompletionService中获取结果以及使每张图片在下载完成后立即显示出来,提高响应性。

public abstract class Renderer {
    private final ExecutorService executorService;

    public Renderer(ExecutorService executorService){
        this.executorService = executorService;
    }

    void renderPage(CharSequence source){
        final List<ImageInfo> imageInfos = scanForImageInfo(source);
        CompletionService<ImageData> completionService = new ExecutorCompletionService<ImageData>(executorService);
        for (ImageInfo imageInfo:imageInfos){
        //每个图片下载创建一个任务
            completionService.submit(new Callable<ImageData>() {
                @Override
                public ImageData call() throws Exception {
                    return imageInfo.downloadImage();
                }
            });
            renderText(source);
            try {
                for (int t = 0,n = imageInfos.size();t < n;t++){
                    Future<ImageData> f = completionService.take();
                    //每张图片下载完成后立即显示
                    ImageData imageData = f.get();
                    renderImage(imageData);
                }
            }catch (InterruptedException e){
                Thread.currentThread().interrupt();
            }catch (ExecutionException e){
                throw launderThrowable(e.getCause());
            }
        }
    }



    interface ImageData{}
    interface ImageInfo{
        ImageData downloadImage();
    }
    abstract void renderText(CharSequence s);
    abstract List<ImageInfo> scanForImageInfo(CharSequence s);
    abstract void renderImage(ImageData i);
}

6.3.7为任务设置时限

示例:一个门户网站可以从多个数据源并行地获取数据,但可能只会在指定的时间内等待数据,如果超出了等待时间,那么只显示已经获取的数据。

Page renderPageWithAd() throws InterruptedException{
	long endNanos = System.nanoTime()+Time_BUDGET;
	Future<Ad> f = exec.submit(new FetchAdTask());
	//在等待广告的同时显示页面
	Page page = renderPageBody();
	Ad ad;
	try{
		long timeLeft = endNanos - System.nanoTime();
		ad = f.get(timeLeft,NANOSECONDS);
	}catch(ExecutionException e){
		 ad = DEFAULT_AD;
	}catch(TimeoutException e){\
		ad = DEFAULT_AD;
		f.cancel(true);
	}
	page.setAd(ad);
	return page;
}

6.3.8示例:旅行预定门户网站

需求:用户输入旅行的日期和其他要求,门户网站获取并显示来自多条航线、旅店或汽车租赁公司的报价。
分析:

  • 从一个公司获得报价的过程与从其他公司获得报价的过程无关,因此可以将获取报价的过程作为一个任务,从而并发执行。
  • 创建n个任务,将其提交到一个线程池,保留n个future,并使用限时的get方法通过future串行地获取每一个结果。或者使用invokeAll方法。
/**
	QuoteTask.java
**/
public class QuoteTask implements Callable<TravelQuote>{
	private final TravelCompany company;
	private final TravelInfo travelInfo;
	public QuoteTask(TravelCompany company,TravelInfo travelInfo)
	{
		this.company = company;
		this.travelInfo = travelInfo;
	}
	TravelQuote getFailureQuote(Throwable t){
		return null;
	}
	TravelQuote getTimeoutQuote(CancellationException e){
		return null;
	}
	public TravelQuote call() throws Exception{
		return company.solicitQuote(travelInfo);
	}
}
interface TravelCompany{
	TravelQuote solicitQuote(TravelInfo travelInfo)throws Exception;
}
interface TravelQuote{}
interface TravelInfo{}
/*******
* TimeBudget.java
********/
public class TimeBudget{
	private static ExecutorService exec = Executors.newCachedThreadPool();
	public  List<TravelQuote> getRankedTravelQuotes(TravelInfo travelInfo,Set<TravelCompany> companies,Comparator<TravelQuote> ranking,long time,TimeUnit unit)throws InterruptedException{
		List<QuoteTask> tasks = new ArrayList<QuoteTask>();
		for(TravelCompany company:companies)
			tasks.add(new QuoteTask(company,travelInfo));
			//invokeAll按照任务集合中迭代器的顺序将所有的Future添加到返回集合中
		List<Future<TravelQuote>> futures = exec.invokeAll(tasks,time,unit);
		List<TravelQuote> quotes  = new ArrayList<>(tasks.size());
		Iterator<QuoteTask> taskIter = tasks.iterator();
		for(Future<TravelQuote> f :futures){
			QuoteTask task = taskIter.next();
			try{
				quotes.add(f.get());
			}catch(ExecutionException e){
				quotes.add(task.getFailureQuote(e.getCause()));
			}catch(TimeoutException e){
				quotes.add(task.getTimeoutQuote(e));
			}
		}
		Collections.sort(quotes,ranking);
		return quotes;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值