阻塞队列和线程池原理

队列是一种特殊的线性表,先进先出(FIFO-first in first out)线性表

 

什么是阻塞队列?

1、支持阻塞的插入方法:当队列满了,往里面插入元素是,队列会阻塞插入元素的线程,直到队列不满

2、支持阻塞的移出方法:当队列为空时,获取元素的线程会等待队列变为非空

在并发编程中使用生产者和消费者模式时,一般都是通过阻塞队列形成一个缓冲区,平衡消费者和生产者。

一般阻塞队列的方法大致如下:

阻塞队列和线程池原理_第1张图片

        1、抛出异常:当队列满时,如果往队列插入元素,会抛出IllegalStateException("Qu"euefull)异常。当队列空时,从队列里面获取元素会抛出NoSunchElementException异常。

2、返回特殊值:当往队列插入元素时,会返回元素是否插入成功,成功返回TRUE。如果是移出方法,则是从队列取出一个元素,如果没有则返回null。

        3、一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到队列可用或者响应中断推出。当队列空时,如果消费者线程从队列里take元素,队列会阻塞消费者线程,直到队列不为空。

        4、超时退出:当阻塞队列满时,如果生产者线程往队列里插入元素,队列会阻塞生产者线程一段时间,如果超过了指定的时间,生产者线程就会退出。

常用阻塞队列

        ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。

        LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。

        PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。

        DelayQueue:一个使用优先级队列实现的无界阻塞队列。

        SynchornousQueue:一个不存储元素的阻塞队列。

        LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。

        LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

什么是有界无界?

        有限队列就是长度有限,满了以后生产者会阻塞,无界队列就是里面能放无数的东西而不会因为队列长度限制被阻塞,当然空间限制与系统资源的限制,如果处理不及时,导致队列越来越大,超出一定的限制导致呢村超限,操作系统或者JVM帮你解决烦恼,直接OOM kill。

        无界队列也会阻塞,因为阻塞不仅仅体现在生产者放入元素时会阻塞,先飞着拿去元素时,如果没有元素,同样也会阻塞。

ArrayBlockingQueue:

public ArrayBlockingQueue(int capacity) {
    this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair){}

public void put(E e) throws InterruptedException {
        Objects.requireNonNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == items.length)
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

根据构造方法,可以看出,我先传一个参数capacity,初始化队列的大小;fair参数,按照参数说明,为TRUE时,说明是公平的访问队列,即阻塞的线程会按照阻塞的先后顺序访问队列;为FALSE时是非公平的访问队列,即先阻塞的线程可能最后访问队列。默认为FALSE。

根据put和take方法可以看出,放和拿用的是同一把锁。

LinkedBlockingQueue:

private final ReentrantLock putLock = new ReentrantLock();

private final ReentrantLock takeLock = new ReentrantLock();

public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

public LinkedBlockingQueue(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
        last = head = new Node(null);
    }

 static class Node {
        E item;

        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head.next
         * - null, meaning there is no successor (this is the last node)
         */
        Node next;

        Node(E x) { item = x; }
    }

LinkedBlockingQueue是用链表实现的,默认长度是Integer.MAX_VALUE。构造方法中,一开始链表头=链表尾(Node类里面有一个指向下一个节点的next);放和拿是两个不同的锁:putLock和takeLock。

ArrayBlockingQueue实现和LinkedBlockingQueue实现的不同

1、队列中锁的实现不同

        ArrayBlockingQueue实现的队列,放和拿是同一把锁;LinkedBlockingQueue队列是放是putLock,拿的takeLock。

2、在生产或者消费时操作不同

        ArrayBlockingQueue实现的队列中在生产和消费的时候,是直接将枚举对象插入或者溢出的;

      LinkedBlockingQueue实现的队列在生产和消费的时候,需要把么局对象转为Node进行插入或移除的,会影响性能

3、队列初始化方式不同

       ArrayBlockingQueue实现的队列中必须指定队列的大小

       LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE。

PriorityBlockingQueue:

是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序圣墟排列。也可以自定义实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。

// 队列默认大小是11 
private static final int DEFAULT_INITIAL_CAPACITY = 11;
// 堆数组
private transient Object[] queue;
public PriorityBlockingQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }

队列采用堆数组实现的,当放入元素的时候,会尝试扩容数组并且比较排序。

package com.example.retrofit.ex2.blockingqueue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;

/**
 * ArrayBlockingQueue
 */
public class ArrayBlockingQueueTest {

    public static PriorityBlockingQueue queue = new PriorityBlockingQueue<>();

    public static void main(String[] args){
        queue.put(2);
        queue.put(20);
        queue.put(12);
        queue.put(4);
        queue.put(1);
        queue.put(8);

        int legth = queue.size();
        for (int i = 0; i < legth; i++) {
            try {
                System.out.println("queue"+i+" = "+queue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

DelayQueue:

        DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟时间到才能从队列中提取出来。

        缓存系统的设计:可以yogaDelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素,标识缓存有效期到了。

package com.example.retrofit.ex2.blockingqueue.delay;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class DelayOrder implements Delayed {

    private long delayTime; // delaytime
    private T data; // 数据

    public DelayOrder(long delayTime, T data) {
        this.delayTime = delayTime*1000+System.currentTimeMillis();
        this.data = data;
    }

    public long getDelayTime() {
        return delayTime;
    }

    public T getData() {
        return data;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        long d;
        d = unit.convert(delayTime-System.currentTimeMillis(),unit);
        return d;
    }

    @Override
    public int compareTo(Delayed o) {
        long d = (getDelay(TimeUnit.MILLISECONDS)-o.getDelay(TimeUnit.MILLISECONDS));
        if (d==0){
            return 0;
        }else {
            if (d<0){
                return -1;
            }else {
                return 1;
            }
        }
    }
}

SynchornousQueue:

        是一个​​​​​​​不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchornousQueue可以看成一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合传递性场景。


// fair:如果为true,等待线程将以FIFO顺序竞争访问;否则,顺序未明。
public SynchronousQueue(boolean fair) {
        transferer = fair ? new TransferQueue() : new TransferStack();
    }


abstract static class Transferer {
        /**
         * Performs a put or take.
         *
         * @param e if non-null, the item to be handed to a consumer;
         *          if null, requests that transfer return an item
         *          offered by producer.
         * @param timed if this operation should timeout
         * @param nanos the timeout, in nanoseconds
         * @return if non-null, the item provided or received; if null,
         *         the operation failed due to timeout or interrupt --
         *         the caller can distinguish which of these occurred
         *         by checking Thread.interrupted.
         */
        abstract E transfer(E e, boolean timed, long nanos);
    }

LinkedTransferQueue​​​​​​​:

        多了tryTransfer和transfer方法:

        1、transfer方法:

        将元素传输给使用者,必要时等待。
更准确地说,如果存在已在等待接收指定元素的消费者(在take或timed poll中),则立即传输指定元素,否则将在该队列的尾部插入指定元素,并等待直到消费者接收到该元素。

        2、tryTransfer方法:

        如果可以在超时时间过去之前将元素传输给使用者,则将其传输给使用者。
更准确地说,如果存在已在等待接收指定元素的消费者(在take或timed poll中),则立即传输指定元素,否则将在该队列的尾部插入指定元素,并等待直到消费者接收到该元素,如果在传输该元素之前已过指定的等待时间,则返回false。

 public void transfer(E e) throws InterruptedException {
        if (xfer(e, true, SYNC, 0) != null) {
            Thread.interrupted(); // failure possible only due to interrupt
            throw new InterruptedException();
        }
    }

public boolean tryTransfer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {
        if (xfer(e, true, TIMED, unit.toNanos(timeout)) == null)
            return true;
        if (!Thread.interrupted())
            return false;
        throw new InterruptedException();
    }

LinkedBlockingDeque:

        是一个由链表结构组成的双向阻塞列表。所谓双向队列指的是可以从队列的两端插入和移出。双向队列因为多了一个操作队列的入口,在多线程时,也就减少了一般的竞争。

        相较于LinkedBlockingQueue,其多了xxFirst()方法,操作队列的第一个元素;xxLast方法,操作队列的最后一个元素。

public LinkedBlockingDeque() {
        this(Integer.MAX_VALUE);
    }

public LinkedBlockingDeque(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
    }

public void put(E e) throws InterruptedException {
        putLast(e);
    }

public E take() throws InterruptedException {
        return takeFirst();
    }

线程池

为什么要用线程池?

1、降低资源的消耗。通过重复利用已创建的线程降低线程创建、销毁造成的消耗。

2、提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。

3、线程是稀缺的资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

ThreadPoolExecutor的类关系:

阻塞队列和线程池原理_第2张图片

线程池的创建各个参数含义: 

corePoolSize:池中要保留的核心线程数,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut。当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务会被保存到阻塞线程中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动多有核心线程。
maximumPoolSize:线程池允许的最大线程数。如果当前阻塞队列满了,而且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize
keepAliveTime:线程空闲时的存活时间,即当想没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于corePoolSize时才有用。
unit:keepAliveTime的单位
workQueue:workQueue必须是BlockingQueue阻塞队列。当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能。

一般来说,我们应该尽量使用有界队列,因为无界队列作为工作队列会对线程池带下如下影响:

        1、当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。

        2、由于1,使用无界队列时 maximumPoolSize将是一个无效参数

        3、由于1、2,使用无界队列时,KeepAliveTime将是一个无效参数

        4、更重要的,使用无界队列queue可能会耗尽系统资源,有界队列则有助于防止资源耗尽,同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范围。

threadFactory:创建线程的工厂,通过自定义的线程工厂可给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置线程为守护线程。

Executor静态工厂里默认的ThreadFactory,线程的命名规则是“pool-数字-thread-数字”。

RejectExecutionHandler:

        线程池的饱和策略,当达到了最大线程数并且阻塞队列满了,而且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:

        1、AbortPolicy:直接抛出异常,默认策略;

        2、CallerRunsPolicy:用调用者所在的线程来执行任务

        3、DiscardOldestPolicy:丢弃阻塞队列中最靠前的任务,并执行当前任务;

        4、DiscardPolicy:直接丢弃任务。

当然也可以根据应用场景实现RejectExecutionHandler接口,自定义饱和策略,如记录日志或者持久化存储不能处理的任务。

线程池的工作机制:

1、如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务(注意,执行这一步需要获取全局锁);

2、如果运行的线程数>=corePoolSize,则将任务加入BlockingQueue;

3、如果无法将任务加入BlockingQueue(队列已满),并且没有达到最大线程数,则创建新的线程数来处理任务;

4、如果创建新线程将使当前运行的线程数超过maximumPoolSize,任务将被拒绝,并调用RejectExecutionHandler.rejectExetion()方法。

阻塞队列和线程池原理_第3张图片

提交任务: 

        execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功

        submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,铜鼓哦这个future对象可以判断任务是否执行成功,并通过future的get()方法获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前想一段时间后立即返回,这时候有可能任务没有完成。

关闭线程池:

        可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow 首先将线程池的状态设置成STOP,然后尝试停止所有正在执行或者暂停线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。

        

只要调用了这两个关闭方法中的任意一个,isShutdown 方法就会返回 true。 当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 isTerminaed 方法 会返回 true。至于应该调用哪一种方法雷关闭线程池,应该有提交到线程池的任务特性来决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

合理的配置线程池:

        想要合理的配置线程池,就必须首先分析任务的特性,大致分为:

任务的性质:CPU 密集型任务、IO 密集型任务和混合型任务。 

•任务的优先级:高、中和低。 

•任务的执行时间:长、中和短。 

•任务的依赖性:是否依赖其他系统资源,如数据库连接。 

性质不同的任务可以用不同规模的线程池分开处理。 

CPU 密集型任务应配置尽可能小的线程,如配置 Ncpu+1 个线程的线程池。 

由于 IO 密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如 

2*Ncpu

混合型的任务,如果可以拆分,将其拆分成一个 CPU 密集型任务和一个 IO 

密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐 

量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行 

分解。可以通过 Runtime.getRuntime().availableProcessors()方法获得当前设备的 

CPU 个数

4种默认线程池:

        1、newSingleThreadExecutor

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

 corePoolSize=maximumPoolSize=1,单线程,只创建一个线程,KeepAlive=0,线程空闲存活时间为0

        2、newCachedThreadPool

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

 corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE,线程可以复用,但是我们不无法控制线程数量,空闲线程存活时间为60S。

        3、newFixedThreadPool

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

 corePoolSize=maximumPoolSize=n,我们可以指定线程数,但是核心=最大,空闲线程存活时间为0 

        4、newScheduledThreadPool

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

corePoolSize=corePoolSize,maximumPoolSize=Integer.MAX_VALUE,空闲线程存活时间默认10S。采用DelayedWorkQueue队列实现延时操作。

你可能感兴趣的:(java,开发语言)