一、在任务和执行策略之间隐性耦合

Executor框架将任务的提交和它的执行策略解耦开来。虽然Executor框架为制定和修改执行策略提供了相当大的灵活性,但并非所有的任务都能适用所有的执行策略。

  • 依赖性任务:依赖其他同步任务的结果,使其不得不顺序执行,影响活跃性
  • 使用线程封闭的任务:在单线程的Executor中执行,任务可以不是线程安全的,但是一旦提交到线程池时,就会失去线程安全
  • 对响应时间敏感的任务:在单个线程或含有少量线程的线程池中执行是不可接受的
  • 使用ThreadLocal的任务:ThreadLocal使每个线程都可以拥有某个变量的一个私有"版本",而线程池中的线程是重复使用的,即一次使用完后,会被重新放回线程池,可被重新分配使用。因此,ThreadLocal线程变量,如果保存的信息只是针对一次请求的,放回线程池之前需要清空这些Threadlocal变量的值(或者取得线程之后,首先清空这些Threadlocal变量的值)

只有任务都是同类型并且相互独立时,线程池的效率达到最佳

1、线程饥饿死锁——在线程池中所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞

  例1:在单线程池中,正在执行的任务阻塞等待队列中的某个任务执行完毕

  例2:线程池不够大时,通过栅栏机制协调多个任务时

  例3:由于其他资源的隐性限制,每个任务都需要使用有限的数据库连接资源,那么不管线程池多大,都会表现出和和连接资源相同的大小 

每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程"饥饿"死锁,因此需要在代码或配置Executor地配置文件中记录线程池地大小限制或配置限制

2、运行时间较长的任务

  线程池的大小应该超过有较长执行时间的任务数量,否则可能造成线程池中线程均服务于长时间任务导致其它短时间任务也阻塞导致性能下降

缓解策略:限定任务等待资源的时间,如果等待超时,那么可以把任务标示为失败,然后中止任务或者将任务重新返回队列中以便随后执行。这样,无论任务的最终结果是否成功,这种方法都能确保任务总能继续执行下去,并将线程释放出来以执行一些能更快完成的任务。例如Thread.join、BlockingQueue.put、CountDownLatch.await以及Selector.select等

二、设置线程池的大小

线程池的理想大小取决于被提交任务的类型及所部署系统的特性

  • 线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争,这不仅会导致更高的内存使用量,而且还可能耗尽资源
  • 如果线程池过小,那么将导致许多空闲的处理器无法执行工作,从而降低吞吐量

对于计算密集型的任务,在拥有Ncpu个处理器的系统上,当线程池的大小为Ncpu+1时,通常能实现最优的利用率;对于包含I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大

N(threads)=N(cpu)*U(cpu)*(1+W/C)   N(cpu)=CPU的数量=Runtime.getRuntime().availableProcessors(); U(cpu)= 期望CPU的使用率,0<=U(cpu)<=1 ;W/C=等待时间与运行时间的比率

三、配置ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

1、线程的创建与销毁

  • CorePoolSize: 线程池基本大小,在创建ThreadPoolExecutor初期,线程并不会立即启动,而是等到有任务提交时才会启动,除非调用prestartAllCoreThreads,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。
  • MaxmumPooSize: 线程池最大大小表示可同时活动的线程数量的上限。若某个线程的空闲时间超过了keepAliveTime, 则被标记为可回收的

newFixedThreadPool: CorePoolSize = MaxmumPoolSize

newCachedThreadPool: CorePoolSize=0,MaxmumPoolSize=Integer.MAX_VALUE,线程池可被无限扩展,需求降低时自动回收

2、管理队列任务

  • workQueue:用于保存超过线程池线程处理速率的Runnable任务的队列 (三种:无界队列、有界队列和同步移交)

newFixedThreadPool和newSingleThreadPool在默认情况下将使用一个无界的LinkedBlockingQueue,有更好的性能

使用有界队列有助于避免资源耗尽的情况发生,为了避免当队列填满后,在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节,能防止过载

对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素,任务会直接移交给执行它的线程,否则将拒绝任务。newCachedThreadPool工厂方法中就使用了SynchronousQueue

使用优先队列PriorityBlockingQueue可以控制任务被执行的顺序

3、饱和策略

  • AbortPolicy(中止策略),默认的饱和策略。会抛出RejectedExecutionException异常(抛弃当前任务vs抛弃最旧任务)
  • 调用者运行:下一个任务在调用了execute方法的主线程中进行运行,主线程至少在一段时间内不能提交任何任务。到达的请求将被保存在TCP层的队列中而不是在应用程序的队列中,导致服务器在高负载下实现一种平缓的性能降低

其他:对执行策略进行修改,使用信号量,控制处于执行中的任务

public class BoundedExecutor {
private final Executor exec;
private final Semaphore semaphore; public BoundedExecutor(Executor exec, int bound) {
this.exec = exec;
this.semaphore = new Semaphore(bound);
} public void submitTask(final Runnable command){
try {
semaphore.acquire(); //提交任务前请求信号量
exec.execute(new Runnable() {
@Override
public void run() {
try{
command.run();
} finally{
semaphore.release(); //执行完释放信号
}
}
});
} catch (InterruptedException e) {
// handle exception
}
}
}

4、线程工厂

通过自定义线程工厂可以对其进行扩展加入新的功能实现

当应用需要利用安全策略来控制某些特殊代码库的访问权,可以利用PrivilegedThreadFactory来定制自己的线程工厂,以免出现安全性异常。将与创建privilegedThreadFactory的线程拥有相同的访问权限、AccessControlContext和contextClassLoader

自定义线程工厂
 public class MyThreadFactory implements ThreadFactory {
private final String poolName; public MyThreadFactory(String poolName) {
super();
this.poolName = poolName;
} @Override
public Thread newThread(Runnable r) {
return new MyAppThread(r);
}
} public class MyAppThread extends Thread {
public static final String DEFAULT_NAME="MyAppThread";
private static volatile boolean debugLifecycle = false;
private static final AtomicInteger created = new AtomicInteger();
private static final AtomicInteger alive = new AtomicInteger();
private static final Logger log = Logger.getAnonymousLogger(); public MyAppThread(Runnable r) {
this(r, DEFAULT_NAME);
} public MyAppThread(Runnable r, String name) {
super(r, name+ "-" + created.incrementAndGet());
setUncaughtExceptionHandler( //设置未捕获的异常发生时的处理器
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
log.log(Level.SEVERE, "UNCAUGHT in thread " + t.getName(), e);
}
});
} @Override
public void run() {
boolean debug = debugLifecycle;
if (debug)
log.log(Level.FINE, "running thread " + getName());
try {
alive.incrementAndGet();
super.run();
} finally {
alive.decrementAndGet();
if (debug)
log.log(Level.FINE, "existing thread " + getName());
}
}
}

5、在调用构造函数后在定制ThreadPoolExecutor

  • 可以在创建线程池后,再通过Setter方法设置其基本属性(将ExecutorService扩展为ThreadPoolExecutor)
  • 在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其只暴露出ExecutorService的方法,因此不能对它进行配置

四、扩展ThreadPoolExecutor

ThreadPoolExecutor使用了模板方法模式,提供了beforeExecute、afterExecute和terminated扩展方法

  • 线程执行前调用beforeExecute(如果beforeExecute抛出了一个RuntimeException,那么任务将不会被执行)
  • 线程执行后调用afterExecute(抛出异常也会调用,如果任务在完成后带有一个Error,那么就不会调用afterExecute)
  • 在线程池完成关闭操作时调用terminated,也就是所有任务都已经完成并且所有工作者线程也已经关闭后
增加日志和记时等功能的线程池
 public class TimingThreadPoolExecutor extends ThreadPoolExecutor {
private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();//任务执行开始时间
private final Logger log = Logger.getAnonymousLogger();
private final AtomicLong numTasks = new AtomicLong(); //统计任务数
private final AtomicLong totalTime = new AtomicLong(); //线程池运行总时间 public TimingThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
} @Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
log.fine(String.format("Thread %s: start %s", t, r));
startTime.set(System.nanoTime());
} @Override
protected void afterExecute(Runnable r, Throwable t) {
try{
long endTime = System.nanoTime();
long taskTime = endTime - startTime.get();
numTasks.incrementAndGet();
totalTime.addAndGet(taskTime);
log.fine(String.format("Thread %s: end %s, time=%dns", t, r, taskTime));
} finally{
super.afterExecute(r, t);
}
} @Override
protected void terminated() {
try{
//任务执行平均时间
log.info(String.format("Terminated: average time=%dns", totalTime.get() / numTasks.get()));
}finally{
super.terminated();
}
}
}

五、递归算法的并行化

  • 如果循环中的迭代操作都是独立的,并且不需要等待所有的迭代操作都完成再继续执行,那么就可以使用Executor将串行循环转化为并行循环
  • 如果需要提交一个任务集并等待它们完成,那么可以使用ExecutorService.invokeAll
  • 如果递归执行的任务中,在每个迭代操作中都不需要来自于后续递归迭代的结果,可以创建一个特定于遍历过程的Executor,并使用shutdown和awaitTermination等方法,等待上面并行运行的结果

java并发编程实战:第八章----线程池的使用的更多相关文章

  1. Java 并发编程——Executor框架和线程池原理

    Eexecutor作为灵活且强大的异步执行框架,其支持多种不同类型的任务执行策略,提供了一种标准的方法将任务的提交过程和执行过程解耦开发,基于生产者-消费者模式,其提交任务的线程相当于生产者,执行任务 ...

  2. [Java并发编程(二)] 线程池 FixedThreadPool、CachedThreadPool、ForkJoinPool?为后台任务选择合适的 Java executors

    [Java并发编程(二)] 线程池 FixedThreadPool.CachedThreadPool.ForkJoinPool?为后台任务选择合适的 Java executors ... 摘要 Jav ...

  3. [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ...

    [Java并发编程(一)] 线程池 FixedThreadPool vs CachedThreadPool ... 摘要 介绍 Java 并发包里的几个主要 ExecutorService . 正文 ...

  4. Java 并发编程——Executor框架和线程池原理

    Java 并发编程系列文章 Java 并发基础——线程安全性 Java 并发编程——Callable+Future+FutureTask java 并发编程——Thread 源码重新学习 java并发 ...

  5. Java并发编程、多线程、线程池…

    <实战java高并发程序设计>源码整理https://github.com/petercao/concurrent-programming/blob/master/README.md Ja ...

  6. Java并发编程之深入理解线程池原理及实现

    Java线程池在实际的应用开发中十分广泛.虽然Java1.5之后在JUC包中提供了内置线程池可以拿来就用,但是这之前仍有许多老的应用和系统是需要程序员自己开发的.因此,基于线程池的需求背景.技术要求了 ...

  7. Java并发编程(08):Executor线程池框架

    本文源码:GitHub·点这里 || GitEE·点这里 一.Executor框架简介 1.基础简介 Executor系统中,将线程任务提交和任务执行进行了解耦的设计,Executor有各种功能强大的 ...

  8. Java并发编程(十一)线程池的使用

    1.new Thread的弊端如下: a. 每次new Thread新建对象性能差. b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom. c. 缺乏更多 ...

  9. java并发编程(四) 线程池 & 任务执行、终止源码分析

    参考文档 线程池任务执行全过程:https://blog.csdn.net/wojiaolinaaa/article/details/51345789 线程池中断:https://www.cnblog ...

  10. Java并发编程:4种线程池和缓冲队列BlockingQueue

    一. 线程池简介 1. 线程池的概念: 线程池就是首先创建一些线程,它们的集合称为线程池.使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动 ...

随机推荐

  1. ORA-09817: Write to audit file failed 的解决

    今天在进行awr报表导出时,用sys as sysdba 登录,不能connect,报ORA-09817: Write to audit file failed 错误,是系统空间不足的报警.df -l ...

  2. java中的变量和常量

    也可以先声明后赋值  自动类型转换 1.  目标类型能与源类型兼容,如 double 型兼容 int 型,但是 char 型不能兼容 int 型 2.  目标类型大于源类型,如 double 类型长度 ...

  3. WinForm中Application.Idle事件用法

    Application.Idle 事件 描述:当应用程序完成处理并即将进入空闲状态时发生.如果您有必须执行的任务在线程变为空闲之前,请将它们附加到此事件. public partial class F ...

  4. python-ini文件使用(读和写)

    注意事项: 1.读文件: read(filename):读取ini文件中的内容 sections():得到所有section,返回列表形式 options(section):得到给定section的所 ...

  5. java高并发总结-常用于面试复习

    定义: 独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性. 共享锁则是一种乐观锁, ...

  6. 一次使用 Redis 优化查询性能的实践

    因为我的个人网站 restran.net 已经启用,博客园的内容已经不再更新.请访问我的个人网站获取这篇文章的最新内容,一次使用 Redis 优化查询性能的实践 应用背景 有一个应用需要上传一组ID到 ...

  7. 可视化库-Matplotlib-直方图(第四天)

    1.plt.hist(array, bins, color)  # array表示数值, bins表示的是bin的范围 data = np.random.normal(0, 20, 1000) # 画 ...

  8. 6 MySQL--表--完整性约束

    参考:https://www.cnblogs.com/alice-bj/p/8824693.html 完整性约束: http://www.cnblogs.com/linhaifeng/articles ...

  9. 用API处理位图

    procedure TForm1.Button1Click(Sender: TObject); var dc : hdc; MemDc : hdc; MemBitmap : hBitmap; OldM ...

  10. 一些jquery常用方法

    1.jquery实现平滑滚动到指定锚点 $(document).ready(function() { $("a.topLink").click(function() { $(&qu ...