概述

java cocurrent包提供了很多并发容器,在提供并发控制的前提下,通过优化,提升性能。本文主要讨论常见的并发容器的实现机制和绝妙之处,但并不会对所有实现细节面面俱到。

为什么JUC需要提供并发容器?

java collection framework提供了丰富的容器,有map、list、set、queue、deque。但是其存在一个不足:多数容器类都是非线程安全的,即使部分容器是线程安全的,由于使用sychronized进行锁控制,导致读/写均需进行锁操作,性能很低。

java collection framework可以通过以下两种方式实现容器对象读写的并发控制,但是都是基于sychronized锁控制机制,性能低:

1. 使用sychronized方法进行并发控制,如HashTable 和 Vector。以下代码为Vector.add(e)的java8实现代码:

    public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}

2.使用工具类Collections将非线程安全容器包装成线程安全容器。以下代码是Collections.synchronizedMap(Map<K,V> m)将原始Map包装为线程安全的SynchronizedMap,但是实际上最终操作时,仍然是在被包装的原始m上进行,只是SynchronizedMap的所有方法都加上了synchronized锁控制。

    public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m); //将原始Map包装为线程安全的SynchronizedMap
}
    private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable { private final Map<K,V> m; // Backing Map 原始的非线程安全的map对象
final Object mutex; // Object on which to synchronize 加锁对象 SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
} public V get(Object key) {
synchronized (mutex) {return m.get(key);} //所有方法加上synchronized锁控制
} public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);} //所有方法加上synchronized锁控制
}
......
}

为了提供高效地并发容器,java 5在java.util.cocurrent包中 引入了并发容器。

JUC并发容器

本节对juc常用的几个并发容器进行代码分析,重点看下这些容器是如何高效地实现并发控制的。在进行具体的并发容器介绍之前,我们提前搞清楚CAS理论是什么东西。因为在juc并发容器的很多地方都使用到了CAS,他比加锁处理更加高效。

CAS

CAS是一种无锁的非阻塞算法,全称为:Compare-and-swap(比较并交换),大致思路是:先比较目标对象现值是否和旧值一致,如果一致,则更新对象为新值;如果不一致,则表明对象已经被其他线程修改,直接返回。算法实现的伪码如下:

function cas(p : pointer to int, old : int, new : int) returns bool {
if *p ≠ old {
return false
}
*p ← new
return true
}

参考自wiki:Compare-and-swap

ConcurrentHashMap

ConcurrentHashMap实现了HashTable的所有功能,线程安全,但却在检索元素时不需要锁定,因此效率更高。

ConcurrentHashMap的key 和 value都不允许null出现。原因在于ConcurrentHashMap不能区分出value是null还是没有map上,相对的HashMap却可以允许null值,在于其使用在单线程环境下,可以使用containKey(key)方法提前判定是否能map上,从而区分这两种情况,但是ConcurrentHashMap在多线程使用上下文中则不能这么判定。参考:关于ConcurrentHashMap为什么不能put null

A hash table supporting full concurrency of retrievals and high expected concurrency for updates. This class obeys the same functional specification as Hashtable, and includes versions of methods corresponding to each method of Hashtable. However, even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access. This class is fully interoperable with Hashtable in programs that rely on its thread safety but not on its synchronization details.

ConcurrentHashMap个put和get方法,细节请看代码对应位置的注释。

public V put(K key, V value) {
return putVal(key, value, false);
} /** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin,当前hash对应的bin(桶)还不存在时,使用cas写入; 写入失败,则再次尝试。
}
else if ((fh = f.hash) == MOVED) //如果tab[i]不为空并且hash值为MOVED,说明该链表正在进行transfer操作,返回扩容完成后的table
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { // 加锁保证线程安全,但不是对整个table加锁,只对当前的Node加锁,避免其他线程对当前Node进行写操作。
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { //如果在链表中找到值为key的节点e,直接设置e.val = value即可
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e; //如果没有找到值为key的节点,直接新建Node并加入链表即可
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) { //如果首节点为TreeBin类型,说明为红黑树结构,执行putTreeVal操作
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) //如果节点数大于阈值,则转换链表结构为红黑树结构
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); //计数增加1,有可能触发transfer操作(扩容)
     return null; 
    }
  transient volatile Node<K,V>[] table; //元素所在的table是volatile类型,线程间可见

  public V get(Object key) {  //get无需更改size和count等公共属性,加上table是volatile类型,故而无需加锁。
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}

思考一个问题:为什么当新加Node对应的‘桶’不存在时可以直接使用CAS操作新增该桶,并插入新节点,但是当新增Node对应的‘桶’存在时,则必须加锁处理?

参考资料:Java并发编程总结4——ConcurrentHashMap在jdk1.8中的改进

java1.7 ConcurrentHashMap实现细节

附上HashMap jdk 1.8版本中的实现原理讲解,讲的很细也很通俗易懂:Jdk1.8中的HashMap实现原理

ConcurrentLinkedQueue

ConcurrentLinkedQueue使用链表作为数据结构,它采用无锁操作,可以任务是高并发环境下性能最好的队列。

ConcurrentLinkedQueue是非阻塞线程安全队列,无界,故不太适合做生产者消费者模式,而LinkedBlockingQueue是阻塞线程安全队列,可以做到有界,通常用于生产者消费者模式。

下面看下其offer()方法的源码,体会下:不使用锁,只是用CAS操作来保证线程安全。细节参考代码对应位置的注释。

     /**
* 不断尝试:找到最新的tail节点,不断尝试想最新的tail节点后面添加新节点
     */
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e); for (Node<E> t = tail, p = t;;) { //不断尝试:找到最新的tail节点,不断尝试想最新的tail节点后面添加新节点。
Node<E> q = p.next;
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time //t引用有可能并不是真实的tail节点的引用,多线程操作时,允许该情况出现,只要能保证每次新增元素是在真实的tail节点上添加的即可。
casTail(t, newNode); // Failure is OK. 即使失败,也不影响下次offer新的元素,反正后面会试图寻找到最新的真实tail元素
return true;
}
// Lost CAS race to another thread; re-read next CAS竞争失败,再次尝试
}
else if (p == q) //遇到哨兵节点(next和item相同,空节点或者删除节点),从head节点重新遍历。确保找到最新的tail节点
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q; //java中'!='运算符不是原子操作,故使用t != (t = tail)做一次判定,如果tail被其他线程更改,则直接使用最新的tail节点返回。
}
}

CopyOnWriteArrayList

CopyOnWriteArrayList提供高效地读取操作,使用在读多写少的场景。CopyOnWriteArrayList读取操作不用加锁,且是安全的;写操作时,先copy一份原有数据数组,再对复制数据进行写入操作,最后将复制数据替换原有数据,从而保证写操作不影响读操作。

下面看下CopyOnWriteArrayList的核心代码,体会下CopyOnWrite的思想:

public class CopyOnWriteArrayList<E>    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
} /**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
} public E get(int index) {
return get(getArray(), index);
} /**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); //写 互斥 读
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e; //对副本进行修改操作
setArray(newElements); //将修改后的副本替换原有的数据
return true;
} finally {
lock.unlock();
}
} }

ConcurrentSkipListMap

SkipList(跳表)是一种随机性的数据结构,用于替代红黑树,因为它在高并发的情况下,性能优于红黑树。跳表实际上是以空间换取时间。跳表的基本模型示意图如下:

ConcurrentSkipListMap的实现就是实现了一个无锁版的跳表,主要是利用无锁的链表的实现来管理跳表底层,同样利用CAS来完成替换。

参考资料

从零单排 Java Concurrency, SkipList&ConcurrnetSkipListMap

java并发编程——并发容器的更多相关文章

  1. Python并发编程-并发解决方案概述

    Python并发编程-并发解决方案概述 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.并发和并行区别 1>.并行(parallel) 同时做某些事,可以互不干扰的同一个时 ...

  2. Java并发编程:CopyOnWrite容器的实现

    Java并发编程:并发容器之CopyOnWriteArrayList(转载) 原文链接: http://ifeve.com/java-copy-on-write/ Copy-On-Write简称COW ...

  3. Java并发编程-并发工具包(java.util.concurrent)使用指南(全)

    1. java.util.concurrent - Java 并发工具包 Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包.这个包包含有一系列能够让 Ja ...

  4. java并发编程 --并发问题的根源及主要解决方法

    目录 并发问题的根源在哪 缓存导致的可见性 线程切换带来的原子性 编译器优化带来的有序性 主要解决办法 避免共享 Immutability(不变性) 管程及其他工具 并发问题的根源在哪 首先,我们要知 ...

  5. 并发编程>>并发级别(二)

    理解并发 这是我在开发者头条看到的.@编程原理林振华 有目标的提升自己会事半功倍,前行的道路并不孤独. 1.阻塞 当一个线程进入临界区(公共资源区)后,其他线程必须在临界区外等待,待进去的线程执行完成 ...

  6. Java并发编程--同步容器

    BlockingQueue 阻塞队列 对于阻塞队列,如果BlockingQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤 ...

  7. Java并发编程-并发工具类及线程池

    JUC中提供了几个比较常用的并发工具类,比如CountDownLatch.CyclicBarrier.Semaphore. CountDownLatch: countdownlatch是一个同步工具类 ...

  8. Java多线程编程——并发编程原理(分布式环境中并发问题)

    在分布式环境中,处理并发问题就没办法通过操作系统和JVM的工具来解决,那么在分布式环境中,可以采取一下策略和方式来处理: 避免并发 时间戳 串行化 数据库 行锁 统一触发途径 避免并发 在分布式环境中 ...

  9. Java并发编程--并发容器之Collections

    在JDK1.2之前同步容器类包括Vector.Hashtable,这两个容器通过内置锁synchronized保证了同步.后面的ArrayList.LinkedList.HashMap.LinkedH ...

随机推荐

  1. 程序员减少代码BUG的7种方法,拒绝编程5分钟,查代码2小时!

    0.别对警告视而不见 相信不少的程序员会使用IDEA,使用它敲代码,有时候会出现警告,那么这时你对警告就不能视而不见了. 我们的目标是,写干净的代码,做风一样的男子! 1.编程习惯 种瓜得瓜种豆得豆, ...

  2. mac lnmp

    Mac下安装LNMP(Nginx+PHP5.6)环境 Mac下安装LNMP(Nginx+PHP5.6)环境 安装Homebrew 最近工作环境切换到Mac,所以以OS X Yosemite(10.10 ...

  3. javascript 大中小括号的区别

    小括号 JavaScript中小括号有五种语义 语义1,函数声明时参数表 function func(arg1,arg2){ // ... } 语义2,和一些语句联合使用以达到某些限定作用 // 和f ...

  4. ZT 人生真的是一场马拉松吗?

    中国合伙人:孟晓俊:生活应该是什么样的?自己提出的问题应该由自己来回答,别人的回答是别人的答案,是别人的生活,而你应该过自己的生活,不是别人的生活.     人生真的是一场马拉松吗? 投递人 itwr ...

  5. HTML5的新标签-整体布局

    过去:<div class="header"> <div class="hgroup"> <h1>....</h1&g ...

  6. Gitflow 工作流简介

    Gitflow工作流简介 Gitflow工作流通过为功能开发.发布准备和项目维护分配独立的分支,让发布迭代过程更流畅. Gitflow工作流定义了一个围绕项目发布的严格分支模型,它会相对复杂一点,但提 ...

  7. quartz(转)

    1.   CronTrigger时间格式配置说明 CronTrigger配置格式: 格式: [秒] [分] [小时] [日] [月] [周] [年] 序号 说明 是否必填 允许填写的值 允许的通配符 ...

  8. Upgrade Oracle Database 12c Release 2(12.2) RAC on RHEL7.3 with RU

    Upgrade Oracle Database 12c Release 2(12.2) RAC on RHEL7.3 -- [ RU: 26610291 (GRID INFRASTRUCTURE RE ...

  9. for 循环使用 enumerate 以及yield生成器简单例子

    >>>seq = ['one', 'two', 'three'] >>> for i, element in enumerate(seq): ... print i ...

  10. 【OC底层】AssociatedObject 关联对象

    如何实现给分类“添加成员变量”? 默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中.但可以通过关联对象来间接实现 关联对象提供了以下API 1> 添加关联对象 void objc_s ...