【Java】多线程编程(二)

阻塞队列简介

介绍

  介绍阻塞队列的定义、种类、实现原理和使用场景有利于大家更好理解线程池。

  阻塞队列一般用于生产者和消费者场景:生产者在队列添加线程,消费者取出线程。队列就是这样一种元素存取的容器。

  阻塞队列满足以下必要条件:

  1. 队列数据为空,消费者端所有线程都会被阻塞(挂起),直到有数据放入队列。
  2. 在队列填满数据下,生产者端所有线程都会被阻塞(挂起),直到队列有空位。

BlockingQueue 阻塞队列核心方法

  阻塞队列用到的类大多是BlockingQueue的派生类,BlockingQueue只是java.util.concurrent包中的一个接口,但我们还是要说说其中核心方法:

放入数据

  • offer(Object): 将Object放入队列,可以容纳则返回true,否则false,本方法不阻塞当前执行方法线程。
  • offer(Object,long,TimeUnit):比上方法加了等待时间参数,在指定时间内无法加入元素,则返回失败。
  • put(Object):将Object加入队列,若没有空间,此方法被线程阻断,直到有空间才能继续。

获取数据

  • poll(long, TimeUnit):取出队首的对象。如果有数据可取,则立刻返回数据;若超时没有数据,返回失败。
  • take():取出队首的对象,为空进行等待直到取出。
  • drainTo():取出全部可用数据(可指定数量),多个数据批量取出有利于提升数据获取率。

阻塞队列实现类

ArrayBlockingQueue

由数组结构组成的有界队列(FIFO 先进先出)。在默认情况不保证公平访问队列公平访问队列:阻塞所有生产者或消费者线程,当队列可用,可以按照阻塞的先后顺序访问队列)。

  通常情况下,为了保证公平性会减低吞吐量。

LinkedBlockingQueue

链表结构组成的有界阻塞队列,与ArrayBlockingQueue不同的是,LinkedBlockingQueue能高效处理并发数据,是因为生产者和消费者端分别用了独立的锁控制数据同步,这也意味着在高并发情况下,两端可以并行操作队列的数据,以此提高性能。

  注意,如果没有指定大小,会构建一个无限大的容量限制,这是很危险的,有耗尽内存的风险。

一般来说,用上述两个方法足矣。

PriorityBlockingQueue

  支持优先级排序的无界阻塞队列,默认优先级越高越快被取出,可以自定义实现compareTo或指定构造参数Comparator对元素进行排序,但不保证同优先级的先后顺序

DelayQueue

  支持延时队列获取元素的无界阻塞队列,队列使用PriorityBlockingQueue来实现,队列元素必须实现Delayed接口,创建元素时,可指定元素到期时间,在到期前才能取走。

SynchronousQueue

不存储元素的阻塞队列。每个插入操作必须等待另一个线程的移除操作,删除操作也是如此。由于没有容器,peek操作自然也无法使用。

LinkedTransferQueue

链表结构组成的无界阻塞队列:他可以算是LinkedBolckingQueueSynchronousQueue 和合体。我们知道 SynchronousQueue 内部无法存储元素,当要添加元素的时候,需要阻塞,不够完美,LinkedBolckingQueue则内部使用了大量的锁,性能不高。而他继承两者性能又高,又不阻塞的特点。

LinkedBlockingDeque

链表结构组成的双向阻塞队列:该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除);并且,该阻塞队列是支持线程安全。
此外,LinkedBlockingDeque还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer. MAX_VALUE

阻塞队列使用场景

  除了线程池的实现使用阻塞队列外,我们还可以在生产者-消费者模式中使用阻塞队列:非阻塞队列实现生产者-消费者模式无需单独考虑同步和线程之间通信问题,实现起来更为简单。

线程池

简介

  在进行大量异步计算、使用太多线程,会使得线程的创建和销毁浪费大量资源,并且难以控制,这就产生了线程池创建的条件。

  Java 1.5就提供了Executor框架用于把任务的提交和执行解耦。Executor核心成员就是ThreadPoolExecutor,是线程池的核心类。我们就以此展开。

ThreadPoolExecutor


  我们先看看最原始的构造方法,也是最多参数的构造方法,上面展示了取值范围▲

  • corePoolSize :核心线程数。默认线程池没有线程,只有在任务提交到时,才会创建线程来处理任务;若 线程数 < corePoolSize ,创建新的线程。若 线程数 >= corePoolSize;则不再创建新线程,此时会加入到指定队列(见参数 workQueue)。注意: 若调用prestartAllCoreThreads()则会提前创建并启用所有核心线程来等待任务。
  • maximumPoolSize: 允许创建的最大线程数。
  • keepAliveTime: 非核心线程闲置的超时时间。超时则非核心线程被回收,如果任务过多且执行时机短,可提高keepAliveTime时间来保证线程利用率。此外,设置allowCoreThreadTimeOut(true),也会作用到核心线程上。
  • TimeUnit: 上面参数单位。
  • workQueue: 线程数 >= corePoolSize;则不再创建新线程,此时会加入到此指定队列中,只要是继承于此的类即可。
  • ThreadFactory: 线程工厂。可以用线程工厂给每个创建的线程设置名字,一般可不设置。
  • RejectedExecutionHandler: 饱和策略。当队列和线程池满时,多余的任务采取的处理策略。默认是AbortyPolicy,表示不处理并抛出异常。
    此外还有三种策略:
  1. CallerRunsPolicy:调用所在线程处理任务。此策略提供简单反馈控制机制,能减缓新任务的提交速度。
  2. DiscardPolicy:不执行任务,并删除。
  3. DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。

一般处理流程

逻辑流程图▼

原理图则是(错误修正:线程数 < corePoolSize 改为线程数 >= corePoolSize):

线程池的种类

  通过直接或间接地配置ThreadPoolExecutor的参数可以创建不同类型的ThreadPoolExecutor,其中4种常用,我们以此介绍。

FixedThreadPool

  可重用固定线程数的线程池,在Executors类提供了创建FixedThreadPool的方法,如下所示:

public static ExecutorService newFixedThreadPool(int nThreads) {
     return new ThreadPoolExecutor(nThreads, nThreads,
                                   0L, TimeUnit.MILLISECONDS,
                                   new LinkedBlockingQueue<Runnable>());
}

可见:

maximumPoolSizecorePoolSize 都为同一需要指定的参数,这说明:只有核心线程,且数量固定

keepAliveTime = 0L,这也意味着多余的线程立刻终止

LinkedBlockingQueue : 采用无阻塞队列(默认无限)

它的基本执行过程如下

  1. 线程数少于corePoolSize, 会立刻创建新线程执行任务。
  2. 线程数到达corePoolSize后,任务则加入到LinkedBlockingQueue中。
  3. 当线程执行有空缺时,会循环从LinkedBlockingQueue中获取任务来执行。

FixedThreadPool使用了LinkedBlockingQueue, 也就是无界队列(队列最大可容纳Integer.MAX_VALUE), 因此会造成以下影响:

  • 线程池线程数到达corePoolSize后,任务会被存放在LinkedBlockingQueue
  • 因为无界队列,只要未调用shutdown()或者shutdownNow()方法,就不会拒绝任意数量任务。

CachedThreadPool

代码如下:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

  这意味着,没有核心线程,非核心线程无界,线程最多等待60s,由于使用了SynchronousQueue,所以每个插入操作都必须等待另一个线程移除后才执行。同理,任何一个移除操作都等待另一个插入操作。

  当执行execute方法时,首先执行SynchronousQueue的offer方法提交任务,并查询线程池是否有空闲的线程执行SynchronousQueue的poll方法来移除任务。如果有则配对成功,将任务交个空线程处理;没有则配对失败,创建新线程去处理任务。当线程池空闲时,会执行SynchronousQueuepoll方法,等待SynchronousQueue提交新的任务。超时则空线程终止。

  因为无界,如果提交的任务大于线程池中线程处理的任务速度,就会不断创建新线程。此外,提交的任务都立刻有线程处理,所以适合大量需要处理并耗时少的任务。

SingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
    }

  只有一个核心线程,其他参数与FixedThreadPool一样,这里就不赘述了。

ScheduledThreadPool

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

ScheduledThreadPool是一个能实现定时、周期性任务的线程池。构造方法最后调用的是ThreadPoolExectuor构造方法。因为采用的是无界DelayedWorkQueue,所以Integer.MAX_VALUE没有实际意义。
具体用法可见: 线程池之ScheduledThreadPool学习

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值