多线程(二)

队列阻塞

阻塞队列一般用于生产者和消费者的场景中,生产者就是往队列里添加元素的线程,消费者是从队列里拿元素的线程,阻塞队列就是盛放元素的容器。

常见的阻塞场景:

  1. 队列中没有数据,消费者会阻塞直到有数据放入。
  2. 队列被填满,生产者所在的队列被阻塞。

BlockingQueue阻塞队列

主要方法:

public boolean offer(E e); 向对列里面添加元素,如果队列还有容积,返回true,否则返回false,这个方法不阻塞当前执行方法的线程。

public boolean offer(Object s, long timeout, TimeUnit unit);如果在指定的时间内还不能往队列中添加元素,就返回false,否则返回true。

public void put(Object o); 把一个元素放入队列,如果队列里面没有空间,则调用该方法的线程会阻塞,直到有空间为止。

public Object poll();取走队列的首位的元素
public Object poll(long timeout, TimeUnit unit); 取走队列的首位元素,如果在指定时间内如果队列里面有了新的数据就返回该数据。

public Object take();取出队列里的首个元素,如果队列为空,则调用该方法的队列被阻塞,直到队列中有了数据为止。

public int drainTo(@NonNull Collection<? super Object> c);从队列中取出所有可用的元素,放入指定集合。通过该方法,可以提升获取数据效率;不需要多次分批加锁或释放锁。

Java中的阻塞队列

ArrayBlockingQueue:由数组结构构成的有界阻塞队列,按照先进先出的原则对元素进行排序,生产者把数据插入的队列的末尾,消费者总是从队列的头部拿数据,当队列满了之后,再往队列中插入数据会造成队列的阻塞,同样,从空的队列中取数据,也是阻塞的。

有一个公平策略,我们如果希望,队列中的数据总是按照生产者生产数据的顺序,取队列中的数据,那么我们可以在构造函数中把fair参数设置为true就可以了,但是这种操作会降低吞吐量。比如::

ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(2000,true);

LinkedBlockingQueue:由链表结构构成的有界阻塞队列,由于LinkedBlockingQueue实现是线程安全的,实现了先进先出等特性,是作为生产者消费者的首选,LinkedBlockingQueue可以指定容量,也可以不指定,不指定的话,默认最大是Integer.MAX_VALUE,
它之所以能够高效的处理并发,是因为它对生产端和消费端分别采用了独立的锁来控制数据的同步。其中主要用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来。
如果生产的速度大于消费的速度,这个时候最好指定一下容量,否则可能还没等到队列塞满内存就溢出了。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列,默认情况下元素采取自然元素的升序,可以指定compareTo()方法来自定义排序
DelayQueue:是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。这种队列是有序的,即队头对象的延迟到期时间最长。注意:不能将null元素放置到这种队列中
SynchronousQueue:没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样
LinkedTransferQueue:由链表结构构成的无界阻塞队列,实现了TransferQueue接口。TransferQueue有几个重要的方法
void transfer(E e);如果当前有消费者正在等待,直接给消费者,否则将元素放入到队列的尾部并且进入阻塞状态知道消费者取走该元素
boolean tryTransfer(E e); 这个操作不阻塞,如果有消费者接受直接给消费者,没有的话直接返回false。
boolean tryTransfer(E e, long timeout, TimeUnit unit);如果当前有消费者正在等待,直接给消费者,否则将元素放入到队列的尾部,如果指定时间内还没有消费者来取走数据就返回false.
LinkedBlockingDeque:由链表结构构成的双向的阻塞队列,可以两端同时出入。

线程池

使用线程来处理异步任务很方便,不过假如有很多任务需要执行,有的任务执行的时间不是很长,这时候就有大量的线程的创建和销毁,会消耗很多的内存资源,这时候使用线程池来管理线程是很好的选择。

我们可以使用ThreadPoolExecutor来创建一个线程池,它有四个构造方法,最常用的是下面一个

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

参数的含义:

corePoolSize 线程池可创建的核心线程的数量,默认情况下线程池是空的,只有提交任务的时候才会创建线程,如果当前线程的数量少于corePoolSize,就会创建新的线程,如果等于或者多余就不创建。调用prestartAllcoreThread方法可以提前创建好所有的核心线程。

maximumPoolSize 线程池中允许创建的最大的线程的数量。

keepAliveTime 如果线程的数量大于核心的线程数,多出的部分超过keepAliveTime时间后会被回收,如果任务很多,并且每个线程的执行时间很短,我们可以调大keepAliveTime来提高线程的利用率。
假如allowCoreThreadTimeOut属性为true,则keepAliveTime参数可以作用到核心线程数量上。

TimeUnit keepAliveTime的时间,有天(DAYS),小时(HOURS),分钟(MINUTES),秒(SECONDS),毫秒(MILLISECONDS)等

BlockingQueue 任务队列,如果当前的线程数大于corePoolSize,就放到阻塞队列BlockingQueue中。

ThreadFactory 线程工厂,可以使用线程工厂来给每个创建出来的线程设置名字。

RejectedExecutionHandler 饱和策略,当线程池和任务队列都满了之后的策略,有几种策略

(1)CallerRunsPolicy :用调用者所在的线程来处理任务
(2)DiscardOldestPolicy:丢弃队列中最近的任务
(3)DiscardPolicy:删除不能执行的任务
(4)AbortPolicy:抛出异常

线程池的工作流程:

提交一个任务后:

  • 判断当前线程数是否大于corePoolSize
  • 如果小于corePoolSize则创建新的线程来执行该任务,否则就把该任务放入到阻塞队列中,线程中的空闲线程会去队列中领任务
  • 如果队列中也满了,不过线程的数量还打到最大线程数,就继续创建非核心线程
  • 如果线程数量打到了最大线程数,则执行饱和策略
四中常见的线程池

FixedThreadPool 固定大小的线程池,也就是corePoolSize和maximumPoolSize相等,阻塞队列使用的是LinkedBlockingQueue,大小为整数最大值。Executors类中提供了创建方法

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

该线程池中的线程数量始终不变,当有新任务提交时,线程池中有空闲线程则会立即执行,如果没有,则会暂存到阻塞队列。对于固定大小的线程池,不存在线程数量的变化。同时使用无界的LinkedBlockingQueue来存放执行的任务。当任务提交十分频繁的时候,LinkedBlockingQueue迅速增大,存在着耗尽系统资源的问题。而且在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源,需要shutdown。

newCachedThreadPool 线程的核心池corePoolSize大小为0,核心池最大为Integer.MAX_VALUE,这意味着CachedThreadPool没有核心线程,非核心线程几乎无穷大,线程存活时间默认60秒,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销。如果同时又大量任务被提交,而且任务执行的时间不是特别快,那么线程池便会新增出等量的线程池处理任务,这很可能会很快耗尽系统的资源。所以CachedThreadPool适合处理有大量的需要处理的任务并且耗时较少的任务。

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

newSingleThreadExecutor 单个工作线程的线程池,里面只有一个线程,这个线程死了之后,会重新起一个线程来代替它工作。

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

可以看到其核心线程和最大线程都是1,其阻塞队列使用的是LinkedBlockingQueue。当执行的时候,如果当前没有运行的线程,就创建一个来执行任务,如果有,就把任务放入到LinkedBlockingQueue队列中。所以,newSingleThreadExecutor可以保证所有任务逐一执行。

newScheduledThreadPool 能实现定时和周期性任务的线程池

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
        
public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);

可以看到这里创建了ScheduledThreadPoolExecutor,ScheduledThreadPoolExecutor继承自ThreadPoolExecutor

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }
 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

从上面可以看到最终调用的是ThreadPoolExecutor的构造方法,其中maximumPoolSize是传的Integer.MAX_VALUE基本无穷大,阻塞队列使用的DelayedWorkQueue也是无边界的。
此方法无论任务执行时间长短,都是当第一个任务执行完成之后,延迟指定时间再开始执行第二个任务。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值