java并发编程——容器

1. 同步容器

在早期的JDK中,同步容器有两种现成的实现,Vector和Hashtable,可以直接new对象获取;在JDK1.2中,引入了同步封装类,可以由Collections.synchronizedXxxx等方法创建;这两种方式底层实现都是通过将状态封装起来,并对每个公有方法进行同步,使得每次只有一个线程能访问容器的状态。
以Hashtable为例查看jdk源码实现如下所示:

public synchronized V remove(Object key) {}
public synchronized V put(K key, V value) {}
public synchronized V get(Object key) {}

可以看到Hashtable与普通HashMap的区别就是相应方法都添加了synchronized进行同步,这个保证了容器操作的安全。但是,所有对容器状态的访问都串行化,这种代价就是严重降低了并发性,多个线程竞争的时候,吞吐量严重降低。

2. 并发容器

前面叙述了同步容器使用的局限性,JDK5.0开始增加了ConcurrentHashMap等并发容器来代替同步容器提高多线程并发情况下的性能。下面就其中典型并发容器使用和实现进行介绍:

2.1 ConcurrentHashMap

2.1.1ConcurrentHashMap设计

ConcurrentHashMap采用了分段锁的设计,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。相比于对整个Map加锁的设计,分段锁大大的提高了高并发环境下的处理能力。
ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点),对应下图可以看出之间的关系:

ConcurrentHashMap内部构成

从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。由于不同的Segment有各自的锁,理想情况下ConcurrentHashMap可以最高同时支持Segment数量大小的写操作。

2.1.2ConcurrentHashMap常用方法分析
get操作

查看JDK源码整个get操作过程不需要加锁,主要分两步第一步进行再散列,并根据再散列值定位到Segment;第二步找到对应HashEntry并遍历链表找到具体对应值。


get操作
put操作

查看JDK源码put操作,为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里面进行插入操作。插入操作需要进行两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里。


put操作
size操作

前面两个操作都是在单个segment中进行的,但是size操作是在多个segment中进行。ConcurrentHashMap的size操作也采用了一种比较巧的方式,来尽量避免对所有的Segment都加锁。ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计过程中,容器count发生了变化,则采用加锁的方式来统计所有Segment大小。这里ConcurrentHashMap通过判断modCount是否变化来判断容器在计算size过程中是否发生变化。

size操作

2.2 CopyOnWrite

CopyOnWrite即写入时复制的容器,每次修改的时,都会创建并重新发布一个新的容器副本,然后新的容器副本里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器,由于当前容器不会被修改所以支持无锁并发读取。

2.2.1 CopyOnWriteArrayList的实现原理

作为CopyOnWrite思想具体实现类CopyOnWriteArrayList,首先看下源码如何实现元素添加修改。可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

CopyOnWriteArrayList添加元素

同时,读取元素操作非常简单不需要锁如下图所示:

CopyOnWriteArrayList读取元素.png
2.2.2使用场景和注意点

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新的场景。
使用注意点:

  • 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。
  • 根据实际需求初始化容器大小,减少扩容带来的开销。
  • 该容器不适合存储占据内存大的大对象由于复制的原因容易引起FullGC
  • CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

2.3 并发队列ConcurrentLinkedQueue

在并发编程中需要使用线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现。并发队列ConcurrentLinkedQueue通过非阻塞方式实现。

2.3.1 ConcurrentLinkedQueue设计

ConcurrentLinkedQueue 的非阻塞算法实现主要可概括为下面几点:

  • 使用 CAS 原子指令来处理对数据的并发访问,这是非阻塞算法得以实现的基础。
  • head/tail 并非总是指向队列的头 / 尾节点,也就是说允许队列处于不一致状态。 这个特性把入队 /出队时,原本需要一起原子化执行的两个步骤分离开来,从而缩小了入队 /出队时需要原子化更新值的范围到唯一变量。这是非阻塞算法得以实现的关键。
  • 以批处理方式来更新head/tail,从整体上减少入队 / 出队操作的开销。

ConcurrentLinkedQueue由head节点和tail节点组成,每个节点(Node)由节点元素(item)和指向下一个节点的引用(next)组成,节点与节点之间就是通过这个next关联起来,从而组成一张链表结构的队列。默认情况下head节点存储的元素为空,tail节点等于head节点。

3. 阻塞队列

阻塞队列(BlockingQueue)与普通队列的区别在于添加了两个附加操作:当队列是空的时,从队列中获取元素的操作将会被阻塞;当队列是满时,往队列里添加元素的操作会被阻塞。
BlockingQueue最终会有四种状况,抛出异常、返回特殊值、阻塞、超时,下表总结了这些方法:

BlockingQueue方法总结.png
  • 抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException("Queue full")异常。
  • 返回特殊值:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null。
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。
  • 超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

3.1 JDK提供阻塞队列

JDK7提供了7个阻塞队列实现类,下面就其中常用类进行分析:

  1. ArrayBlockQueue:一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。创建其对象必须明确大小,像数组一样。默认情况下不保证访问者公平的访问队列,即不会根据阻塞的先后顺序来提供给访问者。
  2. LinkedBlockQueue:一个可改变大小的阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。创建其对象如果没有明确大小,默认值是Integer.MAX_VALUE。
  3. PriorityBlockingQueue:类似于LinkedBlockingQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数所带的Comparator决定的顺序。
  4. SynchronousQueue:同步队列。同步队列没有任何容量,每个插入必须等待另一个线程移除,反之亦然。即每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。
  5. DelayQueue:一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

3.2 ArrayBlockQueue实现原理

以ArrayBlockingQueue为例我们查看下JDK源码可以发现其主要使用Condition来实现通知模——当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

ArrayBlockingQueue初始化

生产者往满的队列里添加元素时会阻塞住生产者


put操作

消费者消费空队列的时候会阻塞消费者

take操作

3.3 阻塞队列的应用

阻塞队列支持生产者——消费者这种设计模式,当数据产生时候,生产者把数据放到队列当中,而当消费者准备处理数据时,将从队列中获取数据。生产者不需要知道消费者的标识或数量,只需要将数据放入队列即可。同样,消费者也不需要知道生产者是谁。 阻塞队列简化了生产者——消费者设计的实现过程,支持任意数量的生产者和消费者。

参考

http://www.cnblogs.com/dolphin0520/p/3932905.html
http://www.importnew.com/22007.html
http://ifeve.com/java-copy-on-write/
http://ifeve.com/java-blocking-queue/
http://blog.csdn.net/ghsau/article/details/8108292

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

推荐阅读更多精彩内容

  • layout: posttitle: 《Java并发编程的艺术》笔记categories: Javaexcerpt...
    xiaogmail阅读 5,804评论 1 19
  • 锁 锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多...
    黄俊彬阅读 1,478评论 1 15
  • Java SE 基础: 封装、继承、多态 封装: 概念:就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽...
    Jayden_Cao阅读 2,103评论 0 8
  • 1、与synchronized相比ReentrantLock拥有非阻塞的获取锁、响应中断、超时机制、支持公平性设置...
    wangjie2016阅读 1,155评论 1 8
  • 雷雨停歇 月亮爬上来 一颗、两颗星星 说着悄悄话 这个时候 老树打起了呼噜 小草轻轻地呓语 花儿娇羞地翻了身 这个...
    痴人一念阅读 336评论 8 25