ConcurrentHashMap
JDK1.6采用Segment分段锁技术提高并发访问效率,首先将数据分成一段一段的存储,然后给每一个段数据配一个锁,这样就可以多线程访问不同段的数据而不受竞争影响。
JDK1.8采用CAS+Synchronized保证并发更新安全,采用数组+链表+红黑树存储结构,默认table[16],put等操作会采用CAS更新,找到table[i]后会将此索引处的链表进行锁定后操作
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());//计算hash值,两次hash操作
int binCount = 0;//统计链表长度
for (Node<K,V>[] tab = table;;) {//类似于while(true),死循环,直到插入成功
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)//检查是否初始化了,如果没有,则初始化
tab = initTable();
/*
i=(n-1)&hash 等价于i=hash%n(前提是n为2的幂次方).即取出table中位置的节点用f表示。
有如下两种情况:
1、如果table[i]==null(即该位置的节点为空,没有发生碰撞),则利用CAS操作直接存储在该位置,
如果CAS操作成功则退出死循环。
2、如果table[i]!=null(即该位置已经有其它节点,发生碰撞)
*/
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
}
else if ((fh = f.hash) == MOVED)//检查table[i]的节点的hash是否等于MOVED,如果等于,则检测到正在扩容,则帮助其扩容
tab = helpTransfer(tab, f);//帮助其扩容
else {//运行到这里,说明table[i]的节点的hash值不等于MOVED。
V oldVal = null;
synchronized (f) {//锁定,(hash值相同的链表的头节点)
if (tabAt(tab, i) == f) {//避免多线程,需要重新检查
if (fh >= 0) {//链表节点
binCount = 1;
/*
下面的代码就是先查找链表中是否出现了此key,如果出现,则更新value,并跳出循环,否则将节点加入到索引处链表的尾部
*/
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)//仅putIfAbsent()方法中onlyIfAbsent为true
e.val = value;//putIfAbsent()包含key则返回get,否则put并返回
break;
}
Node<K,V> pred = e;
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;
}
}
}
}
//插入成功后,如果插入的是链表节点,则要判断下该桶位是否要转化为树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)//实则是>8,执行else,说明该桶位本就有Node
treeifyBin(tab, i);//若length<64,直接tryPresize,两倍table.length;不转树
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
put流程
- 未初始化则初始化
- hash计算得到索引i,table[i]处头节点为null,则插入新节点 return
- table[i]处节点已有值,synchronized锁定此链表后遍历此节点处的链表,已存在则更新,不存在key则在此链表尾部插入
扩容流程
默认table大小16,若某索引位置的链表长度达到8,则把此链表转换成红黑树。若某索引位置的树达到64个节点,则进行table扩容,扩容大小是原先的2倍。
第一个线程进行扩容操作,假如操作到table[2]处,线程2进行put操作,会发现此时table正在进行扩容且扩容到该索引处(table[i].hash = MOVE),则线程2也开始进行扩容(扩容索引为下一个没有进行过的索引处),可能会有多个线程辅助进行扩容完成。
CopyOnWriteArrayList
Copy-On-Write简称COW,写时复制,也是一种读写分离的容器
优点:
1. 读写分离,读与写操作的不是同一个容器,并发时读操作没有同步机制具有很高的性能 ,写操作会重新生成个数组替换原数组
2. 集合遍历时无同步机制,性能高(遍历的是原数组)
缺点:
1. 内存占用问题:添加元素时会进行原数组的复制,数组比较大时性能开销很大
2. 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
使用场景
对容器的写操作比较少,读操作多的情况下使用,比如白名单,黑名单,商品类目的访问和更新场景
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
/**
* 读操作
* 读取的是底层数组array
*/
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* Set操作 重入锁 locak上进行锁定,保证多线程下只有一个线程进行写操作
* copy一个新数组,然后新数组引用替换原数组引用
*/
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
BlockingQueue、BlockingDeque (FIFO)
BlockingQueue阻塞队列,有基于数组、链表两类实现
BlockingDeque阻塞双端队列只有链表实现LinkedBlockingDeque
1. ArrayBlockingQueue
基于数组实现的阻塞队列,包括非阻塞入队、出队方法,阻塞入队出队方法,
相应方法又包括失败返回false与失败抛出异常两类
阻塞方法采用ReentrantLock的Condition实现(默认ArrayBlockingQueue采用非公平锁),阻塞实现如下:
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty = lock.newCondition();
/** Condition for waiting puts */
private final Condition notFull = lock.newCondition();
//入队操作 队列满condition等待
public void put(E e) throws InterruptedException {
checkNotNull(e);
//同一时刻只能有一个线程进行put
final ReentrantLock lock = this.lock;
//重入锁,单个线程可以获取多次锁
lock.lockInterruptibly();
try {
//队列满,执行等待,只可能有一个线程进行等待
//多个线程的话会在获取锁处自旋
while (count == items.length)
notFull.await();
//enqueue操作不需要考虑多线程
enqueue(e);
} finally {
lock.unlock();
}
}
//入队
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
//如果有等待出队的线程则唤醒等待出队的线程
notEmpty.signal();
}
/*
* 出队操作,队列为空则等待
*/
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
//如果有等待入队的线程则唤醒
notFull.signal();
return x;
}
**采用ReentrantLock保证同一时刻只能有一个线程操作队列,使用lock的Conndition控制线程的唤醒与阻塞,ReentrantLock若使用公平锁,唤醒顺序与阻塞顺序一致,
阻塞队列FIFO,先进先出**
链表实现的阻塞队列与数组实现的不同点就是节点类型,一个是数组,一个是链表
LinkedBlockingQueue基于链表实现,与ALinkedBlockingQueue阻塞原理一致
2.PriorityBlockingQueue
优先级阻塞队列,基于数组实现。通过实现Comparator接口控制队列元素的优先级,优先级排序采用二分法计算,阻塞实现同ArrayBlockingQueue一样
3.LinkedTransferQueue
LinkedTransferQueue采用的一种预占模式。意思就是消费者线程取元素时,如果队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程park住,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,唤醒该节点上park住线程,被唤醒的消费者线程拿货走人。这就是预占的意思:有就拿货走人,没有就占个位置等着,等到或超时。
4.SynchronousQueue
如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)。
可以认为这是一种线程与线程间一对一传递消息的模型
公平模式下: 基于队列实现
队尾匹配队头出队,take操作匹配队尾(1对1)然后队头元素出队,FIFO
非公平模式下: 基于栈实现
put入栈,take入栈,put、take匹配,栈指针向下移动两位
-----
take ------栈指针 take、put匹配 指针下移两位指向putA
-----
putB
-----
putA
-----
并发Queue、Deque
lock-free: 无锁
An algorithm is lock-free if, when the program threads are run for a sufficiently long time, at least one of the threads makes progress (for some sensible definition of progress).
如果所有线程运行了足够长时间后,至少有一个线程能获得进展,那么这个算法是无锁的。
用人话讲就是:系统中无论何时,始终有一个线程在工作。
wait-free:无等待
An algorithm is wait-free if every operation has a bound on the number of steps the algorithm will take before the operation completes.
假如一个方法是无等待的,那么它保证了每一次调用都可以在有限的步骤内结束。
用人话讲就是,系统中的所有线程,都会在有限时间内结束,无论如何也不可能出现饿死(starving)的情况。
1. ConcurrentLinkedQueue
采用CAS实现并发队列,wait-free
2. ConcurrentLinkedDeque
采用CAS实现并发队列,wait-free