ConcurrentHashMap
HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry列表形成环形数据结构,一旦形成环形数据结构,Entry的next结点永远不为空,产生死循环获取Entry.
HashTable使用synchronized来保证线程安全,因此效率低下。
ConcurrentHashMap使用锁分段技术:首先将数据分成一段一段存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
结构
ConcurrentHashMap由Segment数组结构和HashEntry数组结构组成。Segment是可重入锁,扮演锁的角色;HashEntry存储键值对数据。
一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改的时候,必须首先获得与它对应的Segment锁。
初始化
ConcurrentHashMap的初始化方法是通过initialCapacity,loadFactor,concurrencyLevel等几个参数来初始化segment数组,段偏移量segmentShift,段掩码segmentMask和每个segment里的HashEntry数组来实现。默认情况下concurrencyLevel等于16.
初始化segment:segment数组的长度ssize是大于或等于concurrentLevel的2的N次方值。
初始化segmentShift和segmentMask:sshift等于ssize从1向左移位的次数。默认情况下等于1需要向左位移4次,所以sshift=4.segmentShift等于32-sshift,segmentMask=ssize-1;
初始化每个segment:同样的initialCapacity,loadFactor.segment里HashEntry数组的长度cap=initialCapacity/ssize*c;c是倍数,如果c>1,取c的2的N次方值。所以cap不是1就是2的N次方。segment的容量threshold = (int)cap x loadFactor.
综上默认情况下,initialCapacity=16,loadFactor=0.75,concurrencyLevel=16。对应的ssize=16;sshift=4;segmentShift=28;segmentMask=15;cap=1;threshold=0。
定位Segment
分段锁Segment保护不同段的数据,那么在插入和获取ConcurrentHashMap元素的时候,必须先通过散列算法定位到Segment.
ConcurrentHashMap会对元素的hashcode进行二次hash,以减少hash冲突、
操作
- get,get过程不需要加锁,只有值为空值的时候才加锁重读。内部value定义为volatile。
- put,put过程必须加锁,首先定位到Segment,然后在segment进行插入操作。第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放到HashEntry数组里。
- size,先尝试2次不锁住Segment的方式统计各个Segment大小,如果统计过程中count发生了变化,对所有的Segment的put、remove、clean进行加锁再统计。
ConcurrentLinkedQueue
实现一个线程安全的队列有两种方式:
- 使用阻塞方法:用一个锁(入队和出队用同一把锁)或者用两个锁(入队和出队用不同的锁)等方式实现。
- 使用非阻塞的方法:使用循环CAS。
ConcurrentLinkedQueue是一个基于链接结点的无界线程安全队列,采用FIFO对结点进行排序。
阻塞队列
阻塞队列是支持两个附加操作的队列,插入and移除方法:
- 插入:队列满时不进行插入
- 移除:队列空时不进行移除
ArrayBlockingQueue
用数组实现的有界阻塞队列
LinkedBlockingQueue
用链表实现的有界阻塞队列,默认最大长度为Integer.MAX_VALUE.
PriorityBlockingQueue
支持优先级的无界阻塞队列。默认情况下使用自然顺序排序,可以自定义类实现compareTo方法或初始化时特定构造参数Comparator。需要注意的是不能保证同优先级元素的顺序。
DelayQueue
支持延时获取元素的无界阻塞队列。队列使用PriorityQueue实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素,只有在延迟期满时才能从队列中获取元素。
SynchronousQueue
不存储元素的阻塞队列。每一个put必须等待一个take操作,否则不能继续添加元素。
LinkedTransferQueue
由链表结构构成的无界阻塞队列。实现了tryTransfer和transfer操作。tryTransfer方法是用来试探传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,返回false。transfer等待消费者消费后返回。
LinkedBlockingDeque
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。
Fork/Join框架
将大任务fork成小任务,最后Join各个任务的结果。
ForkJoinTask:
RecursiveAction:用于没有返回结果的任务。
RecursiveTask:用于有返回结果的任务。
ForkJoinPool:ForkJoinTask要通过ForkJoinPool执行。
举例,计算1+2+3+4:
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
public class CountTask extends RecursiveTask<Integer> {
public static final int THRESHOLD = 2;
private int start;
private int end;
public CountTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
boolean canCompute = (end-start) <= THRESHOLD;
if (canCompute) {
for (int i = start; i <= end; i++) {
sum += i;
}
}else {
int middle = (start+end)/2;
CountTask leftTask = new CountTask(start, middle);
CountTask rightTask = new CountTask(middle + 1, end);
leftTask.fork();
rightTask.fork();
int leftResult = leftTask.join();
int rightResult = rightTask.join();
sum = leftResult+rightResult;
}
return sum;
}
public static void main(String[] args){
ForkJoinPool forkJoinPool = new ForkJoinPool();
CountTask task = new CountTask(1, 4);
Future<Integer> result = forkJoinPool.submit(task);
try {
System.out.println(result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
步骤:
1. 类继承RecursiveTask
2. 类实现方法compute()
3. 主线程调用
Fork/Join框架的异常处理:
ForkJoinTask在执行时可能抛异常但是我们在主线程无法直接捕获异常,所以ForkJoinTask提高了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,
并且可以通过getException方法获取异常。
工作窃取算法
工作窃取算法是指某个线程从其他队列里窃取任务来执行。先干完自己队列任务的线程帮其他线程干活。常用双端队列。