详解-阻塞队列-底层原理

683 阅读6分钟

生产者消费者的实际应用

分布式消息队列应该都使用过,比如ActiveMQ、 kafka、RabbitMQ等等,消息队列可以使得程序之间实现解耦,提升程序响应的效率。如果我们把多线程环境比作是分布式的话,那么线程与线程之间是不是也可以使用这种消息队列的方式进行数据通信和解耦呢?

阻塞队列

阻塞队列的应用场景

阻塞队列这块的应用场景,比较多的仍然是对于生产者消费者场景的应用,但是由于分布式架构的普及,是的大家更多的关注在分布式消息队列上。所以其实如果把阻塞队列比作成分布式消息队列的话,那么所谓的生产者和消费者其实就是基于阻塞队列的解耦。另外,阻塞队列是一个 fifo 的队列,所以对于希望在线程级别需要实现对目标服务的顺序访问的场景中,也可以使用。

阻塞队列案例演示

  • 注册成功后增加积分 假如我们模拟一个场景,就是用户注册的时候,在注册成功以后发放积分。
public class ThreadTest {
    private final ExecutorService single = Executors.newSingleThreadExecutor();
    private volatile boolean isRunning = true;
    ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue(10);
    {
        init();
    }
    public void init() {
        single.execute(() -> {
            while (isRunning) {
                try {
                    //阻塞的方式获取队列中的数据
                    User user = (User) arrayBlockingQueue.take();
                    sendPoints(user);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public boolean register() {
        while (isRunning) {
            User user = new User();
            user.setName("Mic"); 
            addUser(user);
            //添加到异步队列
            arrayBlockingQueue.add(user);
        }
        return true;
    }

    public static void main(String[] args) {
        new ThreadTest().register();
    }

    private void addUser(User user) {
        System.out.println("添加用户:" + user);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void sendPoints(User user) {
        System.out.println(" 发送积分给指定用户:" + user);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  • 图解分析

J.U.C中为我们提供的阻塞队列

  • 在Java8中,提供了7个阻塞队列

ArrayBlockingQueue源码分析

阻塞队列的操作方法 在阻塞队列中,提供了四种处理方式

插入操作add(e):添加元素到队列中,如果队列满了,继续插入元素会报错,IllegalStateException。

offer(e): 添加元素到队列,同时会返回元素是否插入成功的状态,如果成功则返回true

put(e):当阻塞队列满了以后,生产者继续通过 put 添加元素,队列会一直阻塞生产者线程,知道队列可用 offer(e,time,unit) :当阻塞队列满了以后继续添加元素,生产者线程会被阻塞指定时间,如果超时,则线程直接退出

移除操作 remove():当队列为空时,调用 remove 会返回 false,如果元素移除成功,则返回true

poll(): 当队列中存在元素,则从队列中取出一个元素,如果队列为空,则直接返回null

take():基于阻塞的方式获取队列中的元素,如果队列为空,则take方法会一直阻塞,直到队列中有新的数据可以消费

poll(time,unit):带超时机制的获取数据,如果队列为空,则会等待指定的时间再去获取元素返回

  • 首先看一下 ArrayBlockingQueue 的构造方法

ArrayBlockingQueue提供了三个构造方法,分别如下。capacity:表示数组的长度,也就是队列的长度 fair:表示是否为公平的阻塞队列,默认情况下构造的是非公平的阻塞队列。其中第三个构造方法就不解释了,它提供了接收一个几个作为数据初始化的方法。

    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        //创建一个 Object 数组
        this.items = new Object[capacity];
        //初始化了一个重入锁
        lock = new ReentrantLock(fair);
        //创建一个 Condition 队列,用来初始化非空等待队列
        notEmpty = lock.newCondition();
        //创建一个 Condition 队列,用来初始化非满等待队列
        notFull =  lock.newCondition();
    }
  • add() 方法
    public boolean add(E e) {
    	//判断是否能插入数据,如果队列满了抛出异常
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

    public boolean offer(E e) {
    	//判断插入数据是否为null
        checkNotNull(e);
        //拿到重入锁,保证线程安全
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//如果队列中的数据等于数组的长度,返回false
            if (count == items.length)
                return false;
            else {
            	//往队列中添加数据
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

enqueue() 方法是最核心的逻辑,方法内部通过putIndex索引直接将元素添加到数组items

    private void enqueue(E x) {
        //拿到 items 队列
        final Object[] items = this.items;
        //添加进数组中
        items[putIndex] = x;
        //如果索引下标等于数组长度,将putIndex置为0
        if (++putIndex == items.length)
            putIndex = 0;
        //队列中的数据数量 + 1
        count++;
        //唤醒处于等待状态下的线程(消费者线程),表示当前队列中的元素不为空,如果存在消费者线程阻塞,就可以开始取出元素 
        notEmpty.signal();
    }

首先解释一下为什么要把 putIndex 置为0,而插入元素的话从头部一直到尾部,如果到了尾部就要开始从头部插入,肯定也会有一个指针表示消费了的数据,并且也是从头部开始拿数据。

可以看到如果调用add()方法是不会进入阻塞队列,如果队列满了则直接报错,只有队列有空的情况下才会插入数据。

  • 接下来看一下使用了阻塞队列的put()方法,和add()方法唯一的不同就是调用了 notFull.await();如果已满则把当前线程阻塞,放入链表中
    public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        //这个也是获得锁,但是和 lock 的区别是,这个方法优先允许在等待时由其他线程调用等待线程的 interrupt 方法来中断等待直接返回。而lock方法是尝试获得锁成功后才响应中断 
        lock.lockInterruptibly();
        try {
            while (count == items.length)
            	//队列满了的情况下,当前线程将会被 notFull 条件对象挂起加到等待队列中 
                notFull.await();
            enqueue(e);
        } finally {
            lock.unlock();
        }
    }

  • take()方法,如果队列中没有数据就把当前线程阻塞,否则从队列中取出一个元素返回
public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
            	//阻塞当前线程
                notEmpty.await();
            //取出数据
            return dequeue();
        } finally {
            lock.unlock();
        }
    } 

如果队列中添加了元素,那么这个时候,会在enqueue中调用notempty.signal唤醒take线程来获得元素

  • dequeue(),出队列的方法
    private E dequeue() {
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        //默认获取 0 位置的元素
        E x = (E) items[takeIndex];
        //该位置的数据设置为 null
        items[takeIndex] = null;
        //如果等于长度,则把索引下标重新设置为0
        if (++takeIndex == items.length)
            takeIndex = 0;
        //数据数量 - 1
        count--;
        //同时更新迭代器中的元素数据
        if (itrs != null)
            itrs.elementDequeued();
        //唤醒 notFull 对象中被阻塞的线程
        notFull.signal();
        return x;
    }