《java并发编程的艺术》并发容器和框架

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方法获取异常。

工作窃取算法

工作窃取算法是指某个线程从其他队列里窃取任务来执行。先干完自己队列任务的线程帮其他线程干活。常用双端队列。

猜你喜欢

转载自blog.csdn.net/saywhat_sayhello/article/details/81164721
今日推荐