概述

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. Java面试题-并发容器和框架

    1. 如何让一段程序并发的执行,并最终汇总结果? 答:使用CyclicBarrier 和CountDownLatch都可以,使用CyclicBarrier 在多个关口处将多个线程执行结果汇总,Coun ...

  2. java并发程序和共享对象实用策略

    java并发程序和共享对象实用策略 在并发程序中使用和共享对象时,可以使用一些实用的策略,包括: 线程封闭 只读共享.共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它.共享的只读对象包括不 ...

  3. 【Java并发基础】利用面向对象的思想写好并发程序

    前言 下面简单总结学习Java并发的笔记,关于如何利用面向对象思想写好并发程序的建议.面向对象的思想和并发编程属于两个领域,但是在Java中这两个领域却可以融合到一起.在Java语言中,面向对象编程的 ...

  4. 聊聊并发-Java中的Copy-On-Write容器

    详见: http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp78   聊聊并发-Java中的Copy-On-Write容器   Cop ...

  5. Java进阶7 并发优化2 并行程序设计模式

    Java进阶7 并发优化2 并行程序设计模式20131114 1.Master-worker模式 前面讲解了Future模式,并且使用了简单的FutureTask来实现并发中的Future模式.下面介 ...

  6. Java 进阶7 并发优化 1 并行程序的设计模式

       本章重点介绍的是基于 Java并行程序开发以及优化的方法,对于多核的 CPU,传统的串行程序已经很好的发回了 CPU性能,此时如果想进一步提高程序的性能,就应该使用多线程并行的方式挖掘 CPU的 ...

  7. 在并发Java应用程序中检测可见性错误

    了解什么是可见性错误,为什么会发生,以及如何在并发Java应用程序中查找难以捉摸的可见性错误.这些问题你可能也遇到过,当在优锐课学习了一段时间后,我对这些问题有了一定见解,写下这篇文章和大家分享. 检 ...

  8. java并发编程实战:第十二章---并发程序的测试

    并发程序中潜在错误的发生并不具有确定性,而是随机的. 安全性测试:通常会采用测试不变性条件的形式,即判断某个类的行为是否与其规范保持一致 活跃性测试:进展测试和无进展测试两方面,这些都是很难量化的(性 ...

  9. java并发编程(5)并发程序测试

    并发程序测试 一.正确性测试 如:对一个自定义缓存的测试 //自定义的缓存 public class SemaphoreBoundedBuffer <E> { private final ...

随机推荐

  1. linux 私房菜 CH5 笔记

    知识点 linux 大小写敏感 接口的切换 [Ctrl] + [Alt] + [F1] ~ [F6] :文字接口登入 tty1 ~ tty6 终端机: [Ctrl] + [Alt] + [F7] :图 ...

  2. JavaScript的基本规范

    1.不要在同一行声明多个变量: 2.请使用===/!==来比较true/false或者数值: 3.使用对象字面量替代new Array这种形式: 4.Switch语句必须带有default分支: 5. ...

  3. javascript继承---组合式继承

    原型链继承和构造函数继承中存在着很多的缺陷,所以,使用组合式继承了进行弥补这些缺陷 //组合继承 //父函数 function a(){ this.name = "兔子只吃胡萝卜" ...

  4. 嵌入式ARM开发环境搭建

    1. 安装,配置,启动FTP服务 安装FTP: sudo apt-get install vsftpd 修改vsftpd的配置文件/etc/vsftpd.conf,将下面两行的'#'去掉#local_ ...

  5. Linux环境Perl链接MS Sql Server数据库

    1.下载相关软件 unixODBC.freetds和DBD-ODBC ①.Linux系统的ODBC unixODBC-2.3.4.tar.gz ( http://www.unixodbc.org) ② ...

  6. ctrl+alt+F1~6进入不了字符界面,黑屏的解决办法

    ubuntu系统,我是ubuntu14.04 本来想装cuda,需要在字符界面下装,奈何按ctrl+alt+F1就黑屏了,按ctrl+alt+F7又可以正常回到图形界面,网上查了很多,有的方法也试过, ...

  7. Liniux系统下目录的权限意义

    访问者及其基本权限 Linux系统内的文件访问者有三种身份,分别是: a) 文件和文件目录的所有者: u---User(所有权);b) 文件和文件目录的所有者所在的组的用户: g---Group;c) ...

  8. 【Egret】Wing3发布移动APP功能,打包APK流程以及会遇到的问题

    流程:1.安装好wing 3,然后 插件--Egret项目工具--发布移动APP--Build android native project 2.配置发布环境: Android 1.egret eng ...

  9. 老李分享:Uber究竟是用什么开发语言? 2

    Uber的任务分派系统是运行在Node上,这是一个运行在服务器端的JavaScript平台.当一个客户打开app或者网站来进行车辆预定或者调用其他的API来查看可用车辆信息的时候,大部分的这些服务都是 ...

  10. [转]使用sklearn进行集成学习——理论

    转:http://www.cnblogs.com/jasonfreak/p/5657196.html 目录 1 前言2 集成学习是什么?3 偏差和方差 3.1 模型的偏差和方差是什么? 3.2 bag ...