java并发编程 BlockingQueue-阻塞队列(七)

8 篇文章 0 订阅

队列

        在学习阻塞队列之前,我们先来了解一下什么是队列,Java中队列是Queue接口,一种特殊的线性表或者说一种存储数据的结构,遵循先入先出、后入后出的基本原则。一般来说,它只允许在表的前端进行删除操作,而在表的后端进行插入操作,但是java的某些队列运行在任何地方插入删除;比如我们常用的 LinkedList 集合,它实现了Queue 接口,因此,我们可以理解为 LinkedList 就是一个队列。
        队列主要分为:

                阻塞和非阻塞,有界和无界、单向和双向链表(本文只对阻塞队列进行说明)

首先来了解下Queue接口:

public interface Queue<E> extends Collection<E> {

    //添加一个元素,添加成功返回true, 如果队列满了,就会抛出异常
    boolean add(E e);

    //添加一个元素,添加成功返回true, 如果队列满了,返回false
    boolean offer(E e);

    //返回并删除队首元素,队列为空则抛出异常
    E remove();

    //返回并删除队首元素,队列为空则返回null
    E poll();

    //返回队首元素,但不移除,队列为空则抛出异常
    E element();

    //获取队首元素,但不移除,队列为空则返回null
    E peek();
}

阻塞队列

特点

        阻塞队列区别于其他类型的队列的最主要的特点就是“阻塞”这两个字 阻塞功能使得生产者和消费者两端的能力得以平衡,当有任何一端速度过快时,阻塞队列便会把过快的速度给降下来。实现阻塞最重要的两个方法是 take 方法和 put 方法

1. take 方法的功能是获取并移除队列的头结点,通常在队列里有数据的时候是可以正常移除 的。可是一旦执行 take 方法的时候,队列里无数据,则阻塞,直到队列里有数据。一旦队列里 有数据了,就会立刻解除阻塞状态,并且取到数据。

2. put 方法插入元素时,如果队列没有满,那就和普通的插入一样是正常的插入,但是如果队 列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间。如果后续队列有了空闲空 间,比如消费者消费了一个元素,那么此时队列就会解除阻塞状态,并把需要添加的数据添加到 队列中。

应用场景:

        通常使用生产者/消费者模式的时候(或者其他入队出队业务),我们生产者只需要往队列里添加元素,而消费者只需要从队列里取出它们就可以了,而且阻塞队列(BlockingQueue)是线程安全的,所以生产者和消费者都可以是多线程的,不会发生线程安全问题。生产者/消费者直接使用线程安全的队列就可以,而不需要自己去考虑更多的线程安全问题

BlockingQueue接口

        java.util.concurrent包下也是专门为并发编程设计,继承自Queue接口,在Queue接口基础上加了阻塞的操作

public interface BlockingQueue<E> extends Queue<E> {
   
    将给定元素设置到队列中,如果设置成功返回true, 否则抛出异常。如果是往限定了长度的队列中设置值,推荐使用offer()方法
    boolean add(E e);

    //将给定的元素设置到队列中,如果队列没满,返回true,如果队列已满,返回false(不阻塞)
    boolean offer(E e);

    //元素设置到队列中,如果队列已满,则阻塞,直至队列空出位置
    void put(E e) throws InterruptedException;

    //将给定的元素设置到队列中,可以设置阻塞时间,如果队列已满,则进行阻塞。超过阻塞时间,则返回false
    boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;

    //队列里有数据会正常取出数据并删除;但是如果队列里无数据,则阻塞,直到队列里有数据
    E take() throws InterruptedException;

    //从队列中获取值,如果有数据,出队,如果没有数据,返回null (不阻塞)
    E poll();

    //从队列中获取值,可以设置阻塞时间,如果没有数据,则阻塞,超过阻塞时间,则返回null
    E poll(long timeout, TimeUnit unit) throws InterruptedException;

     //获取队列中剩余的空间。
    int remainingCapacity();
 
    //从队列中移除指定的值。
    boolean remove(Object o);
 
    //判断队列中是否拥有该值。
    public boolean contains(Object o);
 
    //将队列中值,全部移除,并发设置到给定的集合中。
    int drainTo(Collection<? super E> c);
 
    //指定最多数量限制将队列中值,全部移除,并发设置到给定的集合中。
    int drainTo(Collection<? super E> c, int maxElements);
}

既然是接口,肯定需要实现这些方法来使用,一起看下常用的几个实现类:

        ArrayBlockingQueue 基于数组结构实现的一个有界阻塞队列

        LinkedBlockingQueue 基于链表结构实现的一个有界阻塞队列

        PriorityBlockingQueue 支持按优先级排序的无界阻塞队列

        DelayQueue 基于优先级队列(PriorityBlockingQueue)实现的无界阻塞队列

        SynchronousQueue 不存储元素的阻塞队列

        LinkedTransferQueue 基于链表结构实现的一个无界阻塞队列

        LinkedBlockingDeque 基于链表结构实现的一个双端阻塞队列

        老样子,接口的多态实现,只是为了解决不同的业务场景,所以,需要哪一个自选

ArrayBlockingQueue

        最典型的有界阻塞队列,其内部是用数组存储元素的,初始化时需要指定容量大小(小于等于0抛出异常),利用 ReentrantLock 实现线程安全。

        使用场景:如果生产速度和消费速度基本匹配的情况下,使用 ArrayBlockingQueue是个不错选择;当如果生产速度远远大于消费速度,则会导致队列填满, 大量生产线程被阻塞

        核心源码:

public void put(E e) throws InterruptedException {
        检查是否为空
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        //加锁,如果线程中断抛出异常
        lock.lockInterruptibly();
        try {
            //阻塞队列已满,则将生产者挂起,等待消费者唤醒
            //设计注意点: 用while不用if是为了防止虚假唤醒
            while (count == items.length)
                //队列满了,使用notFull等待(生产者阻塞)
                notFull.await();
            // 入队
            enqueue(e);
        } finally {
            //唤醒消费者线程
            lock.unlock();
        }
}

private void enqueue(E x) {
     final Object[] items = this.items;
     //入队 使用的putIndex
     items[putIndex] = x;

     if (++putIndex == items.length)
     putIndex = 0; //设计的精髓: 环形数组,putIndex指针到数组尽头了,返回头部
     count++;

     //notEmpty条件队列转同步队列,准备唤醒消费者线程,因为入队了一个元素,肯定不为空了
     notEmpty.signal();
}
public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        //加锁,如果线程中断抛出异常
        lock.lockInterruptibly();
        try {
            //如果队列为空,则消费者挂起
            while (count == 0)
                notEmpty.await();
            //出队
            return dequeue();
        } finally {
            lock.unlock();// 唤醒生产者线程
        }
}
 private E dequeue() {
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex]; //取出takeIndex位置的元素
        items[takeIndex] = null;
        if (++takeIndex == items.length)
            takeIndex = 0; //设计的精髓: 环形数组,takeIndex 指针到数组尽头了,返回头部
        count‐‐;
        if (itrs != null)
            itrs.elementDequeued();
        //notFull条件队列转同步队列,准备唤醒生产者线程,此时队列有空位
        notFull.signal();
        return x;
}

        使用方式:

BlockingQueue arrayBlockingQueue= new ArrayBlockingQueue(100);
arrayBlockingQueue.put("1"); //向队列中添加元素
Object object = arrayBlockingQueue.take(); //从队列中取出元素

LinkedBlockingQueue

        一个基于链表实现的阻塞队列,默认情况下,该阻塞队列的大小 为Integer.MAX_VALUE,由于这个数值特别大,所以 LinkedBlockingQueue 也被称作无界队列,代表它几乎没有界限,队列可以随着元素的添加而动态增长,但是如果没有剩余内存, 则队列将抛出OOM错误。所以为了避免队列过大造成机器负载或者内存爆满的情况出现,我们在使用的时候建议手动传一个队列的大小

        LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素。采用两把锁的锁分离技术实现入队出队互不阻塞,添加元素和获取元素 都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写操作可以并行执行

         核心源码:

public void put(E e) throws InterruptedException {
        // 不允许null元素
        if (e == null) throw new NullPointerException();
        int c = ‐1;
        // 新建一个节点
        Node<E> node = new Node<E>(e);
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
        // 使用put锁加锁
        putLock.lockInterruptibly();
        try {
            // 如果队列满了,就阻塞在notFull上等待被其它线程唤醒(阻塞生产者线程)
            while (count.get() == capacity) {
                notFull.await();
            }
            // 队列不满,就入队
            enqueue(node);
            c = count.getAndIncrement();// 队列长度加1,返回原值
            // 如果现队列长度小于容量,notFull条件队列转同步队列,准备唤醒一个阻塞在notFull条件上的线程(可以继续入队)
            // 因为可能有很多线程阻塞在notFull这个条件上,而取元素时只有取之前队列是满的才会唤醒notFull,此处不用等到取元素时才唤醒
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
            putLock.unlock(); // 真正唤醒生产者线程
        }
        // 如果原队列长度为0,现在加了一个元素后立即唤醒阻塞在notEmpty上的线程
        if (c == 0)
            signalNotEmpty();
    }
    private void enqueue(Node<E> node) {
        // 直接加到last后面,last指向入队元素
        last = last.next = node;
    }
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();// 加take锁
        try {
            notEmpty.signal();// notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程
        } finally {
            takeLock.unlock(); // 真正唤醒消费者线程
        }
    }
public E take() throws InterruptedException {
        E x;
        int c = ‐1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        // 使用takeLock加锁
        takeLock.lockInterruptibly();
        try {
            // 如果队列无元素,则阻塞在notEmpty条件上(消费者线程阻塞)
            while (count.get() == 0) {
                notEmpty.await();
            }
            // 否则,出队
            x = dequeue();
            c = count.getAndDecrement();//长度‐1,返回原值
            if (c > 1)// 如果取之前队列长度大于1,notEmpty条件队列转同步队列,准备唤醒阻塞在notEmpty上的线程,原因与入队同理
                notEmpty.signal();
        } finally {
            takeLock.unlock(); // 真正唤醒消费者线程
        }
        // 唤醒是需要加putLock的,这是为了减少锁的次数,所以,这里索性在放完元素就检测一下,未满就唤醒其它notFull上的线程,
        // 这也是锁分离带来的代价
        // 如果取之前队列长度等于容量(已满),则唤醒阻塞在notFull的线程
        if (c == capacity)
            signalNotFull();
        return x;
    }
    private E dequeue() {
        // head节点本身是不存储任何元素的
        // 这里把head删除,并把head下一个节点作为新的值
        // 并把其值置空,返回原来的值
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // 方便GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
    }
    private void signalNotFull() {
        final ReentrantLock putLock = this.putLock;
        putLock.lock();
        try {
            notFull.signal();// notFull条件队列转同步队列,准备唤醒阻塞在notFull上的线程
        } finally {
            putLock.unlock(); // 解锁,这才会真正的唤醒生产者线程
        }
    }

        使用方式:同ArrayBlockingQueue 指定参数就是有界,不指定就是无界

DelayQueue

        一个支持延时获取元素的阻塞队列, 内部采用优先队列 PriorityQueue 存储元素,同时元素必须实现 Delayed 接口;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。延迟队列的特点是:不是先进先出,而是会 按照延迟时间的长短来排序,下一个即将执行的任务会排到队列的最前面。

         它是无界队列,放入的元素必须实现 Delayed 接口,而 Delayed 接口又继承了 Comparable 接口,所以自然就拥有了比较和排序的能力

SynchronousQueue

        一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作 put必须等待消费者的移除操作take。每次取数据都要先阻塞,直到有数据被放入;同理,每次放数据的时候也会阻塞,直到有消费者来取。

        需要注意的是,SynchronousQueue 的容量不是 1 而是 0,因为 SynchronousQueue 不需要去持有元素,它所做的就是直接传递(direct handoff)。由于每当需要传递的时候, SynchronousQueue 会把元素直接从生产者传给消费者,在此期间并不需要做存储,所以如果运用得当,它的效率是很高的。

        使用场景:如果我们不确定来自生产者请求数量,但是这些请求需要很快的处理掉,那么配合SynchronousQueue为每个生产者请求分配一 个消费线程是处理效率最高的办法。Executors.newCachedThreadPool()就使用了 SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

结语

 关于阻塞队列就简单介绍到这儿,活学活用才是我们的目的,并发编程系列还有最后一些内容。有空了再补充

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java并发编程 背景介绍 并发历史 必要性 进程 资源分配的最小单位 线程 CPU调度的最小单位 线程的优势 (1)如果设计正确,多线程程序可以通过提处理器资源的利用率来提升系统吞吐率 (2)建模简单:通过使用线程可以讲复杂并且异步的工作流进一步分解成一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置交互 (3)简化异步事件的处理:服务器应用程序在接受来自多个远程客户端的请求时,如果为每个连接都分配一个线程并且使用同步IO,就会降低开发难度 (4)用户界面具备更短的响应时间:现代GUI框架中大都使用一个事件分发线程(类似于中断响应函数)来替代主事件循环,当用户界面用有事件发生时,在事件线程中将调用对应的事件处理函数(类似于中断处理函数) 线程的风险 线程安全性:永远不发生糟糕的事情 活跃性问题:某件正确的事情迟早会发生 问题:希望正确的事情尽快发生 服务时间过长 响应不灵敏 吞吐率过低 资源消耗过 可伸缩性较低 线程的应用场景 Timer 确保TimerTask访问的对象本身是线程安全的 Servlet和JSP Servlet本身要是线程安全的 正确协同一个Servlet访问多个Servlet共享的信息 远程方法调用(RMI) 正确协同多个对象中的共享状态 正确协同远程对象本身状态的访问 Swing和AWT 事件处理器与访问共享状态的其他代码都要采取线程安全的方式实现 框架通过在框架线程中调用应用程序代码将并发性引入应用程序,因此对线程安全的需求在整个应用程序中都需要考虑 基础知识 线程安全性 定义 当多个线程访问某个类时,这个类始终能表现出正确的行为,那么就称这个类是线程安全的 无状态对象一定是线程安全的,大多数Servlet都是无状态的 原子性 一组不可分割的操作 竞态条件 基于一种可能失效的观察结果来做出判断或执行某个计算 复合操作:执行复合操作期间,要持有锁 锁的作用 加锁机制、用锁保护状态、实现共享访问 锁的不恰当使用可能会引起程序性能下降 对象的共享使用策略 线程封闭:线程封闭的对象只能由一个线程拥有并修改 Ad-hoc线程封闭 栈封闭 ThreadLocal类 只读共享:不变对象一定是线程安全的 尽量将域声明为final类型,除非它们必须是可变的 分类 不可变对象 事实不可变对象 线程安全共享 封装有助于管理复杂度 线程安全的对象在其内部实现同步,因此多个接口可以通过公有接口来进行访问 保护对象:被保护的对象只能通过特定的锁来访问 将对象封装到线程安全对象中 由特定锁保护 保护对象的方法 对象的组合 设计线程安全的类 实例封闭 线程安全的委托 委托是创建线程安全类的最有效策略,只需要让现有的线程安全类管理所有的状态 在现有线程安全类中添加功能 将同步策略文档化 基础构建模块 同步容器类 分类 Vector Hashtable 实现线程安全的方式 将状态封装起来,对每个公有方法都进行同步 存在的问题 复合操作 修正方式 客户端加锁 迭代器 并发容器 ConcurrentHashMap 用于替代同步且基于散列的Map CopyOnWriteArrayList 用于在遍历操作为主要操作的情况下替代同步的List Queue ConcurrentLinkedQueue *BlockingQueue 提供了可阻塞的put和take方法 生产者-消费者模式 中断的处理策略 传递InterruptedException 恢复中断,让更层的代码处理 PriorityQueue(非并发) ConcurrentSkipListMap 替代同步的SortedMap ConcurrentSkipListSet 替代同步的SortedSet Java 5 Java 6 同步工具类 闭锁 *应用场景 (1)确保某个计算在其需要的所有资源都被初始化后才能继续执行 (2)确保某个服务在其所依赖的所有其他服务都已经启动之后才启动 (3)等待知道某个操作的所有参与者都就绪再继续执行 CountDownLatch:可以使一个或多个线程等待一组事件发生 FutureTask *应用场景 (1)用作异步任务使用,且可以使用get方法获取任务的结果 (2)用于表示一些时间较长的计算 状态 等待运行 正在运行 运行完成 使用Callable对象实例化FutureTask类 信号量(Semaphore) 用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量 管理者一组虚拟的许可。acquire获得许可(相当于P操作),release释放许可(相当于V操作) 应用场景 (1)二值信号量可用作互斥体(mutex) (2)实现资源池,例如数据库连接池 (3)使用信号量将任何一种容器变成有界阻塞容器 栅栏 能够阻塞一组线程直到某个事件发生 栅栏和闭锁的区别 所有线程必须同时到达栅栏位置,才能继续执行 闭锁用于等待事件,而栅栏用于等待线程 栅栏可以重用 形式 CyclicBarrier 可以让一定数量的参与线程反复地在栅栏位置汇集 应用场景在并行迭代算法中非常有用 Exchanger 这是一种两方栅栏,各方在栅栏位置上交换数据。 应用场景:当两方执行不对称的操作(读和取) 线程池 任务与执行策略之间的隐形耦合 线程饥饿死锁 运行时间较长的任务 设置线程池的大小 配置ThreadPoolExecutor 构造参数 corePoolSize 核心线程数大小,当线程数= corePoolSize的时候,会把runnable放入workQueue中 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了” keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。 workQueue 保存任务的阻塞队列 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建线程运行这个任务 threadFactory 创建线程的工厂 handler 拒绝策略 unit 是一个枚举,表示 keepAliveTime 的单位(有NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS,7个可选值 线程的创建与销毁 管理队列任务 饱和策略 AbortPolicy DiscardPolicy DiscardOldestPolicy CallerRunsPolicy 线程工厂 在调用构造函数后再定制ThreadPoolExecutor 扩展 ThreadPoolExecutor afterExecute(Runnable r, Throwable t) beforeExecute(Thread t, Runnable r) terminated 递归算法的并行化 构建并发应用程序 任务执行 在线程中执行任务 清晰的任务边界以及明确的任务执行策略 任务边界 大多数服务器以独立的客户请求为界 在每个请求中还可以发现可并行的部分 任务执行策略 在什么(What)线程中执行任务? 任务按照什么(What)顺序执行(FIFO、LIFO、优先级)? 有多少个(How Many)任务能并发执行? 在队列中有多少个(How Many)任务在等待执行? 如果系统由于过载而需要拒绝一个任务,那么应该选择哪一个(Which)任务?另外,如何(How)通知应用程序有任务被拒绝? 在执行一个任务之前或之后,应该进行什么(What)动作? 使用Exector框架 线程池 newFixedThreadPool(固定长度的线程池) newCachedThreadPool(不限规模的线程池) newSingleThreadPool(单线程线程池) newScheduledThreadPool(带延迟/定时的固定长度线程池) 具体如何使用可以查看JDK文档 找出可利用的并行性 某些应用程序中存在比较明显的任务边界,而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性 任务的取消和关闭 任务取消 停止基于线程的服务 处理非正常的线程终止 JVM关闭 线程池的定制化使用 任务和执行策略之间的隐性耦合 线程池的大小 配置ThreadPoolExecutor(自定义的线程池) 此处需要注意系统默认提供的线程池是如何配置的 扩展ThreadPoolExector GUI应用程序探讨 活跃度(Liveness)、性能、测试 避免活跃性危险 死锁 锁顺序死锁 资源死锁 动态的锁顺序死锁 开放调用 在协作对象之间发生的死锁 死锁的避免与诊断 支持定时的显示锁 通过线程转储信息来分析死锁 其他活跃性危险 饥饿 要避免使用线程优先级,因为这会增加平台依赖性,并可能导致活跃性问题。在大多数并发应用程序中,都可以使用默认的线程优先级。 糟糕的响应性 如果由其他线程完成的工作都是后台任务,那么应该降低它们的优先级,从而提前台程序的响应性。 活锁 要解决这种活锁问题,需要在重试机制中引入随机性(randomness)。为了避免这种情况发生,需要让它们分别等待一段随机的时间 性能与可伸缩性 概念 运行速度(服务时间、延时) 处理能力(吞吐量、计算容量) 可伸缩性:当增加计算资源时,程序的处理能力变强 如何提升可伸缩性 Java并发程序中的串行,主要来自独占的资源锁 优化策略 缩

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值