很多开发者谈到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的用法见下面的实例就一目了然了:

1. public class FutureRenderer {

2.     private final ExecutorService executor = ...;

3.     void renderPage(CharSequence source) {

4.         final List<ImageInfo> imageInfos = scanForImageInfo(source);

5.         Callable<List<ImageData>> task =

6.                 new Callable<List<ImageData>>() {

7.                     public List<ImageData> call() {

8.                         List<ImageData> result

9.                                 = new ArrayList<ImageData>();

10.                         for (ImageInfo imageInfo : imageInfos)

11.                             result.add(imageInfo.downloadImage());

12.                         return result;

13.                     }

14.                 };

15.         Future<List<ImageData>> future =  executor.submit(task);

16.         renderText(source);

17.         try {

18.             List<ImageData> imageData =  future.get();

19.             for (ImageData data : imageData)

20.                 renderImage(data);

21.         } catch (InterruptedException e) {

22.             // Re-assert the thread's interrupted status

23.             Thread.currentThread().interrupt();

24.             // We don't need the result, so cancel the task too

25.             future.cancel(true);

26.         } catch (ExecutionException e) {

27.             throw launderThrowable(e.getCause());

28.         }

29.     }

30. }

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

线程的取消与关闭

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

· 调用者强制取消。比如一个长时间运行的任务,用户点击"cancel"按钮强行取消

· 限时任务

· 发生不可处理的任务

· 整个应用程序或服务的关闭

因此需要有相应的取消或关闭的方法和策略来控制线程,一般有以下方法:

1)通过变量标识来控制

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

1. public class PrimeGenerator implements Runnable {

2.      @GuardedBy("this")

3.      private final List<BigInteger> primes

4.              = new ArrayList<BigInteger>();

5.      private  volatile boolean cancelled;

6.      public void run() {

7.          BigInteger p = BigInteger.ONE;

8.          while (!cancelled ) {

9.              p = p.nextProbablePrime();

10.              synchronized (this) {

11.                  primes.add(p);

12.              }

13.          }

14.      }

15.      public void cancel() { cancelled = true;  }

16.      public synchronized List<BigInteger> get() {

17.          return new ArrayList<BigInteger>(primes);

18.      }

19. }

2)中断

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

· 处理完中断清理后继续传递中断异常(InterruptedException)

· 调用interrupt方法,使得上层能感知到中断异常

3) 取消不可中断阻塞

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

· java.io和java.nio中同步读写IO

· Selector的异步IO

· 获取锁

对于这些线程的取消,则需要特定情况特定对待,比如对于socket阻塞,如果要安全取消,则需要调用socket.close()

4)JVM的关闭

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

1. public void start() {

2.     Runtime.getRuntime().addShutdownHook(new Thread() {

3.         public void run() {

4.             try { LogService.this.stop(); }

5.             catch (InterruptedException ignored) {}

6.         }

7.     });

Java多线程开发技巧的更多相关文章

  1. Java多线程开发系列之番外篇:事件派发线程---EventDispatchThread

    事件派发线程是java Swing开发中重要的知识点,在安卓app开发中,也是非常重要的一点.今天我们在多线程开发中,穿插进来这个线程.分别从线程的来由.原理和使用方法三个方面来学习事件派发线程. 一 ...

  2. Java多线程开发系列之四:玩转多线程(线程的控制2)

    在上节的线程控制(详情点击这里)中,我们讲解了线程的等待join().守护线程.本节我们将会把剩下的线程控制内容一并讲完,主要内容有线程的睡眠.让步.优先级.挂起和恢复.停止等. 废话不多说,我们直接 ...

  3. Java多线程开发系列之一:走进多线程

    对编程语言的基础知识:分支.选择.循环.面向对象等基本概念理解后,我们需要对java高级编程有一定的学习,这里不可避免的要接触到多线程开发. 由于多线程开发整体的系统比较大,我会写一个系列的文章总结介 ...

  4. java多线程开发,Executors、FutureTask、Callable

    java多线程如何应用呢,几乎学java的同学都知道Thread类和Runable接口.继承Thread类或者实现Runable接口,调用thread的start方法即可启动线程. 然后是线程池,就是 ...

  5. Java多线程开发系列之二:如何创建多线程

    前文已介绍过多线程的基本知识了,比如什么是多线程,什么又是进程,为什么要使用多线程等等. 在了解了软件开发中使用多线程的基本常识后,我们今天来聊聊如何简单的使用多线程. 在Java中创建多线程的方式有 ...

  6. Java Collection开发技巧

    Java Collection(集合) 集合中的一些技巧: 通过Collections类的静态方法,可以对集合进行一些操作 1 java.util.List<Integer> number ...

  7. Java多线程开发系列之四:玩转多线程(线程的控制1)

    在前文中我们已经学习了:线程的基本情况.如何创建多线程.线程的生命周期.利用已有知识我们已经可以写出如何利用多线程处理大量任务这样简单的程序.但是当应用场景复杂时,我们还需要从管理控制入手,更好的操纵 ...

  8. Java多线程开发系列之三:线程这一辈子(线程的生命周期)

    前文中已经提到了,关于多线程的基础知识和多线程的创建.但是如果想要很好的管理多线程,一定要对线程的生命周期有一个整体概念.本节即对线程的一生进行介绍,让大家对线程的各个时段的状态有一定了解. 线程的一 ...

  9. java多线程开发容易犯的错误

    昨天在社区上看到有人讨论多线程使用,多线程遇到一些问题以及一些使用技巧记录一下.为什么要使用多线程, 不能是为了用而用,和设计模式一样用的合理,会让程序更易于理解,用的不合理反而会让程序变得更难理解. ...

随机推荐

  1. * {margin:0px; padding:0px;}什么意思?

    * {margin:0px; padding:0px;} *  表示所有的元素的对齐方式以及和父类之间的间距都为0 body{margin:0px;padding:0px;} body里面的则表示的是 ...

  2. Ubuntu 14.10安装SecureCRT 7.3(转)

    原文 :http://blog.csdn.net/chszs/article/details/40623169 1.软件准备 Ubuntu14.04 x64 下载SecureCRT7.3的版本:scr ...

  3. sassCore

    core文件 setting 负责基础变量的文件,如常用的颜色,字体等变量. css3 负责css3属性前缀的文件.参考了bourbon,然后进行一系列的扩展及优化,以使解析出来的代码更加合理. me ...

  4. Codeigniter

    最近准备接手改进一个别人用Codeigniter写的项目,虽然之前也有用过CI,但是是完全按着自己的意思写的,没按CI的一些套路.用在公众的项目,最好还是按框架规范来,所以还是总结一下,免得以后别人再 ...

  5. 定制自己的linux系统

    算是总结式文档,以备后忘 先说一下背景,一开始我就知道有这个任务,同时我自己也非常感兴趣,打算去研究研究 于是才同意接这个任务,但是我的原意是从头开始搭建系统,也就是lfs 但接到任务后,由于种种原因 ...

  6. python模块学习心得

    初始模块 1.什么是模块 模块是用来实现某项功能的一大堆代码,为什么会有模块呢?过程式编程的时候为了减少程序员编程代码的重复性,就利用函数的调用减少了代码的重复性,但是某些时候程序会过于的庞大,我们会 ...

  7. 博客代码:iframe—网页中嵌入其他网页

    iframe 是一个可以把另外一个网页嵌入到一个网页里的代码,非常有用.对于一个内容不错的网页,要方便地把它搬到自己的博客里,用这个代码最合适.而对于在新浪博客里不支持的一些网页效果和代码,可先把他们 ...

  8. IUnknown(TVarData(Params[0]).VPointer) as Range

    IUnknown(TVarData(Params[0]).VPointer) as Range 修改为  IUnknown(TVarData(Params[0]).VPointer) as WOrd_ ...

  9. C6000系类的内联函数

    1.求绝对值函数 (1) _abs()  C代码  : int _abs(int src)  汇编: ABS  功能: 求32位数据的绝对值   (2) _labs()  C代码: int _labs ...

  10. 仿东软OA协同办公服务管理系统

    兼容IE6,7,8以上.GooleChrome.360及遨游等浏览器.系统特色:1.系统经过抗压测试.2.语音提示功能.3.支持office2007在线编辑.4.强大的图形化工作流程设计及文档编辑留痕 ...