并发,几乎总是离不开集合这类高级数据结构的支持。
如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。
总结:
其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下思考:
1. JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
2. JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
3. JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
4. JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock,我觉得有以下几点:因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
5. JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
6. 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据
HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。
HashTable是线程安全的,在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,只有一个线程独占。在多线程的环境下,它是安全的,但是效率低下的。
其实HashTable有很多的优化空间,锁住整个table这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table,这就是ConcurrentHashMap JDK1.7版本的核心思想
ConcurrentHashMap(JDK1.7)允许多个修改操作并发进行,其关键在于使用了锁分离技术。使用了多个锁来控制对hash表的不同部分进行的修改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hash table,有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。(可以看成是优化了Hashtable)、ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁,可简单理解成把一个大的HashTable分解成多个,形成了锁分离
ConcurrentHashMap (JDK1.8)摒弃了Segment的概念,直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本
在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成(ReentrantLock+Segment+HashEntry)
Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,和HashMap的数据存储结构一样
JDK1.8实现版本:ConcurrentHashMap的数据结构(数组+链表+红黑树),桶中的结构可能是链表,也可能是红黑树,红黑树是为了提高查找效率。(synchronized+CAS+HashEntry+红黑树)
ConcurrentHashMap是线程安全的哈希表,它是通过“锁分段”来保证线程安全的。ConcurrentHashMap将哈希表分成许多片段(Segment),每一个片段除了保存哈希表之外,本质上也是一个“可重入的互斥锁”(ReentrantLock)。多线程对同一个片段的访问,是互斥的;但是,对于不同片段的访问,却是可以同步进行的。
对于ConcurrentHashMap需要掌握以下几点
• Map的创建:ConcurrentHashMap()
• 往Map中添加键值对:即put(Object key, Object value)方法
• 获取Map中的单个对象:即get(Object key)方法
• 删除Map中的对象:即remove(Object key)方法
• 判断对象是否存在于Map中:containsKey(Object key)
• 遍历Map中的对象:即keySet().iterator(),在增强for
基于JDK1.8在HashMap和concurrentHashMap和以往都发生了变化。
介绍一下Java内存模型,volatile关键字和CAS算法。
技术点:
1、悲观锁与乐观锁:
悲观锁:指如果一个线程占用了一个锁,导致其他所有需要这个锁的线程进入等待,一直到该锁被释放,就是这个锁被独占,典型的就是synchronized;
乐观锁:指操作并不加锁,抱着尝试的态度去执行某项操作,如果操作失败或冲突,就进入重试,直到执行成功为止。
2、原子性,指令有序性和线程可见性: 都是多线程编程中是核心的问题
原子性和事务的原子性一样,对于一个操作或者多个操作,要么都执行,要么都不执行。
指令有序性,在编写的代码中,上下两个互不关联的语句不会被指令重排序。
指令重排序是指处理器为了性能优化,在无关联的代码的执行是可能会和代码顺序不一致。比如说int i = 1;int j = 2;那么这两条语句的执行顺序可能会先执行int j = 2;
线程可见性是指一个线程修改了某个变量,其他线程能马上知道。
3、无锁算法(nonblocking algorithms):
使用低层原子化的机器指令, 保证并发情况下数据的完整性。典型的如CAS算法。
4、内存屏障:
确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;它会强制将对缓存的修改操作立即写入主存;如果是写操作,它会导致其他CPU中对应的缓存行无效。在使用volatile修饰的变量会产生内存屏障(后面会详细解释)。
①解释:从图中可以看出每个线程都需要从主内存中读取操作,java内存模型的规定之一,所有的变量存储在主内存中,每个线程都需要从主内存中获得变量的值。
然后从图中可以看到每个线程获得数据之后会放入自己的工作内存,这个就是java内存模型的规定之二,保证每个线程操作的都是从主内存拷贝的副本,也就是说线程不能直接写主内存的变量,需要把主内存的变量值读取之后放入自己的工作内存中的变量副本中,然后操作这个副本。
最后线程与线程之间无法直接访问对方工作内存中的变量。最后需要解释一下这个访问规则局限于对象实例字段,静态字段等,局部变量不包括在内,因为局部变量不存在竞争问题。
②基本执行步骤:
a、lock(锁定):在某一个线程在读取主内存的时候需要把变量锁定。
b、unlock(解锁):某一个线程读取玩变量值之后会释放锁定,别的线程就可以进入操作
c、read(读取):从主内存中读取变量的值并放入工作内存中
d、load(加载):从read操作得到的值放入工作内存变量副本中
e、use(使用):把工作内存中的一个变量值传递给执行引擎
f、assign(赋值):它把一个从执行引擎接收到的值赋值给工作内存的变量
g、store(存储):把工作内存中的一个变量的值传送到主内存中
h、write(写入):把store操作从工作内存中一个变量的值传送到主内存的变量中。
这里我再引入一张别的地方被我搜来的图供大家一起理解:
在concurrentHashMap之中,有很多的成员变量都是用volatile修饰的。
被volatile修饰的变量有如下特性:
①使得变量更新变得具有可见性,只要被volatile修饰的变量的赋值一旦变化就会通知到其他线程,如果其他线程的工作内存中存在这个同一个变量拷贝副本,那么其他线程会放弃这个副本中变量的值,重新去主内存中获取
②产生了内存屏障,防止指令进行了重排序,关于这点的解释,请看下面一段代码:
public classVolatileTest {
int a = 0; //1
int b = 1; //2
volatile int c = 2; //3
int d = 3; //4
int e = 4; //5
}
在如上的代码中,因为c变量是用volatile进行修饰,那么就会对该段代码产生一个内存屏障,用以保证在执行语句3的时候语句1和语句2是绝对执行完毕的,而且在执行语句3的时候,语句4和语句5肯定没有执行。同时说明一下,在上述代码中虽然保证了语句3的执行顺序不可变换,但是语句1和语句2,语句4和语句5可能发生指令重排序哦。
总结:volatile修饰的变量具有可见性与有序性。
下面,我们用一段代码来进一步解释volatile和原子性的概念:
public classVolatileTest {
// int a = 0; //1
// int b = 1; //2
public static volatile int c = 0; //3
// int d = 3; //4
// int e = 4; //5
public static void increase(){
c++;
}
public static void main(String[] args)throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
public void run() {
increase();
}
}
).start();
}
Thread.sleep(5000);
System.out.println(c);
}
}
//运行3次结果分别是:997,995,989
典型的volatile操作与原子性的概念。执行结果是小于等于1000的,为什么会这样呢?不是说volatile修饰的变量是具有原子性的么?是的,volatile修饰的变量的确具有原子性,也就是c是具有原子性的(直接赋值是原子性的),但是c++不具有原子性,c++其实就是c = c +1,已经存在了多步操作。所以c具有原子性,但是c++这个操作不具有原子性。
根据前面介绍的java内存模型,当有一个线程去读取主内存的过程中获取c的值,并拷贝一份放入自己的工作内存中,在对c进行+1操作的时候线程阻塞了(各种阻塞情况),那么这个时候有别的线程进入读取c的值,因为有一个线程阻塞就导致该线程无法体现出可见性,导致别的线程的工作内存不会失效,那么它还是从主内存中读取c的值,也会正常的+1操作。如此便导致了结果是小于等于1000的。
注意,这里笔者也有个没有深刻理解的问题,首先在java内存模型中规定了:在对主内存的unlock操作之前必须要执行write操作,那意思就是c在写回之前别的线程是无法读取c的。然而结果却并非如此。
CAS算法
CAS(Compare And Swap)比较与交换,主要操作思想是:
首先它具有三个操作数,a、内存位置V,预期值A和新值B。如果在执行过程中,发现内存中的值V与预期值A相匹配,那么会将V更新为新值A。如果不相匹配,那么处理器就不会执行任何操作。CAS算法就是 “无锁定算法”,因为线程不必再等待锁定,只要执行CAS操作就可以,会在预期中完成。
在JDK1.8的ConcurrentHashMap中,很多的操作都会依靠CAS算法完成。
如何实现线程安全:
ConcurrentHashMap核心是线程安全的,用什么来实现线程安全的呢?
在jdk1.8中主要采用了CAS算法实现线程安全的。通过CAS算法又实现了3种原子操作(线程安全的保障就是操作具有原子性)
在以前的ConcurrentHashMap中是锁定了Segment,而在jdk1.8被移除,现在锁定的是一个Node头节点(注意,synchronized锁定的是头结点),减小了锁的粒度,性能和冲突都会减少,
如何存储数据:
jdk1.8中取消了segment,所以结构其实和HashMap是极其相似的,在HashMap的基础上实现了线程安全,同时在每一个“桶”中的节点会被锁定。
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable {
重要的成员变量:
1、capacity:容量,目前map的存储大小,默认是在没有指定容量大小的时候会赋予这个值
private static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量,容量不能超过这个值 private static final int DEFAULT_CAPACITY = 16; // 默认容量
2、laodfactor:加载因子,和HashMap是一样的,默认值也是0.75f。
private static final float LOAD_FACTOR = 0.75f;
3、TREEIFY_THRESHOLD与UNTREEIFY_THRESHOLD:作为了解,这个两个主要是控制链表和红黑树转化的
static final int TREEIFY_THRESHOLD = 8; // 大于这个值,需要把链表转换为红黑树
static final int UNTREEIFY_THRESHOLD = 6; // 红黑树节点小徐这个值,退化为链表
4、在扩容和参与扩容(当线程进入put的时候,发现该map正在扩容,那么它会协助扩容)的时候使用
private static int RESIZE_STAMP_BITS = 16;
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
5、线程判断map当前处于什么阶段。
static final int MOVED = -1; // hash for forwarding nodes 该节点是个forwarding Node,有线程处理过了 static final int TREEBIN = -2; // hash for roots of trees 这个节点是树节点 static final int RESERVED = -3; // hash for transient reservations
6、sizeCtl,标志控制符。
private transientvolatile int sizeCtl; //出现在ConcurrentHashMap的各个阶段,不同的值也表示不同情况和不同功能
①负数代表正在进行初始化或扩容操作
②-N 表示有N-1个线程正在进行扩容操作 (当线程进行值添加的时候判断到正在扩容,它就会协助扩容)
③正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,类似于扩容阈值。它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。实际容量>=sizeCtl,则扩容。
注意:在某些情况下,这个值就相当于HashMap中的threshold阀值。用于控制扩容。
极其重要的几个内部类:
1. Node类:主要用于存储具体键值对,其子类有ForwardingNode、ReservationNode、TreeNode和TreeBin四个子类。
2. Traverser类:主要用于遍历操作,其子类有BaseIterator、KeySpliterator、ValueSpliterator、EntrySpliterator四个类,BaseIterator用于遍历操作。KeySplitertor、ValueSpliterator、EntrySpliterator则用于键、值、键值对的划分。
3. CollectionView抽象类:主要定义了视图操作,其子类KeySetView、ValueSetView、EntrySetView分别表示键视图、值视图、键值对视图。对视图均可以进行操作。
4. Segment类:在JDK1.8中与之前的版本的JDK作用存在很大的差别,JDK1.8下,其在普通的ConcurrentHashMap操作中已经没有失效,其在序列化与反序列化的时候会发挥作用。
5. CounterCell类:主要用于对baseCount的计数。
1、Node
Node数据结构很简单,与HashMap中的定义相似,但是有一些差别它对value和next属性设置了volatile同步锁,它不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; Node(int hash, K key, V val, Node<K,V> next) { this.hash = hash; this.key = key; this.val = val; this.next = next; } public final K getKey() { return key; } public final V getValue() { return val; } public final int hashCode() { return key.hashCode() ^ val.hashCode(); } public final String toString(){ return key + "=" + val; } public final V setValue(V value) { // 不可set throw new UnsupportedOperationException(); } public final boolean equals(Object o) { Object k, v, u; Map.Entry<?,?> e; return ((o instanceof Map.Entry) && (k = (e = (Map.Entry<?,?>)o).getKey()) != null && (v = e.getValue()) != null && (k == key || k.equals(key)) && (v == (u = val) || v.equals(u))); } /** * Virtualized support for map.get(); overridden in subclasses. */ Node<K,V> find(int h, Object k) { Node<K,V> e = this; if (k != null) { do { K ek; if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; } while ((e = e.next) != null); } return null; } }
value 和 next是用volatile修饰的,使得value和next具有可见性和有序性,从而保证线程安全。
setValue()方法访问是会抛出异常,是禁止用该方法直接设置value值的。同时它还错了一个find的方法,该方法主要是用户寻找某一个节点。
2、TreeNode和TreeBin
TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构,树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap集成自Node类,而并非HashMap中的集成自LinkedHashMap.Entry<K,V>类,也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问
TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制。这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。
static final class TreeNode<K,V> extends Node<K,V> { TreeNode<K,V> parent; // red-black tree links TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; // needed to unlink next upon deletion boolean red; TreeNode(int hash, K key, V val, Node<K,V> next, TreeNode<K,V> parent) { super(hash, key, val, next); this.parent = parent; } Node<K,V> find(int h, Object k) { return findTreeNode(h, k, null); } /** * Returns the TreeNode (or null if not found) for the given key * starting at given root. */ final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) { if (k != null) { TreeNode<K,V> p = this; do { int ph, dir; K pk; TreeNode<K,V> q; TreeNode<K,V> pl = p.left, pr = p.right; if ((ph = p.hash) > h) p = pl; else if (ph < h) p = pr; else if ((pk = p.key) == k || (pk != null && k.equals(pk))) return p; else if (pl == null) p = pr; else if (pr == null) p = pl; else if ((kc != null || (kc = comparableClassFor(k)) != null) && (dir = compareComparables(kc, k, pk)) != 0) p = (dir < 0) ? pl : pr; else if ((q = pr.findTreeNode(h, k, kc)) != null) return q; else p = pl; } while (p != null); } return null; } }
Treebin: static final class TreeBin<K,V> extends Node<K,V> { TreeNode<K,V> root; volatile TreeNode<K,V> first; volatile Thread waiter; volatile int lockState; // 读写锁状态
static final int WRITER = 1; // 获取写锁的状态 static final int WAITER = 2; // 等待写锁的状态 static final int READER = 4; // 增加数据时读锁的状态 static int tieBreakOrder(Object a, Object b) {…. } TreeBin(TreeNode<K,V> b) { super(TREEBIN, null, null, null); this.first = b; TreeNode<K,V> r = null; for (TreeNode<K,V> x = b, next; x != null; x = next) { next = (TreeNode<K,V>)x.next; x.left = x.right = null; if (r == null) { x.parent = null; x.red = false; r = x; } else { K k = x.key; int h = x.hash; Class<?> kc = null; for (TreeNode<K,V> p = r;;) { int dir, ph; K pk = p.key; if ((ph = p.hash) > h) dir = -1; else if (ph < h) dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) dir = tieBreakOrder(k, pk); TreeNode<K,V> xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { x.parent = xp; if (dir <= 0) xp.left = x; else xp.right = x; r = balanceInsertion(r, x); break; } } } } this.root = r; assert checkInvariants(root); } private final void lockRoot() {.. } private final void unlockRoot() {..} private final void contendedLock() {…. } final Node<K,V> find(int h, Object k) {…} final TreeNode<K,V> putTreeVal(int h, K k, V v) {…} final boolean removeTreeNode(TreeNode<K,V> p) {.. } static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) { } static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) {..} static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {…} static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) {… } static <K,V> boolean checkInvariants(TreeNode<K,V> t) {…. } private static final sun.misc.Unsafe U; private static final long LOCKSTATE; static { try { U = sun.misc.Unsafe.getUnsafe(); Class<?> k = TreeBin.class; LOCKSTATE = U.objectFieldOffset (k.getDeclaredField("lockState")); } catch (Exception e) { throw new Error(e); } } } ConcurrentHashMap中不是直接存储TreeNode来实现的,而是用TreeBin来包装TreeNode来实现的。也就是说在实际的ConcurrentHashMap桶中,存放的是TreeBin对象,而不是TreeNode对象。之所以TreeNode继承自Node是为了附带next指针,而这个next指针可以在TreeBin中寻找下一个TreeNode,这里也是与HashMap之间比较大的区别。
3、ForwordingNode
一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
Node<K,V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwardingnodes
outer: for (Node<K,V>[] tab = nextTable;;) {
Node<K,V> e; int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (;;) {
int eh; K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null &&k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode){
tab =((ForwardingNode<K,V>)e).nextTable;
continue outer;
}
else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}
这个静态内部内就显得独具匠心,它的使用主要是在扩容阶段,它是链接两个table的节点类,有一个next属性用于指向下一个table,注意要理解这个table,它并不是说有2个table,而是在扩容的时候当线程读取到这个地方发现这个地方为空,这会设置为forwordingNode,或者线程处理完该节点也会设置该节点为forwordingNode,别的线程发现这个forwordingNode会继续向后执行遍历,这样一来就很好的解决了多线程安全的问题。这里有小伙伴就会问,那一个线程开始处理这个节点还没处理完,别的线程进来怎么办,而且这个节点还不是forwordingNode呐?说明你前面没看详细,在处理某个节点(桶里面第一个节点)的时候会对该节点上锁,上面文章中我已经说过了。
其他属性:
// Unsafe mechanics CAS保障了哪些成员变量操作是原子性的
private static final sun.misc.Unsafe U;// 主要用于反射获取对象相应的字段
private static final long SIZECTL;
private static final long TRANSFERINDEX;
private static final long BASECOUNT;
private static final long CELLSBUSY;
private static final long CELLVALUE;
private static final long ABASE;
private static final int ASHIFT;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = ConcurrentHashMap.class;
SIZECTL = U.objectFieldOffset
(k.getDeclaredField("sizeCtl"));
TRANSFERINDEX = U.objectFieldOffset
(k.getDeclaredField("transferIndex"));
BASECOUNT = U.objectFieldOffset
(k.getDeclaredField("baseCount"));
CELLSBUSY = U.objectFieldOffset
(k.getDeclaredField("cellsBusy"));
Class<?> ck = CounterCell.class;
CELLVALUE = U.objectFieldOffset
(ck.getDeclaredField("value"));
Class<?> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak);
int scale = U.arrayIndexScale(ak);
if ((scale & (scale - 1)) != 0)
throw new Error("data typescale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
} catch (Exception e) {
throw new Error(e);
}
}
transient volatile Node<K,V>[] table; // 表
private transient volatile Node<K,V>[] nextTable; // 下一个表
private transient volatile long baseCount; // 基本计数
private transient volatile int transferIndex; / 扩容下另一个表的索引
private transient volatile int cellsBusy; // 旋转锁
private transient volatile CounterCell[] counterCells; // counterCell表
// views// 视图
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
private static final long serialVersionUID = 7249069246763182397L;
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 最大数组大小
private static final int DEFAULT_CONCURRENCY_LEVEL = 16; // 默认并发数
static final int MIN_TREEIFY_CAPACITY = 64; // 转化为红黑树的表的最小容量
private static final int MIN_TRANSFER_STRIDE = 16; //每次进行转移的最小值
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
/** 获取可用的CPU个数Number of CPUS, to place bounds on some sizings */
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**进行序列化的属性 For serialization compatibility. */
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("segments", Segment[].class),
new ObjectStreamField("segmentMask", Integer.TYPE),
new ObjectStreamField("segmentShift", Integer.TYPE)
};
在ConcurrentHashMap中,随处可以看到U, 大量使用了U.compareAndSwapXXX的方法,这个方法是利用一个CAS算法实现无锁化的修改值的操作,可以大大降低锁代理的性能消耗。
这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。这一点与乐观锁,SVN的思想是比较类似的。
unsafe静态块
unsafe代码块控制了一些属性的修改工作,比如最常用的SIZECTL 。 在这一版本的concurrentHashMap中,大量应用来的CAS方法进行变量、属性的修改工作。 利用CAS进行无锁操作,可以大大提高性能。
构造方法:
public ConcurrentHashMap() {/ 创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射
}// 创建一个带有指定初始容量、默认加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射
public ConcurrentHashMap(int initialCapacity) { // 初始化容器大小
if (initialCapacity < 0) throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY : tableSizeFor(initialCapacity +(initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}// 构造一个与给定映射具有相同映射关系的新映射。
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m); // 全都存入
}// 创建一个带有指定初始容量、加载因子和默认 concurrencyLevel (16) 的新的空映射
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}// 创建一个带有指定初始容量、加载因子和并发级别的新的空映射
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) ||initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimatedthreads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
private final Node<K,V>[] initTable() { // 初始化
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt =(Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
PUT方法
①先传入一个k和v的键值对,不可为空(HashMap是可以为空的),如果为空就直接报错。
②接着去判断table是否为空,如果为空就进入初始化阶段。
③如果判断数组中某个指定的桶是空的,那就直接把键值对插入到这个桶中作为头节点,而且这个操作不用加锁。
④如果这个要插入的桶中的hash值为-1,也就是MOVED状态(也就是这个节点是forwordingNode),那就是说明有线程正在进行扩容操作,那么当前线程就进入协助扩容阶段。
⑤需要把数据插入到链表或者树中,如果这个节点是一个链表节点,那么就遍历这个链表,如果发现有相同的key值就更新value值,如果遍历完了都没有发现相同的key值,就需要在链表的尾部插入该数据。插入结束之后判断该链表节点个数是否大于8,如果大于就需要把链表转化为红黑树存储。
⑥如果这个节点是一个红黑树节点,那就需要按照树的插入规则进行插入。
⑦put结束之后,需要给map已存储的数量+1,在addCount方法中判断是否需要扩容
描述2:
① 判断存储的key、value是否为空,若为空,则抛出异常,否则,进入步骤②
② 计算key的hash值,随后进入无限循环,该无限循环可以确保成功插入数据,若table表为空或者长度为0,则初始化table表,否则,进入步骤③
③ 根据key的hash值取出table表中的结点元素,若取出的结点为空(该桶为空),则使用CAS将key、value、hash值生成的结点放入桶中。否则,进入步骤④
④ 若该结点的的hash值为MOVED,则对该桶中的结点进行转移,即该线程帮助其进行扩容。否则,进入步骤⑤
⑤ 对桶中的第一个结点(即table表中的结点)进行加锁,对该桶进行遍历,桶中的结点的hash值与key值与给定的hash值和key值相等,则根据标识选择是否进行更新操作(用给定的value值替换该结点的value值),若遍历完桶仍没有找到hash值与key值和指定的hash值与key值相等的结点,则直接新生一个结点并赋值为之前最后一个结点的下一个结点。进入步骤⑥
⑥ 若binCount值达到红黑树转化的阈值,则将桶中的结构转化为红黑树存储,最后,增加binCount的值。
public V put(K key, V value) { return putVal(key, value, false); }
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException();//key和value不可空,为空直接抛出错误 int hash = spread(key.hashCode());//计算Hash值,确定数组下标,和HashMap是一样的 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(); //初始化
//CAS如果查询数组的某个桶是空的,就直接插入该桶中 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 这个时候插入不用加锁 }
//如果在插入的时候,节点是一个forwordingNode状态,表示正在扩容,那么当前线程进行帮助扩容 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null;
//锁定头节点 synchronized (f) { if (tabAt(tab, i) == f) {//确定这个节点的确是数组中的这个头结点
if (fh >= 0) { binCount = 1;
//遍历这个链表 for (Node<K,V> e = f;; ++binCount) { K ek;
//如果遍历到一个值,这个值和当前的key是相同的,那就更改value值 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e;
//如果遍历到结束都没有遇到相同的key,且后面没有节点了,那就直接在尾部插入一个 if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } }
//如果是红黑树存储就需要用红黑树的专门处理了 else if (f instanceof TreeBin) { 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; } } } }
//判断节点数量是否大于8,如果大于就需要把链表转化成红黑树 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } }
//map已存储的数量+1 addCount(1L, binCount); return null; }
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); }
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) { Node<K,V>[] nextTab; int sc; if (tab != null && (f instanceof ForwardingNode) && (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { int rs = resizeStamp(tab.length); while (nextTab == nextTable && table == tab && (sc = sizeCtl) < 0) { if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || transferIndex <= 0) break; if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) { transfer(tab, nextTab); break; } } return nextTab; } return table; }
public void putAll(Map<? extends K, ? extends V> m) {
tryPresize(m.size());
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
putVal(e.getKey(), e.getValue(), false);
}
transfer在PUT方法中都没出现过,只有一个helpTransfer(协助扩容)方法呢?
transfer方法放在了addCount方法中,下面是addCount方法的源码:
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a =as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}//是否需要进行扩容操作
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) { //如果小于0就说明已经再扩容或者已经在初始
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) //如果是正在扩容就协助扩容
transfer(tab, nt);
} //如果正在初始化就首次发起扩容
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
GET方法
主要步骤是:
①先判断数组的桶中的第一个节点是否寻找的对象是为链表还是红黑树,
②如果是红黑树另外做处理
③如果是链表就先判断头节点是否为要查找的节点,如果不是那么就遍历这个链表查询
④如果都不是,那就返回null值。
public V get(Object key) {
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) {
//先判断头节点,如果头节点的hash值与入参key的hash值相同
if ((eh = e.hash) == h) {
//头节点的key就是传入的key
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}//eh<0表示这个节点是红黑树
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null; //直接从树上进行查找返回结果,不存在就返回null
while ((e = e.next) != null) {//如果首节点不是查找对象且不是红黑树结构,那边就遍历这个列表
if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null; //都没有找到就直接返回null值
}
如在扩容的过程中,把一个链表拆分为两个链表到底是一个怎么样的过程。
在将链表结构或者树结构转化为链表的过程是,利用JDK8新引入的hash高位运算确定元素在数组的位置,取代了原来的取模运算,所以利用这个hash值在第n位的bit值,为0就放在lo,为1就放在hi,而ConcurrentHashMap在拆分链表的时候,hash值在n位为0的链表顺序与原来的顺序与相反的(头插法,最开始指针为null,所以顺序会变),而为1的链表顺序还是与原链表顺序一致(头插法,最开始已经有指针指向最后一个属于这个链表的节点了,所以顺序才不会变)。而HashMap的扩容的时候,两个链表的顺序还是和原来的链表顺序相同。
transfer方法(扩容方法)
扩容的过程:首先有且只能由一个线程构建一个nextTable,这个nextTable主要是扩容后的数组(容量已经扩大),然后把原table复制到nextTable中,这个过程可以多线程共同操作。但是一定要清楚,这个复制并不是简单的把原table的数据直接移动到nextTable中,而是需要有一定的规律和算法操控的(不然怎么把树转化为链表呢)。
再这之前,先简单说下复制的过程:
数组中(桶中)总共分为3种存储情况:空,链表头,TreeBin头
①遍历原来的数组(原table),如果数组中某个值为空,则直接放置一个forwordingNode。
②如果数组中某个值不为空,而是一个链表头结点,那么就对这个链表进行拆分为两个链表,存储到nextTable对应的两个位置。
③如果数组中某个值不为空,而是一个TreeBin头结点,那么这个地方就存储的是红黑树的结构,这样一来,处理就会变得相对比较复杂,就需要先判断需不需要把树转换为链表,做完一系列的处理,然后把对应的结果存储在nextTable的对应两个位置。
多个线程进行扩容操作的时候,会判断原table的值,如果这个值是forwordingNode就表示这个节点被处理过了,就直接继续往下找
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//判断CPU处理的量,如果小于16则直接赋值16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { / initiating只能有一个线程进行构造nextTable,如果别的线程进入发现不为空就不用构造nextTable了
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt =(Node<K,V>[])new Node<?,?>[n << 1]; //把新的数组变为原来的两倍,这里的n<<1就是向左移动一位,也就是乘以二。
nextTab = nt;
} catch (Throwable ex){ // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n; //原先扩容大小
}
int nextn = nextTab.length;
//构造一个ForwardingNode用于多线程之间的共同扩容情况
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true; //遍历的确认标志
boolean finishing = false; // to ensure sweepbefore committing nextTab
//遍历每个节点
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh; //定义一个节点和一个节点状态判断标志fh
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
} else if (U.compareAndSwapInt(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?nextIndex - stride : 0))) {//CAS计算
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//如果原table已经复制结束
if (finishing) {
nextTable = null; //可以看出在扩容的时候nextTable只是类似于一个temp用完会丢掉
table = nextTab;
sizeCtl = (n << 1) - (n >>>1); //修改扩容后的阀值,应该是现在容量的0.75倍
return;
}
//采用CAS算法更新SizeCtl
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n)<< RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; //recheck before commit
}
}//CAS算法获取某一个数组的节点,为空就设为forwordingNode
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED) //如果这个节点的hash值是MOVED,就表示这个节点是forwordingNode节点,就表示这个节点已经被处理过了,直接跳过
advance = true; // already processed
else {
synchronized (f) { //对头节点进行加锁,禁止别的线程进入
if (tabAt(tab, i) == f) {//CAS校验这个节点是否在table对应的i处
Node<K,V> ln, hn;
//如果这个节点的确是链表节点.把链表拆分成两个小列表并存储到nextTable对应的两个位置
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln =lastRun;
hn = null;
} else {
hn =lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln); //CAS存储在nextTable的i位置上
setTabAt(nextTab, i + n, hn); //CAS存储在nextTable的i+n位置上
setTabAt(tab, i, fwd); //CAS在原table的i处设置forwordingNode节点,表示这个这个节点已经处理完毕
advance = true;
}//如果这个节点是红黑树
else if (f instanceof TreeBin) {
TreeBin<K,V> t =(TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}//如果拆分后的树的节点数量已经少于6个就需要重新转化为链表
ln = (lc<= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln); //CAS存储在nextTable的i位置上
setTabAt(nextTab, i + n, hn); //CAS存储在nextTable的i+n位置上
setTabAt(tab, i, fwd); //CAS在原table的i处设置forwordingNode节点,表示这个这个节点已经处理完
advance = true;
}
}
}
}
}
}