Java并发编程----线程池技术(二)

目录

线程的创建与销毁

管理队列任务

线程工厂

饱和策略

线程池配置及执行流程


    Java类库中提供的线程池创建方式常见有四种,通过调用Executors类中的静态方法可以实现不同策略的线程池技术,其中newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor是通过访问ThreadPoolExecutor构造函数实现的,而newScheduleThreadPool是直接继承ThreadPoolExecutor类,通过newSchduleThreadPool的构造函数访问ThreadPoolExecutor构函数来实现。

    ThreadPoolExecutor是比较灵活,稳定的线程池,如果Executors中执行策略不能满足需求,可以通过ThreadPoolExecutor对象来时实例化一个对象,指定参数来定制线程池。ThreadPoolExecutor中定义了多个构造函数,其中比较常见的形式如下:

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

对于ThreadPoolExecute构造函数中的几个参数分以下部分进行介绍。

线程的创建与销毁

线程池的基本大小(corePoolSize),最大数量(maximumPoolSize)以及存活时间(keepAliveTime)等几个参数共同负责线程池的创建与销毁。

参数类型参数名描述
int corePoolSize线程池的基本大小
intmaximumPoolSize线程池的最大数量
longkeepAliveTime超过线程基本大小的线程在空闲时存活时间
TimeUnitunitkeepAliveTime参数的时间单位

    corePoolSize是线程池基本大小,创建线程池初期,线程并不会立即启动,而当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使有其他空闲的线程也会创建线程,直到线程数量达到线程池基本大小。即使在没有任何任务可执行时,线程池数量是corpPoolSize。如果调用了线程池中的prestartAllCoreThreads()方法,线程池会提前创建并启动所有的基本线程。

    maximumPoolSize是线程池的最大数量,线程池中的工作队列被任务填满后会创建超过corePoolSize数量的线程,而阈值不会超过maximumPoolSize,也就是线程池最大数量表示的是可同时活动的线程数量的上限。

    如果某些线程空闲时间超过了keepAliveTime存活时间,表示线程没有任务可执行,那么这些线程将会被标记为可回收的,并且当线程池的当前大小超过了基本大小corePoolSize时,多余的空闲线程将会被终止。

    TimeUnit是线程保持活动的时间单位,TimeUnit是枚举类型,枚举值有:DAYS(天),HOURS(小时),MINUTES(分钟),SECONDS(秒),MILLISECONDS(毫秒,千分之一秒),MICROSECONDS(微秒,千分之一毫秒),NANOSECONDS(纳秒,千分之一微秒)。

    对于java类库中的线程池,翻看方法源码可以看到相应的参数,如下:

创建方法corePoolSizemaximumPoolSizekeepAliveTimeunit

Executors.newFixedThreadPool

(int  nThreads)

nThreadsnThreads0TimeUnit.MILLISECONDS
Executors.newSingleThreadExecutor110TimeUnit.MILLISECONDS
Executors.newCachedThreadPool()0Integer.MAX_VALUE60TimeUnit.SECONDS

Executors.newScheduledThreadPool

(int  nThreads)

nThreadsInteger.MAX_VALUE0NANOSECONDS

    Executors.newFixedThreadPool工厂方法将线程池中基本大小和最大大小都设置为参数中指定的值,线程空闲存活时间是0,线程不会长期空闲而终止。Executors.newSingleThreadExecutor是指定参数为1的是FixedThreadPool。Executors.newCachedThreadPool工厂方法将基本大小设置为零,最大大小位置为Integer.MAX_VALUE,这种方法创建的线程可以无限扩展,并且当需求降低时会自动收缩,线程存活时间keepAliveTime设置为60秒,表示的是空闲线程超过60秒将会被终止。

管理队列任务

与管理对列任务相关的参数如下:

参数类型参数名描述
BlockingQueue<Runnable>workQueue任务队列

    有限的线程池会限制并发执行的任务数量,无限制地创建线程将导致系统不稳定性。通过采用固定线程池大小(非以为每个任务创建一个线程)来解决这个问题,此方案还是有问题。在高负载的情况下,应用程序仍可能会耗尽资源,只是出现问题的概率叫较小。如果请求的到达速率超过了线程池的处理速率,那么新到来的请求将会累计起来。在线程池中,这些请求会在Executor管理的Runnable队列中等待,而不会像线程那样去竞争CPU资源。将等待中的任务放到队列中比使用线程来表示的开销低很多,但如果客户提交给服务器的请求速率超过了服务器的处理速率,那么仍可能会耗尽资源。

    即使请求的平均到达速率很稳定,也仍然会出现请求突增的情况,尽管队列有助于缓解任务的突增问题,但是如果任务持续的高速地到来,那么最终还是会抑制请求的到达率以避免耗尽内存,甚至在耗尽内存之前,响应性能也随着任务队列的增长而变的越来越糟糕。

    ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务,基本的任务排队方法有3中:无界队列、有界队列和同步移交。

线程池名称队列名称队列类型
FixedThreadPoolLinkedBlockingQueue<Runnable>无界队列(链表结构的阻塞队列)
SingleThreadExecutorLinkedBlockingQueue<Runnable>无界队列(链表结构的阻塞队列)
CachedThreadPoolSynchronousQueue<Runnable>同步移交(无存储元素的阻塞队列)
ScheduledThreadPoolExecutorDelayedWorkQueue无界优先级队列
其他
----ArrayBlockingQueue有界阻塞队列
----PriorityBlockingQueue无界优先级队列

1)无界队列

    newFixedThreadPool和newSingleThreadExecutor在默认情况下使用一个无界的LinkedBlockingQueue。其容量的默认值是Integer.MAX_VALUE,如果所有的工作者线程处于忙碌状态,那么任务将在队列中等候。如果任务持续快速到达,并且超过了线程池处理它们的速度,那么队列将无限制地增加。

2)有界队列

    有界队列是一种比较合理的资管管理策略,如ArrayBlockingQueue,有界的LinkedBlockingQueue,PriorityBlockingQueue。有界队列有助于避免资源耗尽的情况发生。但当队列被填满之后,新的任务该如何处理?通常可以采用直接丢弃任务或者抛出异常等方法来解决,这种在队列被填满后所采取的措施称为饱和策略。使用有界队列时,队列的大小与线程池的大小必须一起调节。如果线程池比较大,那么有助于减少内存使用量,降低CPU的使用率,同时还可以减少上下文切换,但付出的代价是可能会限制吞吐量。                                        

3)同步移交

    对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。SynchronousQueue不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另外一个线程正在接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最小值,那么ThreadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,在newCachedThreadPool工厂方法中就是用了SynchronousQueue。

     当使用了像LinkedBlockingQueue或者ArrayBlockingQueue这样的FIFO(先进先出)队列时,任务的执行顺序与它们到达的顺序相同。如果想进一步控制任务的执行顺序,可以使用PriorityBlockingQueue,这个队列将根据优先级来安排任务。

    只有任务相互独立时,为线程或者工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程或者队列可能会冬至线程的“饥饿”死锁问题。此时应该使用无界的线程池,如newCachedThreadPool。

线程工厂

参数类型参数名描述
ThreadFactorythreadFactory用于创建线程的工厂

    类库中创建线程池方法参数中一般指定了线程池大小如Executors.newFixedThreadPool(nThreads),而另外一种创建线程池方法是Executors.newFixedThreadPool(nThreads,threadFactory),每当线程池需要创建一个线程时,都会调用线程工厂方法ThreadFactory来完成。默认的线程工厂方法将创建一个新的,非守护的线程,并且不包含特殊的配置信息。通过指定的线程工厂方法,可以定制线程池的配置信息。

    在ThreadFactory中只定义了一个方法newThread,每当线程池创建一个新线程时都会调用这个方法。

public interface ThreadFactory {
  Thread newThread(Runnable r);
}

    多种情况下都需要定制线程工厂,如下是一个简单的线程工厂方法的实现,指定线程的名称。线程池会调用newThread()方法创建线程。

public class FixedThreadPoolDemo {
  public static void main(String[] args) {
    ExecutorService exec = Executors.newFixedThreadPool(5,new ThreadFactory() {
      private int number = 0;
      public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        t.setName("Thread-pool"+"-"+(++number));
        return t;
      }
    });
    Runnable task =()->{
        try {
          TimeUnit.SECONDS.sleep(1);
          System.out.println(Thread.currentThread().getName() + " executing!!!");
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
    };
    for(int i =0 ; i< 5;i++) {
      exec.submit(task);
    }
    exec.shutdown();
  }
}

饱和策略

参数类型参数名描述
RejectedExecutionHandlerhandler饱和策略

    当线程池和队列都满之后,线程池处于饱和状态,此时必须采取一种策略来处理新提交的任务。ThreadPoolExecutor的饱和策略可以通过调用setRejectExecutionHandler来修改,如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略。JDK线程池框架提供了几种不同的RejectedExecutionHandler实现。

策略名称描述
AbortPolicy默认的饱和策略,该策略将抛出为检查的RejectedExecutionException.
CallerRunsPolicy一种调节机制,策略不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardPolicy新提交的任务会直接被抛弃
DiscardOldestPolicy抛弃下一个将被执行的任务,然后尝试提交新任务。

    案例:自定义线程池,线程池基本大小为2,最大大小为3,线程存活时间是0秒,队列使用的是有界的LinkedBlockingQueue,最多能存储4任务。线程池饱和后将采用饱和策略来处理。

public class RejectionExecutionHandlerDemo {
  public static void main(String[] args) {
    //设置线程池基本大小为2,最大大小为3,而队列能存储4个任务
    ThreadPoolExecutor exec = new ThreadPoolExecutor(2,3,0L,TimeUnit.SECONDS,new LinkedBlockingQueue<Runnable>(4));
    //1. abortPolicy策略:任务超过7个将会抛出异常。
  //exec.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
    // 2. callerRunsPolicy策略:不会抛弃任务,也不会抛出异常,将任务回退到调用者,此例
    //中会回退到主线程,让主线程执行
   //exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    //3. DiscardOldestPolicy:抛弃最老的任务
   // exec.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
    //4. DiscardPolicy:新提交的任务会被抛弃
    exec.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
    for(int i =0 ; i < 10;i++) {
      exec.submit(new Task());
    }
    exec.shutdown();
  }
  static class Task implements Runnable{
    private static int taskCount= 1;
    private final int id = taskCount++;
    @Override
    public void run() {
      try {
        TimeUnit.SECONDS.sleep(2);
      } catch (InterruptedException e) {
        System.out.println("Thread interrupted "+e.getMessage());
      }
      System.out.println(Thread.currentThread().getName()+":"+"task-"+id+" Exeuction finished!!!");
    }
    
  }
}

1.AbortPolicy:“中止策略”,默认的饱和策略,会直接抛出RejectionExecutionException异常。

设置饱和策略的代码是exec.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@55f96302 rejected from java.util.concurrent.ThreadPoolExecutor@3d4eac69[Running, pool size = 3, active threads = 3, queued tasks = 4, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
	at threadSocket.RejectionExecutionHandlerDemo.main(RejectionExecutionHandlerDemo.java:21)
pool-1-thread-1:task-1 Exeuction finished!!!
pool-1-thread-2:task-2 Exeuction finished!!!
pool-1-thread-3:task-7 Exeuction finished!!!
pool-1-thread-1:task-3 Exeuction finished!!!
pool-1-thread-3:task-5 Exeuction finished!!!
pool-1-thread-2:task-4 Exeuction finished!!!
pool-1-thread-1:task-6 Exeuction finished!!!

执行结果:执行了7个任务,第8个任务提交的时候,直接抛出了异常。

2.CallerRunsPolicy:“调用者运行策略”,此策略不会直接抛弃任务,也不回抛出异常,而是将任务退回到调用者。它不会在线程池里面某个线程中执行新提交的任务,而是在一个调用execute的线程中执行任务。

设置饱和策略的代码是exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

pool-1-thread-3:task-7 Exeuction finished!!!
main:task-8 Exeuction finished!!!
pool-1-thread-1:task-1 Exeuction finished!!!
pool-1-thread-2:task-2 Exeuction finished!!!
pool-1-thread-1:task-4 Exeuction finished!!!
pool-1-thread-3:task-3 Exeuction finished!!!
main:task-10 Exeuction finished!!!
pool-1-thread-2:task-5 Exeuction finished!!!
pool-1-thread-3:task-9 Exeuction finished!!!
pool-1-thread-1:task-6 Exeuction finished!!!

执行结果:所有任务得到了执行,2个任务在主线程中得到执行,其余任务在线程池中执行。

3.DiscardOldestPolicy:“抛弃最旧的策略”,策略将会抛弃下一个将被执行的任务,然后尝试提交新任务。如果工作队列是一个优先队列,那么“抛弃最旧的”将导致抛弃优先级最高的任务,因此最好不要将优先级队列和DiscardOldestPolicy一起使用。

设置饱和策略的代码是exec.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());

pool-1-thread-2:task-2 Exeuction finished!!!
pool-1-thread-1:task-1 Exeuction finished!!!
pool-1-thread-3:task-7 Exeuction finished!!!
pool-1-thread-1:task-8 Exeuction finished!!!
pool-1-thread-2:task-6 Exeuction finished!!!
pool-1-thread-3:task-9 Exeuction finished!!!
pool-1-thread-1:task-10 Exeuction finished!!!

执行结果:任务3,4,5,6被抛弃,后面添加的任务得到了执行。

4.DiscardPolicy:"抛弃策略",直接抛弃任务,线程池不作处理。

设置饱和策略的代码是exec.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

pool-1-thread-3:task-7 Exeuction finished!!!
pool-1-thread-2:task-2 Exeuction finished!!!
pool-1-thread-1:task-1 Exeuction finished!!!
pool-1-thread-3:task-4 Exeuction finished!!!
pool-1-thread-1:task-5 Exeuction finished!!!
pool-1-thread-2:task-3 Exeuction finished!!!
pool-1-thread-3:task-6 Exeuction finished!!!

执行结果:任务8,9,10直接被抛弃,前面的任务得到执行。

线程池配置及执行流程

1.线程池配置

    通过调用ThreadPoolExecutor构造函数来自定义线程池,对于上面所提到的几种参数,要仔细斟酌。合理配置线程池,可以使得线程池的执行任务效率提升,避免资源额外消耗。

    线程池理想大小取决于被提交任务的类型以及所部署系统的特性,通常线程池大小可以进行配置或者动态计算Runtime.getRuntime().availableProcessors(),避免将线程池大小设置为极大或者极小。如果线程池过大,那么大量的线程会在相对较少的CPU和内存资源上发生竞争,这会导致更高的内存使用量,还可能耗尽资源。如果线程池过小,那么将会导致许多空闲处理器无法执行工作,从而降低了吞吐率。

    可以根据任务是计算密集型还是IO密集型、任务执行时间长短、任务是否依赖数据库连接池这样的稀缺资源,任务优先级等几个方面, 将不同类型的任务使用不同规模的线程池来执行,从而使得每个线程池可以根据各自工作负载来调整,同一个线程池中的任务是用类型的并且是相互独立时(任务之间不相互依赖),线程池的性能才能达到最大。例如对于计算密集型的任务,在用于N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的利用率(额外的1个线程可以在有线程出现故障或者其他原因而暂停时,确保CPU的时钟周期不会被浪费)。

2.线程池的执行流程

对于ThreadPoolExecutor执行任务的方法execute()可以分为下面几个步骤:

步骤一:线程池中当前运行的线程数量少于corePoolSize,则会创建新线程来执行任务。

步骤二:如果当前运行的线程数量等于或者大于corePoolSize时,会将任务添加到队列BlockingQueue中。

步骤三:队列已经满后,无法将任务添加到队列BlockingQueue中,则会创建新线程来执行任务。

步骤四:如果创建新线程后,线程池中线程数量超过了maximumPoolSize,将会调用线程池ThreadPoolExecutor中的方法RejectedExecutionHandler.rejectedExecution()执行设置的饱和策略。

参考

《Java编程思想》《Java并发编程实战》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值