《Java并发编程的艺术》读书笔记(第6章)

122 阅读5分钟

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操作会造成死循环,因为多线程会导致HashMapEntry链表形成环形数据结构,一旦形成则Entrynext节点永不为空,就会产生死循环获取Entry

ConcurrentHashMap包含Segment(可重入锁)数组和HashEntry数组结构(存储键值对数据)

ConcurrentHashMap初始化

通过initialCapacityloadFactorconcurrencyLevel等几个参数来初始化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);

初始化segmentShiftsegmentMask

初始化每个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

  1. 定位到segment
  2. 是否需要扩容
  3. 定位到位置
  4. 放入HashEntry数组里

size

ConcurrentHashMapcount相加

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);
            }
        }
    }
}
  1. 定位出尾节点
  2. 使用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多了tryTransfertransfer方法

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多了addFirstaddLastofferFirstofferLastpeekFirstpeekLast等方法。

阻塞队列的实现原理

使用通知模式

如下是使用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方法程序会调用ForkJoinWorkerThreadpushTask方法异步执行这个任务

public final ForkJoinTask<V> fork() {
    ((ForkJoinWorkerThread) Thread.currentThread()).pushTask(this);
    return this;
}

pushTask方法将当前任务放在ForkJoinTask数组队列里,调用ForkJoinPoolsignalWork()方法唤醒或者创建一个工作线程来执行任务

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