ArrayBlockingQueue源码分析

本文深入解析了ABQ(ArrayBlockingQueue)的工作原理,包括其构造方法、核心操作如add、offer、put和take,以及内部实现细节。通过对比改造前后的代码逻辑,展示了ABQ在提高程序性能和降低耦合度方面的优势。

前言

  如果说CHM的源码对并发分片和CAS操作运用的淋漓尽致,那么ABQ则是Lock & Condition类的使用参考书~

阻塞队列的应用

  见名知义,ABQ是基于数组实现的阻塞队列。
  大家常用的MQ、Kafka使得程序之间实现解耦。同理,阻塞队列使得线程之间进行数据通信与解耦。
  举一个熟悉场景——用户注册成功后,增加积分。通常,我们可以通过MQ来实现注册、积分操作的异步与解耦。但假如我们的程序规模比较小,采用阻塞队列会更加方便。

  • 改造前的代码逻辑
	public class UserService {
		public boolean register(){
			User user=new User();
			user.setName("Jeremy");
			addUser(user);//注册用户
			sendPoints(user);//增加积分
		return true;
	}

	public static void main(String[] args) {
		new UserService().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();
			}
		}
	}
  • 改造后的逻辑
	public class UserService {
		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(){
			User user=new User();
			user.setName("Mic");
			addUser(user);
			arrayBlockingQueue.add(user);//添加到异步
			队列
			return true;
		}
		
		public static void main(String[] args) {
			new UserService().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();
			}
		}
	}
  • 优化后流程图
    在这里插入图片描述

  修改之后的代码在性能 & 耦合度方面都有提高。
  阻塞队列主要应用于生产者、消费者场景中。

JUC中的阻塞队列

阻塞队列原理
ArrayBlockingQueue数组实现的有界阻塞队列,FIFO
LinkedBlockingQueue链表实现的有界阻塞队列,FIFO
PriorityBlockingQueue支持优先级排序的无界阻塞队列,可通过实体类实现compareTo()或构造参数Comparator实现排序
DelayQueue优先级排序的无界阻塞队列
SynchronousQueue不存储元素的阻塞队列
LinkedTransferQueue链表实现的无界阻塞队列
LinkedBlockingDeque链表实现的双向阻塞队列

阻塞队列的操作方法

  阻塞队列中,插入和删除操作都提供了四种处理方式。

  • 插入操作
    • add(e) → 队列满了,继续插入会报错,IllegalStateException
    • offer(e) → 返回元素是否成功插入,成功返回true
    • put(e) → 队列满了,生产者继续put,会阻塞生产者线程,直到队列可用
    • offer(e,time,unit) → 队列满了继续添加元素,则生产者线程被阻塞指定时间,超时后,线程退出
  • 移除操作
    • remove() → 队列为空,返回false;移除成功,返回true
    • poll() → 队列为空,返回null
    • take() → 队列为空,阻塞take(),直到队列中有数据消费
    • poll(time,unit) → 队列为空,等待指定时间,在获取数据

ABQ源码分析

构造方法

  ABQ提供三个构造方法:

  1. capacity → 队列长度
	public ArrayBlockingQueue(int capacity) {
        this(capacity, false);//默认非公平锁
    }
  1. fair → 是否为公平的阻塞队列
	public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);//重入锁,出队&入队持有同一把锁
        notEmpty = lock.newCondition();//初始化非空等待队列
        notFull =  lock.newCondition();//初始化非满等待队列
    }
  1. 接受初始数据

Add方法

add() → 父类AbstractQueue的add()

	public boolean add(E e) {
        return super.add(e);//调用父类的模板方法 → 解决通用性问题
    }

AbstractQueue的add()

 	public boolean add(E e) {
        if (offer(e))//调用offer()
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

offer方法

  1. 判断添加数据是否为空
  2. 添加重入锁
  3. 判断队列长度,如果队列长度=数组长度,表示满了,直接返回false
  4. 否则,调用enqueue()将元素添加到队列
	public boolean offer(E e) {
        checkNotNull(e);//对请求数据作判断
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            if (count == items.length)
                return false;
            else {
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }
enqueue方法

  核心逻辑,通过putIndex索引直接将元素添加到数组队列中。

	private void enqueue(E x) {
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = x;//通过putIndex对数据赋值
        //当putIndex等于数组长度时,将putIndex重置为0
        if (++putIndex == items.length)
            putIndex = 0;
        count++;//记录队列元素的个数
        //唤醒notEmpty队列中的线程
        //表明当前队列中元素不为空,如果存在消费者线程阻塞,就可以开始取元素了
        notEmpty.signal();
        
    }

  这里需要注意的是,putIndex超过数组长度时,重新从数组头节点插入数据,但并不会出现数据覆盖的情况。这是因为:

  • 元素个数由count统计。满了后,无法继续添加,会报错。
  • 队列中的元素会有消费者通过take()等方法获取,获取后会从队列中移除。

put方法

  队列满了,会阻塞。

	public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.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();//队列为空,加入notEmpty等待队列
            return dequeue();
        } finally {
            lock.unlock();
        }
    }
dequeue方法

  出队列,删除队列头部元素并返回给客户端索引值。

	private E dequeue() {
        // assert lock.getHoldCount() == 1;
        // assert items[takeIndex] != null;
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
        E x = (E) items[takeIndex];//默认获取0位置元素
        items[takeIndex] = null;//将该位置元素设置为空
        if (++takeIndex == items.length)//拿到数组最大值,索引重置为0
            takeIndex = 0;
        count--;//元素个数递减
        if (itrs != null)
            itrs.elementDequeued();//更新迭代器中元素
        notFull.signal();//触发 因队列满了导致阻塞的notFull等待队列
        return x;
    }
itrs.elementDequeued()

remove方法

  移除一个指定元素。

	public boolean remove(Object o) {
        if (o == null) return false;
        final Object[] items = this.items;//获取数组元素
        final ReentrantLock lock = this.lock;
        lock.lock();//获得锁
        try {
            if (count > 0) {//如果队列不为空
            	//获取下一个添加元素时索引
                final int putIndex = this.putIndex;
                //获取当前要移除元素索引
                int i = takeIndex;
                do {
                	//从takeIndex下标开始,找到要被删除的元素
                    if (o.equals(items[i])) {
                        removeAt(i);//移除指定元素
                        return true;//返回执行结果
                    }
                    //当前删除索引+1后,判断是否与数组长度相等
                    //若为true,说明索引已到数组尽头,重置为0
                    if (++i == items.length)
                        i = 0;
                } while (i != putIndex);//继续查找,直到最后一个元素
            }
            return false;
        } finally {
            lock.unlock();
        }
    }

总结

  ABQ的设计也是精妙的。

  • 通过成员变量count获取队列中元素个数
  • 通过两个阻塞队列notEmptynotFull控制添加和获取元素时的阻塞
  • 两个阻塞队列与ABQ之间通过Condition的await()&signal()完成通信
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值