1.Concurrent Collections Overview

Concurrent Collections Overview

生产者-消费者模型最简单的实现方式是什么?使用Java语言中的BlockingQueue是最简单有效的实现方式。本节我们将对Java并发容器给出介绍,完成我们在《深入理解Java集合框架》系列文章中未竟的内容。

BlockingQueue

BlockingQueue是一个阻塞队列接口,所谓阻塞队列就是在添加元素或获取元素时,线程会阻塞等待直到队列不满或者非空。该接口常见实现类有ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueue等,前两个分别是依靠数组和链表实现的阻塞队列,后一个是阻塞的优先队列。关于数组链表以及优先队列数据结构方面的知识,《深入理解Java集合框架》系列文章已经讲解的非常清楚,不再重复。此处主要考察阻塞队列的特点和用法,阻塞队列常见的接口方法如下表,不同方法对特殊情况的处理方式不同:

抛异常 返回特殊值 阻塞 阻塞直到超时
插入 add(e) offer(e) put(e) offer(e, time, unit)
删除 remove() poll() take() poll(time, unit)
查看头部 element() peek()

上表中后两列方法使用较多,因为我们使用阻塞队列显然是想发挥其阻塞的特性。

BlockingQueue是线程安全的且不允许放入null值,常用于生产者-消费者模式的线程间数据共享。通常队列都会有一个固定大小,能够乘放指定个数个元素。当队列空间占满时,生产者将会挂起直到队列不满;当队列为空时,消费者将会挂起直到队列非空。一个简单而实用的例子如下:

// 使用BlockingQueue实现生产者-消费者模式
public class ProducerConsumer {
    public static void main(String[] args) {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(16);// 固定容量为16的阻塞队列
        new Producer<String>(queue).start();// Producer 0
        new Producer<String>(queue).start();// Producer 1
        new Consumer<String>(queue).start();// Consumer
    }
    static class Producer<E> extends Thread{
        private BlockingQueue<E> queue;
        public Producer(BlockingQueue<E> queue){ this.queue = queue; }
        @Override
        public void run(){//不停生产元素,添加到共享队列当中
            while(true){
                try {
                    E e = produce();
                    queue.put(e);// 挂起,直到队列非满
                } catch (InterruptedException e1) { /* exception */ }
            }
        }
        protected E produce(){return (E)String.valueOf(System.nanoTime());}
    }
    static class Consumer<E> extends Thread{
        private BlockingQueue<E> queue;
        public Consumer(BlockingQueue<E> queue){ this.queue = queue; }
        @Override
        public void run(){//不断从共享队列当中取出元素,并消费
            while(true){
                try {
                    E e = queue.take();// 挂起,直到队列非空
                    consume(e);
                } catch (InterruptedException e1) { /* exception */ }
            }
        }
        protected void consume(E e){System.out.println(e);}
    }
}

上述代码使用ArrayBlockingQueue作为共享队列,实现了生产者-消费者模式,并指定了两个生产者和一个消费者,现实场景中生产者和消费者的数量都是不确定的,可以是多个,也可以暂时是零个。上述代码的示意图如下:

BlockingDeque

BlockingDeque是阻塞双端队列接口,所谓双端队列就是队列的首尾都可以添加或删除元素,这意味着双端队列既可以当作栈使用,也可以当作队列使用,LinkedBlockingDeque是该接口的唯一实现类。关于双端队列接口介绍可参考前文,跟阻塞队列类似,阻塞双端队列的方法也分为阻塞和非阻塞,具体可参考JDK API,这里比在列举。跟BlockingQueue类似,阻塞双端队列也不允许放入null值,常用于多线程之间共享数据,如果对应到生产者-消费者模式上,就是生产者和消费者都可以产生或者消费数据。

LinkedBlockqingDeque可以在构造是指定固定大小,如果没有指定,则默认大小为Integer.MAX_VALUE。

ConcurrentLinkedQueue and ConcurrentLinkedDeque

ConcurrentLinkedQueue是线程安全的队列,ConcurrentLinkedDeque是线程安全的双端队列,关于Queue和Deque的接口说明前面已经讲的非常清楚,不再赘述。这两个类都是基于链表的,并且没有容量限制,不允许放入null值;这两个类的迭代器都是弱一致的(weakly consistent),在迭代的过程中插入和删除元素并不会导致ConcurrentModificationException,并且插入和删除效果会直接体现在迭代器中。值得注意的是,跟大多数容器不同这两个类的size()操作并开销较大,因为需要遍历内部的所有元素(而不是通过一个计数器直接返回),由于遍历过程中也可能有元素的插入和删除操作,最后返回的大小不一定表示当前的实际大小。

CopyOnWriteArrayList and CopyOnWriteArraySet

CopyOnWriteArraySet内部通过CopyOnWriteArrayList实现,这里直接介绍CopyOnWriteArrayList,它是一个线程安全的ArrayList,内部通过数组实现,任何改变(add()或者set()等)都会导致产生一个新的内部数组(就像名字中Copy on Write暗示的那样)。你是不是觉得这样实现效率太低,因为如果元素修改频繁,就会导致大量的拷贝,毕竟哪怕只修改一个元素也会导致对所有元素的拷贝。事实确实如此,但该类定位于修改极少而迭代很多的场景,因为修改是在副本上进行的,对CopyOnWriteArrayList的迭代内部并不需要加锁,这使得迭代效率很高。

CopyOnWriteArraySet内部通过CopyOnWriteArrayList实现,适合于集合很小并且迭代次数远远多于修改次数的场景。

ConcurrentSkipListMap and ConcurrentSkipListSet

首先说明ConcurrentSkipListSet内部实现是对ConcurrentSkipListMap的包装,就像参阅:HashSet和HashMap的关系那样,所以这里只着重说一下ConcurrentSkipListMap是个什么东西。ConcurrentSkipListMap是一种基于跳表(skip list)的并发有序Map。跟参阅:TreeMap类似ConcurrentSkipListMap是按照key值有序的,但内部不是通过红黑树实现,而是通过跳表实现。如果你需要一个按照key值排序且线程安全的Map,相比使用Collectoins.synchronizedMap(TreeMap),ConcurrentSkipListMap显然是最佳选择。

ConcurrentHashMap

终于轮到大名鼎鼎的ConcurrentHashMap登场了,它是线程安全的HashMap,用法跟HashMap一样。其内部采用分块加锁的形式来提高并发度,实现的非常巧妙很值得深入学习,我们会专门花一解单独介绍它的具体实现,敬请期待!

总结

本文介绍了Java常见的并发容器,使用这些并发容器能够简化编程,同时保证并发效率,当需要使用并发容器时,应首先考虑这里列举的类,而不是使用Collections.synchronizedXXX()对非并发容器进行包装。这么多工具类,总有一款适合你!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容