多线程-了解BlockingQueue的实现方式-线程安全队列

本文详细介绍了Java并发包中的BlockingQueue接口,包括其阻塞机制、ReentrantLock的使用以及主要实现类如ArrayBlockingQueue、LinkedBlockingQueue的功能和原理,重点讲解了生产者消费者模式在BlockingQueue中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一句话导读:

        BlockingQueue是一个阻塞机制的线程安全队列,通过ReentrantLock实现锁机制,实现多线程之间的操作安全,一般都是使用在资源共享、多线程处理,生产者消费者模式等场景中

目录

一句话导读:

一、什么是BlockingQueue

二、BlockingQueue的接口

三、BlockingQueue的实现类及其原理


一、什么是BlockingQueue

        BlockingQueue是Java并发包中提供的一个接口,是实现生产者消费者模式的关键组件之一,是一个基于阻塞机制实现的线程安全的队列,阻塞机制的实现是通过入队列出队列时加锁的方式实现的。作为生产者消费者中间的通讯通道,使得当多个生产者、多个消费者之间的操作是线程安全的。

二、BlockingQueue的接口

        BlockingQueue的继承关系如下,除了继承Queue的方法外,它还扩展了几个自己的方法。

方法

抛出异常

返回特定值

阻塞

阻塞特定时间

入队

add(e)

offer(e)

put(e)

offer(e, time, unit)

出队

remove()

poll()

take()

poll(time, unit)

获取队首元素

element()

peek()

不支持

不支持

        其中阻塞方法put、take则是我们在生产者消费者模型中使用到的线程安全并且阻塞的两个方法。

三、BlockingQueue的实现类及其原理

        BlockingQueue接口主要由5个实现类,分别如下表所示。

实现类

功能

ArrayBlockingQueue

基于数组的阻塞队列

,使用数组存储数据,并需要指定其长度,

有界队列

LinkedBlockingQueue

基于链表的阻塞队列

,使用链表存储数据,默认是一个

无界队列

;也可以通过构造方法设置最大元素数量,所以也可以作为

有界队列

SynchronousQueue

一种没有缓冲的队列,生产者产生的数据直接会被消费者获取并且立刻消费

PriorityBlockingQueue

基于

优先级别的阻塞队列

,底层基于数组实现,是一个

无界队列

DelayQueue

延迟队列

,其中的元素只有到了其指定的延迟时间,才能够从队列中出队

        这里我们主要了解下LinkedBlockingQueue,他是基于链表的阻塞队列,有一个默认的构造方法,定义了链表的最大长度。

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

        也可以通过有参数的构造方法,在初始化的时候设置好链表队列的最大长度

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

        首先我们来了解下LinkedBlokingQueue的属性

//定义链表的节点对象
static class Node<E> {
    //队列里的内容
    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<E> next;

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

/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity;

/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();

/**
 * Head of linked list.
 * Invariant: head.item == null
 */
transient Node<E> head;

/**
 * Tail of linked list.
 * Invariant: last.next == null
 */
private transient Node<E> last;

/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();

/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();

/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();

/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();

        既然是链表结构,那么就应该有节点Node,以及头节点head、尾结点last和容量大小capacity。同时定义了两个锁,一个是take锁一个是put锁,也就是使用了两个ReentrantLock作为独占锁,进行并发控制,通过双Condition算法控制锁的开合,即出队列条件Condition notEmpty和入队列条件Condition notFull。

        我再来了解下put方法

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    // Note: convention in all put/take/etc is to preset local var
    // holding count negative to indicate failure unless set.
    int c = -1;
    Node<E> node = new Node<E>(e);
    final ReentrantLock putLock = this.putLock;
    final AtomicInteger count = this.count;
    //加锁
    putLock.lockInterruptibly();
    try {
        /*
         * Note that count is used in wait guard even though it is
         * not protected by lock. This works because count can
         * only decrease at this point (all other puts are shut
         * out by lock), and we (or some other waiting put) are
         * signalled if it ever changes from capacity. Similarly
         * for all other uses of count in other wait guards.
         */
         //如果队列满了则阻塞
        while (count.get() == capacity) {
            notFull.await();
        }
        //否则则入队列
        enqueue(node);
        //当前元素个数加一,并返回加一前的数值
        c = count.getAndIncrement();
        //如果当前值小于队列上限,则发出notFull信号,唤醒入队列操作
        if (c + 1 < capacity)
            notFull.signal();
    } finally {
        //释放锁
        putLock.unlock();
    }
    if (c == 0)
        signalNotEmpty();
}
/**
 * Signals a waiting take. Called only from put/offer (which do not
 * otherwise ordinarily lock takeLock.)
 */
private void signalNotEmpty() {
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lock();
    try {
        notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
}

        根据以上代码,我们可以了解到put的主要流程

        其中最主要的是putLock,通过他实现了入队列阻塞。并通过notEmpty和takeLock交互。

        另外就是enqueue的入队列操作,其实就是一个链表在尾部添加元素的操作。即将last.next赋值为node,然后将last指针指导最后

        现在我们来了解下take方法

public E take() throws InterruptedException {
    E x;
    int c = -1;
    final AtomicInteger count = this.count;
    final ReentrantLock takeLock = this.takeLock;
    takeLock.lockInterruptibly();
    try {
        while (count.get() == 0) {
            notEmpty.await();
        }
        x = dequeue();
        c = count.getAndDecrement();
        if (c > 1)
            notEmpty.signal();
    } finally {
        takeLock.unlock();
    }
    if (c == capacity)
        signalNotFull();
    return x;
}
/**
 * Signals a waiting put. Called only from take/poll.
 */
private void signalNotFull() {
    final ReentrantLock putLock = this.putLock;
    putLock.lock();
    try {
        notFull.signal();
    } finally {
        putLock.unlock();
    }
}

        take的流程基本和put流程类似。

        和入队列一样,都是通过一个ReentrantLock实现了出队列的阻塞。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值