聊聊阻塞队列——LinkedBlockingQueue

聊聊阻塞队列——LinkedBlockingQueue

特点

  1. 无界阻塞队列,可以指定容量,默认为 Integer.MAX_VALUE,先进先出,存取互不干扰
  2. 使用链表的数据结构进行实现
  3. 锁分离:存取互不干扰,存取操作的是不同的Node对象,存操作(takeLock)的是尾节点,取操作(putLock)的是头节点
  4. 阻塞队列中实际存储的是数据,条件等待队列中阻塞的是线程

代码

package com.company.blockingqueue;

import java.util.concurrent.LinkedBlockingQueue;

/**
 * @Author: Alan
 * @Date: 2022/11/28 11:23
 */
public class LinkedBlockingQueueDemo {
    public static void main(String[] args) {
        LinkedBlockingQueue<String> linkedBlockingQueue = new LinkedBlockingQueue<>();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                String e = null;
                try {
                    e = linkedBlockingQueue.take();
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                System.out.println("取出元素:" + e);
            }
        }).start();

        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                try {
                    String e = String.valueOf(i);
                    linkedBlockingQueue.put(e);
                    System.out.println("放入元素:" + e);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
            }
        }).start();
    }
}

put流程

put()操作主要负责把一个对象放入到阻塞队列中(即生产数据)。从源码可以看出来,其主要做了以下4件事儿。
在这里插入图片描述

  1. 根据传入的值,构建一个节点
  2. 使用尾插法将新节点入队
    在这里插入图片描述
  3. 新节点入队后,对count(保存队列中当前值的个数)进行加1,执行count.getAndIncrement()方法调用,会对count加1,然后返回count的旧值。如果此时阻塞队列未达到设定的容量,那么会将条件等待队列中的生产者线程转移同步等待队列中。
  4. 如果此时c=0,那么说明此时像阻塞队列中投递了一个可以消费的对象,此时可以唤醒消费者线程来进行消费(实质也是先讲消费者先从条件等待队列中转移到同步等待队列中)。

take流程

take()主要负责从阻塞队列中取出一个对象(即消费数据)。从源码可以看出来,其主要做了4件事儿。
在这里插入图片描述

  1. 查看一下count的值,如果为0,即阻塞队列为空,那么阻塞消费者线程(没有数据可以被消费)

  2. 当第一步的while循环条件不满足时,即阻塞队列中已经有了可以被消费的数据,那么将阻塞队列中的头节点出队(这里给大家提个醒,我们的阻塞队列中实际存储的是数据,条件等待队列中阻塞的是线程,所以这里出队也就是数据)。

    在这里我们来看看阻塞队列中,数据的存储情况。未出队前,阻塞队列如下所示
    在这里插入图片描述
    头节点出队后的阻塞对列情况。
    在这里插入图片描述

  3. 如果count>1,那么说明原本阻塞队列中的元素大于等于两个,所以这里还可以继续将其他的消费者线程转移到同步队列中,对阻塞队列中的元素进行消费(finally代码块中takeLock释放锁后,同步队列中阻塞的线程便会被唤醒)。

  4. 如何c自减前等于capacity,经过前面的操作后,此时阻塞队列中肯定会有一个空位,那么就可以唤醒条件等待队列中的生产者线程。这里signalNotFull()方法,实现了将条件队列中阻塞的生产者线程转移到同步阻塞队列,同时唤醒同步阻塞队列中的线程。

remove流程

在这里插入图片描述

remove(Object o)操作,负责移除阻塞队列中指定的元素。该方法主要阻要做了4件事儿。

  1. 首先获取putLock和takeLock,这两把锁分别用于在阻塞队列中put和take元素时,加锁。我们看了上面put和take的流程分析,每次在取、存元素的时候都只是加了一把锁,为啥在移除的时候却要加两把锁呢?

    我们来假设一下,假设把remove(Object o)操作想象成是特殊的take()操作,即我们只加takeLock锁,那么也就是说我们在移除元素的时候,是允许有线程新增加元素的。此时我们来看下remove时,元素出队的逻辑(下面的第一张图),if前的逻辑我们先不看(这里便是节点出队),if判断通过后会对last指针进行修改。我们再来看看节点执行put操作时,节点入队的逻辑(下面的第二张图),因为是使用尾插法,所以同样会对last指针进行操作。在这里我们便可以发现,last指针在两个方法中是共享变量,如果两个线程对其同时进行写操作,便会存在线程安全问题。因此,只加一把锁是不够的。
    在这里插入图片描述
    在这里插入图片描述
    上面在分析remove(Obejct o)操作为啥要加两把锁时,我们从last指针被共享的角度进行了分析,从head指针被共享的角度同样可以得出这个结论。

  2. 遍历链表,寻找和被移除元素值相同的元素。如果相同,那么通过unlink()方法进行移除,并返回true。这里需要注意,remove(Object e)只会移除一个元素。

  3. 释放锁,这里和操作1是成对出现的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值