`
blueyanghualong
  • 浏览: 228538 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

【转】多线程应用思考

 
阅读更多

 

很多开发者谈到Java多线程开发,仅仅停留在new Thread(...).start()或直接使用Executor框架这个层面,对于线程的管理和控制却不够深入,通过读《Java并发编程实践》了解到了很多不为我知但又非常重要的细节,今日整理如下。

不应用线程池的缺点

有些开发者图省事,遇到需要多线程处理的地方,直接new Thread(...).start(),对于一般场景是没问题的,但如果是在并发请求很高的情况下,就会有些隐患:

新建线程的开销。线程虽然比进程要轻量许多,但对于JVM来说,新建一个线程的代价还是挺大的,决不同于新建一个对象
资源消耗量。没有一个池来限制线程的数量,会导致线程的数量直接取决于应用的并发量,这样有潜在的线程数据巨大的可能,那么资源消耗量将是巨大的
稳定性。当线程数量超过系统资源所能承受的程度,稳定性就会成问题
制定执行策略

在每个需要多线程处理的地方,不管并发量有多大,需要考虑线程的执行策略

任务以什么顺序执行
可以有多少个任何并发执行
可以有多少个任务进入等待执行队列
系统过载的时候,应该放弃哪些任务?如何通知到应用程序?
一个任务的执行前后应该做什么处理
线程池的类型

不管是通过Executors创建线程池,还是通过Spring来管理,都得清楚知道有哪几种线程池:

FixedThreadPool:定长线程池,提交任务时创建线程,直到池的最大容量,如果有线程非预期结束,会补充新线程
CachedThreadPool:可变线程池,它犹如一个弹簧,如果没有任务需求时,它回收空闲线程,如果需求增加,则按需增加线程,不对池的大小做限制
SingleThreadExecutor:单线程。处理不过来的任务会进入FIFO队列等待执行
SecheduledThreadPool:周期性线程池。支持执行周期性线程任务
其实,这些不同类型的线程池都是通过构建一个ThreadPoolExecutor来完成的,所不同的是corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,threadFactory这么几个参数。具体可以参见JDK DOC。

线程池饱和策略

由以上线程池类型可知,除了CachedThreadPool其他线程池都有饱和的可能,当饱和以后就需要相应的策略处理请求线程的任务,ThreadPoolExecutor采取的方式通过队列来存储这些任务,当然会根据池类型不同选择不同的队列,比如FixedThreadPool和SingleThreadExecutor默认采用的是无限长度的LinkedBlockingQueue。但从系统可控性讲,最好的做法是使用定长的ArrayBlockingQueue或有限的LinkedBlockingQueue,并且当达到上限时通过ThreadPoolExecutor.setRejectedExecutionHandler方法设置一个拒绝任务的策略,JDK提供了AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy几种策略,具体差异可见JDK DOC

线程无依赖性

多线程任务设计上尽量使得各任务是独立无依赖的,所谓依赖性可两个方面:

线程之间的依赖性。如果线程有依赖可能会造成死锁或饥饿
调用者与线程的依赖性。调用者得监视线程的完成情况,影响可并发量
当然,在有些业务里确实需要一定的依赖性,比如调用者需要得到线程完成后结果,传统的Thread是不便完成的,因为run方法无返回值,只能通过一些共享的变量来传递结果,但在Executor框架里可以通过Future和Callable实现需要有返回值的任务,当然线程的异步性导致需要有相应机制来保证调用者能等待任务完成,关于Future和Callable的用法见下面的实例就一目了然了:

public class FutureRenderer {  
    private final ExecutorService executor = ...;  
    void renderPage(CharSequence source) {  
        final List<ImageInfo> imageInfos = scanForImageInfo(source);  
        Callable<List<ImageData>> task =  
                new Callable<List<ImageData>>() {  
                    public List<ImageData> call() {  
                        List<ImageData> result  
                                = new ArrayList<ImageData>();  
                        for (ImageInfo imageInfo : imageInfos)  
                            result.add(imageInfo.downloadImage());  
                        return result;  
                    }  
                };  
        Future<List<ImageData>> future =  executor.submit(task);  
        renderText(source);  
        try {  
            List<ImageData> imageData =  future.get();  
            for (ImageData data : imageData)  
                renderImage(data);  
        } catch (InterruptedException e) {  
            // Re-assert the thread's interrupted status  
            Thread.currentThread().interrupt();  
            // We don't need the result, so cancel the task too  
            future.cancel(true);  
        } catch (ExecutionException e) {  
            throw launderThrowable(e.getCause());  
        }  
    }  
}  

以上代码关键在于List<ImageData> imageData = future.get();如果Callable类型的任务没有执行完时,调用者会阻塞等待。不过这样的方式还是得谨慎使用,很容易造成不良设计。另外对于这种需要等待的场景,就需要设置一个最大容忍时间timeout,设置方法可以在 future.get()加上timeout参数,或是再调用ExecutorService.invokeAll 加上timeout参数

线程的取消与关闭 

一般的情况下是让线程运行完成后自行关闭,但有些时候也会中途取消或关闭线程,比如以下情况:

调用者强制取消。比如一个长时间运行的任务,用户点击"cancel"按钮强行取消
限时任务
发生不可处理的任务
整个应用程序或服务的关闭
因此需要有相应的取消或关闭的方法和策略来控制线程,一般有以下方法:

1)通过变量标识来控制

这种方式比较老土,但使用得非常广泛,主要缺点是对有阻塞的操作控制不好,代码示例如下所示:

public class PrimeGenerator implements Runnable {  
     @GuardedBy("this")  
     private final List<BigInteger> primes  
             = new ArrayList<BigInteger>();  
     private  volatile boolean cancelled;  
     public void run() {  
         BigInteger p = BigInteger.ONE;  
         while (!cancelled ) {  
             p = p.nextProbablePrime();  
             synchronized (this) {  
                 primes.add(p);  
             }  
         }  
     }  
     public void cancel() { cancelled = true;  }  
     public synchronized List<BigInteger> get() {  
         return new ArrayList<BigInteger>(primes);  
     }  
}  

2)中断

中断通常是实现取消最明智的选择,但线程自身需要支持中断处理,并且要处理好中断策略,一般响应中断的方式有两种:

处理完中断清理后继续传递中断异常(InterruptedException)
调用interrupt方法,使得上层能感知到中断异常
3) 取消不可中断阻塞

存在一些不可中断的阻塞,比如:

java.io和java.nio中同步读写IO
Selector的异步IO
获取锁
对于这些线程的取消,则需要特定情况特定对待,比如对于socket阻塞,如果要安全取消,则需要调用socket.close()

4)JVM的关闭

如果有任务需要在JVM关闭之前做一些清理工作,而不是被JVM强硬关闭掉,可以使用JVM的钩子技术,其实JVM钩子也只是个很普通的技术,也就是用个map把一些需要JVM关闭前启动的任务保存下来,在JVM关闭过程中的某个环节来并发启动这些任务线程。具体使用示例如下:

public void start() {  
    Runtime.getRuntime().addShutdownHook(new Thread() {  
        public void run() {  
            try { LogService.this.stop(); }  
            catch (InterruptedException ignored) {}  
        }  
    });  
}  
分享到:
评论

相关推荐

    多线程应用程序设计

    ### 多线程应用程序设计 #### 实验背景与目的 多线程技术是现代软件开发中的重要组成部分,尤其是在嵌入式系统中,由于资源有限,合理利用多线程能够显著提升系统的性能和响应速度。本实验旨在通过具体的实践操作...

    多线程应用程序设计 报告

    ### 多线程应用程序设计知识点解析 #### 一、多线程程序设计基本原理 多线程编程是一种软件技术,允许在单个程序中并行执行多个任务或操作,从而提高程序的性能和响应能力。在多核处理器普及的今天,多线程编程...

    windows多线程总结

    理解并熟练运用这些Windows多线程编程的知识,能够帮助开发者构建高效、可靠的多线程应用,同时注意线程安全和资源管理,避免潜在的问题。在实际开发中,还需要结合具体的编程语言和库(如C++的Windows API或C#的...

    C++面向对象多线程编程

    《C++面向对象多线程编程》共分13章,全面讲解构建多线程架构与增量多线程编程技术。第1章介绍了用于构建面向对象程序的不同类型...第12章简单介绍多线程应用程序的测试技术。第13章对全书内容进行扼要地回顾与思考。

    C#多线程Demo

    在.NET框架中,C#语言提供了强大的多线程支持,使得开发者可以充分利用现代多核处理器的优势,提高程序的运行效率。本示例“C#多线程Demo”着重讲解了如何利用`System.Threading.Tasks.Parallel`类来实现并行处理。`...

    多核多线程杂谈-并行计算

    为了更好地理解多核多线程的概念,可以通过与现实生活中的铁路系统的类比来进行思考: - **增加列车数量**:类似于在多线程程序中增加更多的线程来提高程序的吞吐率。 - **资源竞争**:不同列车需要同时使用同一...

    Win32多线程程序设计

    不恰当的多线程应用可能导致程序性能下降,甚至出现错误。因此,是否采用多线程,以及如何设计多线程程序,需要根据实际的应用场景和性能要求慎重考虑。 在Win32平台下,开发多线程程序可以使用多种API函数,如_...

    在Delphi中利用多线程实现数据采集的方法.doc

    ### 在Delphi中利用多线程实现数据采集的方法 #### 概述 本文档主要介绍了如何在Delphi环境中使用多线程技术实现数据采集的基本原理与实现步骤。Delphi是一种功能强大的开发工具,广泛应用于Windows应用程序开发。...

    基于多线程的web服务器java源码

    【描述】多线程技术在Web服务器中的应用 多线程是Java编程中的核心特性,它允许程序同时执行多个任务,提高系统资源利用率和响应速度。在Web服务器中,多线程尤其重要,因为服务器需要处理来自不同客户端的并发请求...

    java实验报告【新】—多线程技术的应用分享.pdf

    实验报告主要围绕Java多线程技术展开,涵盖了两种创建线程的方法:继承Thread类和实现Runnable接口。在Java中,线程是程序执行的独立路径,可以并发运行,提高程序的执行效率。 首先,实验的第一个任务是通过继承...

    C++面向对象多线程编程(part2)

    (此书包含2个部分,大家下载时注意) 全书共分13章,全面讲解构建多线程...第12章简单介绍多线程应用程序的测试技术。第13章对全书内容进行扼要地回顾与思考。 本书适合用C++创建多线程组件和应用框架的程序员阅读。

    JAVA多线程游戏设计之中国象棋

    在Java编程领域,多线程技术是实现高效并发处理的关键,尤其在游戏设计中,它能让不同的游戏元素在同一时间运行...通过设计这样一个游戏,开发者不仅可以提升编程技能,还能深入理解多线程在实际应用中的重要性和挑战。

    哲学家多线程

    "哲学家多线程"是一种经典的并发编程问题,源自计算机科学家Dijkstra提出的一个思想实验,旨在探讨并发系统中的资源竞争和死锁问题。...这在实际开发中是非常重要的,特别是在多线程服务器应用和分布式系统设计中。

    C++ 多线程例子源代码

    在编程领域,尤其是在系统级和应用级开发中,多线程技术是不可或缺的一部分。C++作为一门强大的系统级编程语言,提供了丰富的支持来处理多线程编程。标题中的"C++ 多线程例子源代码"就是一个很好的学习资源,旨在...

    哲学家就餐多线程实例图形版

    《哲学家就餐问题在Java中的多线程实现》 哲学家就餐问题是计算机科学中的一个经典问题,用于模拟并发控制中的死锁现象。该问题由美国数学家及计算机科学家Edsger Dijkstra在1965年提出,旨在揭示并发程序设计中的...

    多线程的哲学家用餐程序

    通过解决这个问题,开发者可以深入理解多线程环境下的并发控制、资源分配和死锁预防策略,这对于开发高效、安全的多线程应用程序至关重要。 总结起来,"多线程的哲学家用餐程序"是理解和实践多线程编程的一个重要...

    炉边夜话---多核多线程杂谈

    - **并发编程**:在多核环境下,开发人员需要重新思考应用程序的设计,利用多线程或多进程等并发模型来提高程序的执行效率。 #### 3. 多核多线程编程面临的挑战 - **资源共享与同步**:在多线程环境中,线程之间...

    Linux多线程服务端编程:使用muduo C++网络库

    《Linux多线程服务端编程:使用muduo C++网络库》主要讲述采用现代C++在x86-64 Linux上编写多线程TCP网络服务程序的主流常规技术,重点讲解一种适应性较强的多线程服务器的编程模型,即one loop per thread。...

Global site tag (gtag.js) - Google Analytics