ConcurrentHashMap
散列,Hash:把任意长度的输入通过一种算法(散列),变换成固定长度的输出,这个输出值就是散列值,属于压缩映射,容易产生哈希冲突
HashMap在多线程put操作时会引起死循环,hashmap里的entry链表产生环形数据结构。关于环形链表的形成,则主要在这扩容的过程。当多个线程同时对这个HashMap进行put操作,而察觉到内存容量不够,需要进行扩容时,多个线程会同时执行resize操作,而这就出现问题了,问题的原因分析如下:
在HashMap扩容时,会改变链表中的元素的顺序,将元素从链表头部插入,而环形链表就在这一时刻发生,以下模拟2个线程同时扩容
- 线程一:读取到当前的hashmap情况,在准备扩容时,线程二介入
- 线程二:读取hashmap,进行扩容
- 线程一:继续执行,先将A复制到新的hash表中,然后接着复制B到链头(A的前边:B.next=A),本来B.next=null,到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将B.next=A,所以,这里继续复制A,让A.next=B,由此,环形链表出现:B.next=A; A.next=B
【位运算】知识补充:
public class Permission {
// 是否允许查询,二进制第1位,0表示否,1表示是
public static final int ALLOW_SELECT = 1 << 0; // 0001 = 1
// 是否允许新增,二进制第2位,0表示否,1表示是
public static final int ALLOW_INSERT = 1 << 1; // 0010 = 2
// 是否允许修改,二进制第3位,0表示否,1表示是
public static final int ALLOW_UPDATE = 1 << 2; // 0100 =4
// 是否允许删除,二进制第4位,0表示否,1表示是
public static final int ALLOW_DELETE = 1 << 3; // 1000 = 8
// 存储目前的权限状态
private int flag;
//设置用户的权限
public void setPer(int per) {
flag = per;
}
//增加用户的权限(1个或者多个)
public void enable(int per) {
flag = flag|per;
}
//删除用户的权限(1个或者多个)
public void disable(int per) {
flag = flag&~per;
}
//判断用户的权限
public boolean isAllow(int per) {
return ((flag&per)== per);
}
//判断用户没有的权限
public boolean isNotAllow(int per) {
return ((flag&per)==0);
}
public static void main(String[] args) {
int flag = 15;
Permission permission = new Permission();
permission.setPer(flag);
permission.disable(ALLOW_DELETE|ALLOW_INSERT);
System.out.println("select = "+permission.isAllow(ALLOW_SELECT));
System.out.println("update = "+permission.isAllow(ALLOW_UPDATE));
System.out.println("insert = "+permission.isAllow(ALLOW_INSERT));
System.out.println("delete = "+permission.isAllow(ALLOW_DELETE));
}
}
【执行结果】:
select = true
update = true
insert = false
delete = false
基本成员变量
/** node数组的最大容量 2^30 */
private static final int MAXIMUM_CAPACITY = 1 << 30;
/** 默认初始化值16,必须是2的冥 */
private static final int DEFAULT_CAPACITY = 16;
/** 虚拟机限制的最大数组长度,在ArrayList中有说过,jdk1.8新引入的,需要与toArrar()相关方法关联 */
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/** 负载因子,兼容以前版本,构造方法中指定的参数是不会被用作loadFactor的,为了计算方便,统一使用 n - (n >> 2) 代替浮点乘法 *0.75 */
private static final float LOAD_FACTOR = 0.75f;
/** 链表转红黑树,阈值>=8 */
static final int TREEIFY_THRESHOLD = 8;
/** 树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,
* <=UNTREEIFY_THRESHOLD 则untreeify(lo))
*/
static final int UNTREEIFY_THRESHOLD = 6;
/** 链表转红黑树的阈值,64(map容量小于64时,链表转红黑树时先进行扩容) */
static final int MIN_TREEIFY_CAPACITY = 64;
/** 下面这三个和多线程协助扩容有关 */
/** // 扩容操作中,transfer这个步骤是允许多线程的,这个常量表示一个线程执行transfer时,最少要对连续的16个hash桶进行transfer
// (不足16就按16算,多控制下正负号就行)
private static final int MIN_TRANSFER_STRIDE = 16;
/** 生成sizeCtl所使用的bit位数 */
private static int RESIZE_STAMP_BITS = 16;
/** 参与扩容的最大线程数 */
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/** 移位量,把生成戳移位后保存在sizeCtl中当做扩容线程计数的基数,相反方向移位后能够反解出生成
戳 */
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/*
* Encodings for Node hash fields. See above for explanation.
*/
static final int MOVED = -1; // 表示正在转移
static final int TREEBIN = -2; // 表示已经转换为树
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
/** 可用处理器数量 */
static final int NCPU = Runtime.getRuntime().availableProcessors();
/** 用于存放node数组 */
transient volatile Node<K,V>[] table;
/**
* baseCount为并发低时,直接使用cas设置成功的值
* 并发高,cas竞争失败,把值放在counterCells数组里面的counterCell里面
* 所以map.size = baseCount + (每个counterCell里面的值累加)
*/
private transient volatile long baseCount;
/**
* 控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
* 当为负数时:-1代表正在初始化,-N就代表在扩容,-N-RS-2就代表有多少个线程在协助扩容
* 当为0时:代表当时的table还没有被初始化
* 当为正数时:表示初始化或者下一次进行扩容的大小
*/
private transient volatile int sizeCtl;
/**
* 通过cas实现的锁,0 无锁,1 有锁
*/
private transient volatile int cellsBusy;
/**
* counterCells数组,具体的值在每个counterCell里面
*/
private transient volatile CounterCell[] counterCells;
基本操作方法
- 构造方法
/**
* 指定初始化大小的构造,不能小于0
* @param initialCapacity
*/
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
// cap必须是2的n次方,
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
- 初始化方法
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { // 空的table 才能初始化
if ((sc = sizeCtl) < 0) // 表示其它线程正在初始化或者扩容
// 当前线程把执行权交给其它线程(拥有相同优先级的线程),然后变成可运行状态
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 原子操作表示把SIZECTL设置为-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); // 下一次扩容阈值 n*0,75
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
put
方法
- 我们可以通过源码判断
key
和value
不允许为null。 - 需要判断table有没有初始化,没有调用initTable初始化,然后接着循环。
- 判断key的hash(调用spread方法)的位置有没有值,证明是第一个,使用cas设置,为什么cas,可能不止一个线程。
- 判断当前线程的hash是不是MOVED,其实就是节点是不是ForwardingNode节点,ForwardingNode代表正在扩容,至于为什么会是ForwardingNode,这个在扩容的方法里面再讲,如果是ForwardingNode节点就协助扩容,也就是当前也去扩容,然后扩容完毕,在执行循环,协助扩容执行helpTransfer方法。
- 如果不是扩容、table也初始化了和hash位置也有值了,那证明当前hash的位置是链表或者树,接下来锁住这个节点,进行链表或者树的节点的追加,如果存在相同的key,就替换,最后释放锁。
- 判断链表的节点数,有没有大于等于8,满足就树化,调用treeifyBin方法,这个方法会在树化前判断大于等于64吗,没有就扩容,调用tryPresize方法,有就树化。
- 修改节点的数量,调用
addCount
方法。
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) {
// 不允许key或者value 为null
if (key == null || value == null) throw new NullPointerException();
// 获取hash
int hash = spread(key.hashCode());
int binCount = 0;
// 遍历table,死循环,直到插入成功
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0) // table 还没有初始化
tab = initTable(); // 初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 当前位置为空 ,直接插入
if (casTabAt(tab, i, null, // 使用cas来进行设置
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) // 如果在进行扩容,则先进行协助扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 没有在扩容,头结点也不是空,
// 锁住链表或者树的头节点
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) { // 普通Node的hash值为key的hash值大于零,而ForwardingNode的是-1,TreeBin是-2
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
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value; // 替换value 结束循环
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) { // 找到最后一个节点
pred.next = new Node<K,V>(hash, key,
value, null); // 把当前节点设置为最后一个节点的next
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) // 如果链表大于等于8,树化
treeifyBin(tab, i); // 树化
if (oldVal != null) // 证明存在相同的key,是替换return旧值
return oldVal;
break;
}
}
}
addCount(1L, binCount); //数量加1
return null;
}
get
方法
// get方法
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 获取hash值
int h = spread(key.hashCode());
// table不为null,table已经初始化,通过hash查找的node不为nul
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) { // hash相等
if ((ek = e.key) == key || (ek != null && key.equals(ek))) // 找到了相同的key
return e.val; // 返回当前e的value
}
else if (eh < 0) // hash小于0,说明是特殊节点(TreeBin或ForwardingNode)调用find
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;
}
transfer
扩容方法
- 看
stride
这个参数其实就是算每个线程处理的数量,和CPU有关,最小是16. - 初始化一个原来二倍的新table就是
nextTable
,然后这个过程可能会出错,n<<1可能为负数,设置nextTable和transferIndex,其中transferIndex就是原table的长度。 - 初始化一个
ForwardingNode
节点在后面会用到。 - 死循环for,这个循环就是为每个线程分配任务,然后每个线程处理各自的任务,倒叙分配,举个例子,加入table.length=32,现在的stride为16,第一个线程其实就是32到16(不包含32,因为是索引),第二个线程就是0-15,参考这一段代码((U.compareAndSwapInt (this, TRANSFERINDEX, nextIndex,nextBound = (nextIndex > stride ?nextIndex - stride : 0)))),然后遍历每个段,处理节点,知道处理完成,具体逻辑参考代码注释。
/**
* 扩容方法
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// n >>> 3(也就是除以8) / cpu个数,每个cpu的每个线程负责的迁移的数量
// 这样的目的是为了每个cpu处理的桶一样多,避免出现任务转移不均匀的现象,如果桶少的话,默认一个cpu(一个线程)处理16个桶
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 扩容table 没有初始化
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 初始化原来的length两倍的table
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
// 初始化失败,使用integer的最大值
sizeCtl = Integer.MAX_VALUE;
return; // 结束
}
// 更新成员变量
nextTable = nextTab;
// 更新转移下标,就是运来的table的length
transferIndex = n;
}
// 新table的length
int nextn = nextTab.length;
// 创建一个fwd节点,用于占位.当别的节点发现这个槽位中有fwd节点时,则跳过这个节点
// 它的hash为MOVED
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 首次推进为true,如果为true说明需要再次推进一个目标(i--),反之如果是false,那么就不能推进下标,需要将当前的下标处理完毕
boolean advance = true;
// 完成状态,如果为true,就结束方法
boolean finishing = false; // to ensure sweep before committing nextTab
// 死循环,因为是倒着遍历,所以i是点前线程的最大位置(i---),bound是边界,也就是区间里面的最小值
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 如果当前线程可以向后推进,这个循环就是控制i递减.同时每个线程都会进入这里取得自己需要转移的桶的下标区间
// 1. true
while (advance) {
int nextIndex, nextBound;
// 1. -1 >= 0,false
if (--i >= bound || finishing)
advance = false;
//transferIndex <= 0 说明已经没有需要迁移的桶了
// 1.nextIndex = 16 <= 0
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
//更新 transferIndex
//为当前线程分配任务,处理的桶结点区间为(nextBound,nextIndex)
// 1.16 > 16 ? 16 -16 : 0 区间 16 到0
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound; // 0
i = nextIndex - 1;// 15
advance = false;
}
}
// i = 15 nextn = 32
// i < 0 ,表示数据迁移已经完成
// i >= n 和 i + n >= nextn 表示最后一个线程也执行完成了,扩容完成了
// 第二个if里面的i=n
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // 完成扩容
nextTable = null; // 删除成员变量
table = nextTab; // 更新table
sizeCtl = (n << 1) - (n >>> 1); // 更新阈值
return;
}
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
}
}
// 待迁移桶为null,用cas把当前节点设置为ForwardingNode节点,表示已经处理
else if ((f = tabAt(tab, i)) == null) // 第一个线程 获取i处的数据为null,
advance = casTabAt(tab, i, null, fwd);// 设置当前节点为 fwd 节点
else if ((fh = f.hash) == MOVED) // 如果当前节点为 MOVED,说明已经处理过了,直接跳过
advance = true; // already processed
else {
// 节点不为空,锁住i位置的头结点
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) { // 表示是链表
int runBit = fh & n; // fn表示f.hash & n ,表示获取原来table的位置
Node<K,V> lastRun = f; // 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; // 当前节点
}
}
// 不管runBit有没有发生变化,只可能是0或者n,
// ln表示的不变化的节点
// hn表示的是变化节点的位置
if (runBit == 0) { // 如果是0,那么ln=lastRun就是位置没有变的这条链 hn=null变化链需要遍历重组
ln = lastRun;
hn = null;
}
else { // 如果当前节点不是0,hn=lastRun这个变化链,ln=null没有变化的链需要遍历重组
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);
// 变化位置
setTabAt(nextTab, i + n, hn);
// 原来table的位置设置fwd节点,表示扩容
setTabAt(tab, i, fwd);
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;
}
}
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);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
size
方法
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
ConcurrentSkipListMap
ConcurrentSkipListMap
和ConcurrentSkipListSet
是TreeMap和TreeSet的有序容器的并发版本
ConcurrentSkipListMap
的底层是通过跳表来实现的。跳表(Skiplist
)是一个链表,但是通过使用“跳跃式”查找的方式使得插入、读取数据时复杂度变成了O(logn),跳表以空间换时间,概率数据结构
- 插入操作
doPut
private V doPut(K kkey, V value, boolean onlyIfAbsent) {
Comparable<? super K> key = comparable(kkey);
for (;;) {
// 找到key的前继节点
Node<K,V> b = findPredecessor(key);
// 设置n为“key的前继节点的后继节点”,即n应该是“插入节点”的“后继节点”
Node<K,V> n = b.next;
for (;;) {
if (n != null) {
Node<K,V> f = n.next;
// 如果两次获得的b.next不是相同的Node,就跳转到”外层for循环“,重新获得b和n后再遍历。
if (n != b.next)
break;
// v是“n的值”
Object v = n.value;
// 当n的值为null(意味着其它线程删除了n);此时删除b的下一个节点,然后跳转到”外层for循环“,重新获得b和n后再遍历。
if (v == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 如果其它线程删除了b;则跳转到”外层for循环“,重新获得b和n后再遍历。
if (v == n || b.value == null) // b is deleted
break;
// 比较key和n.key
int c = key.compareTo(n.key);
if (c > 0) {
b = n;
n = f;
continue;
}
if (c == 0) {
if (onlyIfAbsent || n.casValue(v, value))
return (V)v;
else
break; // restart if lost race to replace value
}
// else c < 0; fall through
}
// 新建节点(对应是“要插入的键值对”)
Node<K,V> z = new Node<K,V>(kkey, value, n);
// 设置“b的后继节点”为z
if (!b.casNext(n, z))
break; // 多线程情况下,break才可能发生(其它线程对b进行了操作)
// 随机获取一个level
// 然后在“第1层”到“第level层”的链表中都插入新建节点
int level = randomLevel();
if (level > 0)
insertIndex(z, level);
return null;
}
}
}
- 删除操作
doRemove
final V doRemove(Object okey, Object value) {
Comparable<? super K> key = comparable(okey);
for (;;) {
// 找到“key的前继节点”
Node<K,V> b = findPredecessor(key);
// 设置n为“b的后继节点”(即若key存在于“跳表中”,n就是key对应的节点)
Node<K,V> n = b.next;
for (;;) {
if (n == null)
return null;
// f是“当前节点n的后继节点”
Node<K,V> f = n.next;
// 如果两次读取到的“b的后继节点”不同(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
if (n != b.next) // inconsistent read
break;
// 如果“当前节点n的值”变为null(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
Object v = n.value;
if (v == null) { // n is deleted
n.helpDelete(b, f);
break;
}
// 如果“前继节点b”被删除(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
if (v == n || b.value == null) // b is deleted
break;
int c = key.compareTo(n.key);
if (c < 0)
return null;
if (c > 0) {
b = n;
n = f;
continue;
}
// 以下是c=0的情况
if (value != null && !value.equals(v))
return null;
// 设置“当前节点n”的值为null
if (!n.casValue(v, null))
break;
// 设置“b的后继节点”为f
if (!n.appendMarker(f) || !b.casNext(n, f))
findNode(key); // Retry via findNode
else {
// 清除“跳表”中每一层的key节点
findPredecessor(key); // Clean index
// 如果“表头的右索引为空”,则将“跳表的层次”-1。
if (head.right == null)
tryReduceLevel();
}
return (V)v;
}
}
}
- 查找操作
findNode
private Node<K,V> findNode(Comparable<? super K> key) {
for (;;) {
// 找到key的前继节点
Node<K,V> b = findPredecessor(key);
// 设置n为“b的后继节点”(即若key存在于“跳表中”,n就是key对应的节点)
Node<K,V> n = b.next;
for (;;) {
// 如果“n为null”,则跳转中不存在key对应的节点,直接返回null。
if (n == null)
return null;
Node<K,V> f = n.next;
// 如果两次读取到的“b的后继节点”不同(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
if (n != b.next) // inconsistent read
break;
Object v = n.value;
// 如果“当前节点n的值”变为null(其它线程操作了该跳表),则返回到“外层for循环”重新遍历。
if (v == null) { // n is deleted
n.helpDelete(b, f);
break;
}
if (v == n || b.value == null) // b is deleted
break;
// 若n是当前节点,则返回n。
int c = key.compareTo(n.key);
if (c == 0)
return n;
// 若“节点n的key”小于“key”,则说明跳表中不存在key对应的节点,返回null
if (c < 0)
return null;
// 若“节点n的key”大于“key”,则更新b和n,继续查找。
b = n;
n = f;
}
}
}
通过上面的源码可以发现:ConcurrentSkipListMap
线程安全的原理与非阻塞队列ConcurrentBlockingQueue
的原理一样:利用底层的插入、删除的CAS原子性操作,通过死循环不断获取最新的结点指针来保证不会出现竞态条件
CopyOnWriteArrayList
CopyOnWriteArrayList
使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组,合适读多写少的场景
当有新元素加入的时候,如下图,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的
当元素在新数组添加成功后,将array这个引用指向新数组
CopyOnWriteArrayList
的整个add操作都是在锁的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的
add
操作的源代码:
public boolean add(E e) {
//1、先加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//2、拷贝数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//3、将元素加入到新数组中
newElements[len] = e;
//4、将array引用指向到新数组
setArray(newElements);
return true;
} finally {
//5、解锁
lock.unlock();
}
}
CopyOnWriteArrayList
有几个缺点:
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然
CopyOnWriteArrayList
能做到最终一致性,但是还是没法满足实时性要求;