Java并发容器和框架
ConcurrentHashMap的实现原理与使用
并发编程中HashMap可能会造成程序死循环,HashTable效率较低。
举个HashMap线程不安全的例子:
final HashMap<String, String> map = new HashMap<String, String>(2);
Thread t = new Thread(new Runnable() {
@Overrride
public void run() {
for(int i = 0; i < 10000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
map.put(UUID.randomUUID().toString(),"");
}
},"ftf" + i).start();
}
}
},"ftf" + i);
t.start();
t.join();
这里执行put操作会造成死循环,因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成则Entry的next节点永不为空,就会产生死循环获取Entry。
ConcurrentHashMap包含Segment(可重入锁)数组和HashEntry数组结构(存储键值对数据)
ConcurrentHashMap初始化
通过initialCapacity、loadFactor和concurrencyLevel等几个参数来初始化Segment数组、段偏移量segmentShift、段掩码segmentMask和每个segment里的HashEntry数组来实现的。
初始化segments数组
if(concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;
while(ssize < concurrencyLevel) {
++ sshift;
ssize <<= 1;
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1;
this.segments = Segment.newAarry(ssize);
初始化segmentShift和segmentMask
初始化每个segment
if(initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAX_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = 1;
while(cap < c)
cap <<= 1;
for(int i = 0; i < this.segments.length; ++i)
this.segments[i] = new Segmet<K,V>(cap, loadFactor);
定位Segment
再进行一次散列算法
ConcurrentHashMap操作
get
不需要加锁,只有读到空值才会加锁重读
public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
并且JMM保证volatile字段的写早于读,因此get总是可以拿到最新的值
transient volatile int count;
volatile V value;
put
- 定位到
segment - 是否需要扩容
- 定位到位置
- 放入
HashEntry数组里
size
各ConcurrentHashMap的count相加
ConcurrentLinkedQueue(非阻塞队列)
使用循环CAS实现
入队
public boolean offer(E e) {
if(e == null) throw new NullPointerException();
//入队前,创建一个入队节点
Node<E> n = new Node<E>(e);
retry:
//死循环,入队不成功反复入队
for(;;) {
//创建一个指向tail节点的引用
Node<E> t = tail;
//p用来表示队列的尾节点,默认情况下等于tail节点。
Node<E> p = t;
for(int hops = 0; ; hops++) {
//获得p节点的下一个节点
Node<E> next = succ(p);
//next节点不为空说明p不是尾节点,需要更新p后在将它指向next节点
if(next != null) {
//循环了两次及其以上,并且当前节点还是不等于尾节点
if(hops > HOPS && t != tail)
continue retry;
p = next;
}
//如果p是尾节点,则设置p节点的next节点为入队节点
else if(p.casNext(null,n)) {
/**
* 如果tail节点有大于等于1个next节点,则将入队节点设置成tail节点,
* 更新失败也没关系,因为失败了表示有其他线程成功更新了tail节点
*/
if(hops >= HOPS)
casTail(t, n);//更新tail节点,允许失败
return true;
}
//p有next节点,表示p的next节点是尾节点,则重新设置p节点
else {
p = succ(p);
}
}
}
}
- 定位出尾节点
- 使用
CAS算法将入队节点设置成尾节点的next节点,若不成功重试
定位尾节点
succ方法
final Node<E> succ(Node<E> p) {
Node<E> next = p.getNext();
return (p == next) ? head : next;
}
设置入队节点为尾节点
p.casNext(null,n)方法将入队节点设置为尾节点的next节点。
p是null则p是当前队列的尾节点,若不是null则其他线程更新了尾节点,p需要重新获取当前队列尾节点
HOPS的设计意图
只有当tail节点和尾节点的距离大于等于HOPS的值,才会更新tail节点,这样不用每次都CAS更新tail节点提高入队效率
出队
每次出队更新head节点,通过hops变量减少使用cas更新head节点消耗
public E poll() {
Node<E> h = head;
//p表示头节点,需要出队的节点
for(int hops = 0;; hops++) {
//获取p节点的元素
E item = p.getItem();
//如果p节点的元素不为空使用CAS设置p节点的引用元素为null
//成功则返回p节点元素
if(item != null && p.casItem(item, null)) {
if(hops >= HOPS) {
//将p节点下一个节点设置成head节点
Node<E> q = p.getNext();
updateHead(h, (q != null) ? q : p);
}
return item;
//如果头节点的元素为空或头节点发生变化,说明头节点已被另一个线程更新
//重新找头节点
Node<E> next = succ(p);
//如果p的下一个节点也为空,说明这个队列已空
if(next == null) {
//更新头节点
updateHead(h, p);
break;
}
//若下一个元素不为空,则将头节点的下一个节点设置成头节点
p = next;
}
return null;
}
}
Java中的阻塞队列
什么是阻塞队列
支持两个附加操作的队列
- 支持阻塞的插入方法
- 支持阻塞的移除方法
阻塞队列常用于生产者/消费者场景
四种处理方式
- 插入方法:
- 抛出异常:add(e)
- 返回特殊值:offer(e)
- 一直阻塞:put(e)
- 超时退出:offer(e,time,unit)
- 移除方法
- 抛出异常:remove()
- 返回特殊值:poll()
- 一直阻塞:take()
- 超时退出:poll(time, unit)
- 检查方法
- 抛出异常:element()
- 返回特殊值:peek()
- 一直阻塞:不可用
- 超时退出:不可用
Java里的7种阻塞队列
ArrayBlockingQueue
使用数组实现的有界阻塞队列,FIFO,对元素进行排序。
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
访问者的公平性是使用可重入锁实现的
public ArrayBlockingQueue(int capacity, boolean fair) {
if(capacity <= 0) throw new IllegalArgumentException();
this.items = new Object(capacity);
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
LinkedBlockingQueue
- 链表实现
- 最大长度:
Integer.MAX_VALUE - FIFO
PriorityBlockingQueue
- 支持优先级的无界阻塞队列
- 自然顺序顺序排列
- 自定义类实现
compareTo()方法指定元素排序规则或初始化前指定构造函数Comparator - 不能保证同优先级元素的顺序
DelayQueue
延时获取元素的无界阻塞队列,常用于:
- 缓存系统设计
- 定时任务调度
SynchronousQueue
- 不存储元素的阻塞队列,每一个put操作等待一个take操作
- 支持公平访问队列
- 默认为非公平性策略访问队列
public SynchronousQueue(boolean fair) {
transferer = fair ? new TransferQueue() : new TransferStack();
}
LinkedTransferQueue
由链表结构组成的无界阻塞TransferQueue队列。相较于LinkedTransferQueue多了tryTransfer和transfer方法
transfer方法
消费者:take()方法或poll()方法
transfer方法将元素存在队列的tail节点
Node pred = tryAppend(s, haveData);
return awaitMatch(s, pred, e, (how == TIMED), nanos);
尝试将s存入tail节点
CPU自旋等待消费者消费元素,自旋一定此数后使用Thread.yield()方法来暂停当前线程,并执行其他线程
tryTransfer方法
用来试探生产者传入的元素是否能直接传给消费者。
若没有消费者则返回false,它是立即返回,transfer会自旋
tryTransfer(E e, long timeout, TimeUnit unit)
LinkedBlockingDeque
链表结构组成的双向阻塞队列
比LinkedBlockingQueue多了addFirst、addLast、offerFirst、offerLast、peekFirst和peekLast等方法。
阻塞队列的实现原理
使用通知模式
如下是使用Condition实现ArrayBlockingQueue的JDK源码
private final Condition notFull;
private final Condition notEmpty;
public ArrayBlockingQueue(int capacity, boolean fair) {
//省略其他代码
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(E e) throw InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while(count == items.length)
notFull.await();
insert(e);
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
阻塞模式
public final void await() throws InterruptedException {
if(Thread.interrupted()) throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while(!isOnSyncQueue(node)) {
LockSupport.park(this);
if((interruptMode = checkInterruptWhileWaiting) != 0)
break;
}
if(acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if(node.nextWaiter != null)
unlinkCancelledWaiters();
if(interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
Fork/Join框架
- 是Java7提供的一个用于并行执行任务的框架
- 大任务分割成小任务,汇总小任务结果得到大任务结果的框架
ForkJoinTask类
需要继承该类的子类:
RecursiveAction:无返回RecursiveTask:有返回值
ForkJoinPool类
执行ForkJoinTask
应用实例
需求:计算1+2+3+4
package fj;
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> {
private 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();
//生成一个计算任务,负责计算1+2+3+4
CountTask task = new CountTask(1,4);
//执行一个任务
Future<Integer> result = forkJoinPool.submit(task);
try {
System.out.println(result.get());
} catch (InterruptedException e) {
} catch (ExecutionException e){
}
}
}
``Fork/Join`框架的异常处理
执行的时候可能会抛出异常,但是我们没有办法在主线程里直接捕获异常,可以通过ForkJoinTask提供的isCompletedAbnormally()方法来检查是否已经抛出异常或被取消了,并可以通过getException方法获取异常
if(task.isCompletedAbnormally()) {
System.out.println(task.getException());
}
getException()方法返回一个Throwable对象,被取消则返回CancellationException。未完成或没有异常则返回null。
fork方法实现
使用fork方法程序会调用ForkJoinWorkerThread的pushTask方法异步执行这个任务
public final ForkJoinTask<V> fork() {
((ForkJoinWorkerThread) Thread.currentThread()).pushTask(this);
return this;
}
pushTask方法将当前任务放在ForkJoinTask数组队列里,调用ForkJoinPool的signalWork()方法唤醒或者创建一个工作线程来执行任务
final void pushTask(ForkJoinTask<?> t) {
ForkJoinTask<?>[] q;
int s, m;
if((q = queue) != null) {
long u = (((s = queueTop) & (m = q.length - 1)) << ASHIFT) + ABASE;
UNSAFE.putOrderedObject(q, u, t);
queueTop = s + 1;
if((s -= queueBase) <= 2)
pool.signalWork();
else if(s == m)
growQueue();
}
}
join实现原理
调用doJoin()方法,结合任务状态:
- 已完成:NORMAL
- 被取消:CANCELLED
- 信号:SIGNAL
- 异常:EXCEPTIONAL 有三种返回值:
- 已完成:返回任务结果
- 被取消:抛出
CancellationException - 异常:抛出对应异常
public final V join() {
if(doJoin() != NORMAL) return reportResult();
else return getRawResult();
}
private V reportResult() {
int s; Throwable ex;
if((s = status) == CANCELLED)
throw new CancellationException();
if(s == EXCEPTIONAL && (ex = getThrowableException()) != null)
UNSAFE.throwException(ex);
return getRawResult();
}
- doJoin源码
private int doJoin() {
Thread t;
ForkJoinWorkerThead w;
int s;
boolean completed;
if((t = Thread.currentThread()) instanceof ForkJoinWorkerThead) {
if((s = status) < 0) return s;
if((w = (ForkJoinWorkerThead)t).unpushTask(this)) {
try {
completed = exec();
} catch (Throwable rex) {
return setExceptionalCompletion(rex);
}
if(completed) return setCompletion(NORMAL);
}
return w.joinTask(this);
}
else return externalAwaitDone();
}
首先查看了任务状态,已完成直接返回,未执行则从任务数组里取出并执行
顺利完成设置为NORMAL,异常则记录并设置EXCEPTIONAL