java并发面试常识之ArrayBlockingQueue

       ArrayBlockingQueue是常用的线程集合,在线程池中也常常被当做任务队列来使用。使用频率特别高。他是维护的是一个循环队列(基于数组实现),循环结构在数据结构中比较常见,但是在源码实现中还是比较少见的。

线程安全的实现

      线程安全队列,基本是离不开锁的。ArrayBlockingQueue使用的是ReentrantLock,配合两种Condition,实现了集合的线程安全操作。这里稍微说一个好习惯,下面是成员变量的声明。

1 private static final long serialVersionUID = -817911632652898426L; 2 final Object[] items; 3 int takeIndex; 4 int putIndex; 5 int count; 6 final ReentrantLock lock; 7 private final Condition notEmpty; 8 private final Condition notFull; 9 transient Itrs itrs = null;

        赋值的操作基本都是在构造函数里做的。这样有个好处,代码执行可控。成员变量的初始化也是会合并在构造方法里执行的,但是在执行顺序上需要好好斟酌,如果写在构造方法里初始化,则没有相关问题。

        阻塞队列的常用场所就是生产者消费者。一般都是生产者放入,消费者从头取数据。下面重点说这两个操作。

        这两个操作都是依靠锁来保证线程安全的。

生产操作

1 public void put(E e) throws InterruptedException { 2 checkNotNull(e); 3 final ReentrantLock lock = this.lock; 4 lock.lockInterruptibly(); 5 try { 6 while (count == items.length) 7 notFull.await(); 8 enqueue(e); 9 } finally { 10 lock.unlock(); 11 } 12 }

        put等放入操作,首先是获取锁,如果发现数据满了,就通过notFull的condition,来阻塞线程。这里的条件判定一定是用while而不是if,多线程情况下,可以被唤醒后发现又满了。

1 private void enqueue(E x) { 2 final Object[] items = this.items; 3 items[putIndex] = x; 4 if (++putIndex == items.length) 5 putIndex = 0; 6 count++; 7 notEmpty.signal(); 8 }

        这个是入队列的操作。首先获取维护的数组。putindex就是放入操作的标志。这个操作会一直加。达到预定的长度后就变成0从头开始计数。这样插入的操作就是一个循环的操作了,count就是用来做计数的,作为能否插入数据的一个标准,插入数据后就通过notEmpty的condition发出一个信号唤醒消费线程。

消费操作

1 public E take() throws InterruptedException { 2 final ReentrantLock lock = this.lock; 3 lock.lockInterruptibly(); 4 try { 5 while (count == 0) 6 notEmpty.await(); 7 return dequeue(); 8 } finally { 9 lock.unlock(); 10 } 11 }

        消费的方法也是这样。先获取锁,然后进行条件判断,如果没有数据,则阻塞线程。注意点和put一样。

1 private E dequeue() { 2 final Object[] items = this.items; 3 @SuppressWarnings("unchecked") 4 E x = (E) items[takeIndex]; 5 items[takeIndex] = null; 6 if (++takeIndex == items.length) 7 takeIndex = 0; 8 count--; 9 if (itrs != null) 10 itrs.elementDequeued(); 11 notFull.signal(); 12 return x; 13 }

        取数据的时候,也依靠takeIndex,这是一个标志,这个数值也会一直增加,表示取的第一个数据的位置。如果这个标志走到最后,然后变成0,从头再来。这样保证取出的数据都是fifo的顺序。删除的时候如果发现迭代中,则会修改迭代器的遍历。然后通过notFull的condition来唤醒生产线程。

移除操作

1 public boolean remove(Object o) { 2 if (o == null) return false; 3 final Object[] items = this.items; 4 final ReentrantLock lock = this.lock; 5 lock.lock(); 6 try { 7 if (count > 0) { 8 final int putIndex = this.putIndex; 9 int i = takeIndex; 10 do { 11 if (o.equals(items[i])) { 12 removeAt(i); 13 return true; 14 } 15 if (++i == items.length) 16 i = 0; 17 } while (i != putIndex); 18 } 19 return false; 20 } finally { 21 lock.unlock(); 22 } 23 }

        对于remove操作就比较麻烦了,首先获取锁之后,把两个标志位本地化,然后找到要删除的元素的位置。调用removeAt,这里删除需要对标志位做改变。

1 void removeAt(final int removeIndex) { 2 final Object[] items = this.items; 3 if (removeIndex == takeIndex) { 4 items[takeIndex] = null; 5 if (++takeIndex == items.length) 6 takeIndex = 0; 7 count--; 8 if (itrs != null) 9 itrs.elementDequeued(); 10 } else { 11 final int putIndex = this.putIndex; 12 for (int i = removeIndex;;) { 13 int next = i + 1; 14 if (next == items.length) 15 next = 0; 16 if (next != putIndex) { 17 items[i] = items[next]; 18 i = next; 19 } else { 20 items[i] = null; 21 this.putIndex = i; 22 break; 23 } 24 } 25 count--; 26 if (itrs != null) 27 itrs.removedAt(removeIndex); 28 } 29 notFull.signal(); 30 }

        如果删除的元素是位置和takeindex一样。那就可以直接删除,然后让删除标志位向后移动。如果不是,则从删除的位置开始,进行后面向前面的数据覆盖的操作。直到遇到putindex的前一个位置。然后把那个位置的数据设置为null。并且把putindex的位置往前移动一格,正在迭代的时候要删除数据并且唤醒生产线程。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

Java 并发数据结构

\TOCM\因为Java提供了一些非线程安全的数据结构如HashMap,ArrayList,HashSet等。所有在多线程环境中需要使用支持并发访问操作的数据结构。并发ListVector,CopyOnWriteArrayList是线程安全的List。ArrayList是线程不安全的。如果一定要使用,需要:Collection

ArrayBlockingQueue 和LinkedBlockQueue

ArrayBlockingQueueArrayBlockingQueue是Java多线程常用的线程安全的一个集合,基于数组实现,继承自AbstractQueue,实现了BlockingQueue和Serializable接口。//先看看器内部的成员变量:privatestaticfinallongserialV