Java并发编程的艺术笔记-Java并发容器和框架

1.ConcurrentHashMap的实现原理与使用

1.1 为什么要使用ConcurrentHashMap
  • 线程不安全的HashMap:HashMap在并发执行put操作时会引起死循环(JDK1.8之前的问题)
    • 因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry(原因是因为JDK1.7链表加入节点是头插法)
final HashMap<String, String> map = new HashMap<>(2);
Thread t = new Thread(new Runnable() {
    @Override
    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");
t.start();
t.join();
  • 效率低下的HashTable:HashTable容器使用synchronized来保证线程安全

    • 线程1使用put进行元素添加,线程2不但不能使用put方法,也不能使用get方法(访问HashTable的线程都必须竞争同一把锁)
  • ConcurrentHashMap使用锁分段技术:

    • 假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,则多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争
1.2 ConcurrentHashMap的结构
  • 由Segment数组结构和HashEntry数组结构组成

  • Segment是一种可重入锁(ReentrantLock)

  • HashEntry用于存储键值对数据

  • Segment的结构和HashMap类似,是一种数组和链表结构

  • 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素

  • 当对HashEntry数组的数据进行修改时,须先获得与它对应的Segment锁

在这里插入图片描述

1.3 定位Segment
  • 在插入和获取元素的时候,ConcurrentHashMap须先通过散列算法定位到Segment
  • ConcurrentHashMap会使用算法对元素的hashCode进行一次再散列(减少Hash冲突,使元素均匀分布在不同的Segment上)
1.4 ConcurrentHashMap的操作
  • get操作:
    • 整个get过程不需要加锁,除非读到的值是空才会加锁重读
    • 将要使用的共享变量都定义成volatile类型(get方法中自需要读不需要写这些共享变量)
public V get(Object key) {
    // 1.先经过一次再散列(一次再散列)
    int hash = hash(key.hashCode());
    // 2.使用这个散列值通过散列运算定位到Segment,再通过散列算法定位到元素(两次散列定位)
    return segmentFor(hash).get(key, hash);
}

  • put操作:

    • 因为需要对共享变量进行写入操作,所以在操作共享变量时必须加锁

    • 插入操作需要经历两个步骤:

      • 是否需要扩容:在插入元素前会先判断Segment里的HashEntry数组是否超过容量,如果超过阈值,则对数组进行扩容(HashMap是先插入再判断扩容,很可能扩容后就没有元素要插入了)

      • 如何扩容:创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里(只对某个segment进行扩容)

  • size操作:先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如
    果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小


2.Java中的阻塞队列

2.1 什么是阻塞队列
  • 支持两个附加操作的队列:

    • 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满
    • 支持阻塞的移除方法:在队列为空时,获取元素的线程会等待队列变为非空
  • 常用于生产者和消费者的场景,即生产者用来存放元素、消费者用来获取元素的容器

2.2 Java里的阻塞队列
  • ArrayBlockingQueue:

    • 数组实现的有界阻塞队列
    • 默认情况下不保证线程公平的访问队列,可以指定为公平队列(底层使用ReentrantLock(fair))
  • LinkedBlockingQueue:

    • 链表实现的有界阻塞队列
  • PriorityBlockingQueue:

    • 支持优先级的无界阻塞队列
    • 不能保证同优先级元素的顺序
  • DelayQueue:

    • 支持延时获取元素的无界阻塞队列

    • 在创建元素时可以指定多久才能从队列中获取当前元素(只有在延迟期满时才能从队列中提取元素)

  • SynchronousQueue:

    • 不存储元素的阻塞队列(意味着每一个put操作必须等待一个take操作,否则不能继续添加元素)
    • 支持公平访问队列(默认采用非公平性策略访问队列)
  • LinkedTransferQueue:

    • 由链表结构组成的无界阻塞TransferQueue队列
    • 相较于其他阻塞队列多了tryTransfer和transfer方法
      • transfer:把生产者传入的元素立刻transfer(传输)给消费者
      • tryTransfer:试探生产者传入的元素是否能直接传给消费者
  • LinkedBlockingDeque:

    • 链表结构组成的双向阻塞队列

    • 在多线程同时入队时减少了一半的竞争


3.Fork/Join框架

3.1 什么是Fork/Join框架
  • Fork:把一个大任务切分为若干子任务并行的执行
  • Join:合并这些子任务的执行结果,最后得到大任务的结果
3.2 工作窃取算法
  • 指某个线程从其他队列里窃取任务来执行(因为子任务会被分到不同的队列,并且由不同线程负责,而不同线程执行速度不同)

  • 为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列(被窃取任务线程从双端队列的头部拿任务,窃取任务的线程从双端队列的尾部拿任务)

  • 优点:

    • 充分利用线程进行并行计算,减少了线程间的竞争
  • 缺点:

    • 在某些情况下还是存在竞争(比如双端队列里只有一个任务时)
    • 会消耗了更多的系统资源(比如创建多个线程和多个双端队列)
3.3 Fork/Join框架的设计
  • 分割任务:fork类来把大任务分割成子任务

  • 执行任务并合并结果:

    • 分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行
    • 子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据
3.4 使用Fork/Join框架
@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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值