概述

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. PHP SFTP应用(二)

    SFTP应用(二) SFTP Server SSH2 sftp操作 FTP进行文件传输需要通过端口进行.一般所需端口为: 控制链路—TCP端口21.控制器端.用于发送指令给服务器以及等待服务器响应. ...

  2. Centos下防止ssh暴力破解密码

    参考文章地址:https://yq.aliyun.com/ziliao/48446 https://www.cnblogs.com/lsdb/p/7095288.html 1.收集 /var/log/ ...

  3. 一、Linux中的常用命令2 二、Vim编辑器的使用

    一.Linux的常用命令###<1>文件目录操作 13. echo:用于输出字符串,shell编程,echo 1. 输出字符串 : echo str ,shell编程会使用(类似java中 ...

  4. 原生JS 将canvas生成图片

    核心代码: <script type="text/javascript"> // Converts image to canvas; returns new canva ...

  5. [咸恩静][Good Bye]

    歌词来源:http://music.163.com/#/song?id=35437298 作曲 : 安英民 [作曲 : 安英民] 作词 : 安英民/로코 [作词 : 安英民/lo-Ko] 나를 떠나버 ...

  6. 根据需求设计类并且画UML类图练习

    题目如下: // 打车时,可以打专车或者快车.任何车都有车牌号和名称// 不同车价格不同,快车每公里1元,专车每公里2元// 行程开始时,显示车辆信息// 行程结束时,显示打车金额(假定行程就5公里) ...

  7. 【洛谷】【动态规划/背包】P1417 烹调方案

    由于你的帮助,火星只遭受了最小的损失.但gw懒得重建家园了,就造了一艘飞船飞向遥远的earth星.不过飞船飞到一半,gw发现了一个很严重的问题:肚子饿了~ gw还是会做饭的,于是拿出了储藏的食物准备填 ...

  8. Day1 Mybatis初识(一)

    框架 将重复的,繁琐的代码实现封装,让程序员将更多的精力放在业务的理解和分析上. 框架的作用 提高开发效率 隐藏细节 三大框架SSH  -->   SSM 1)        表述层:   用户 ...

  9. c++——类和对象初探

    2.1 基本概念 1)类.对象.成员变量.成员函数 2)面向对象三大概念 封装.继承.多态 3)编程实践 类的定义和对象的定义,对象的使用 求圆形的面积 定义Teacher类,打印Teacher的信息 ...

  10. 浅谈基于FormsAuthentication的认证

    一般情况下,在我们做访问权限管理的时候,会把用户的正确登录后的基本信息保存在Session中,以后用户每次请求页面或接口数据的时候,拿到 Session中存储的用户基本信息,查看比较他有没有登录和能否 ...