Java并发编程:并发工具(下)

6.8 CountdownLatch-倒计时锁

调用await,等待所有线程完成倒计时(计数减为0)

线程调用countdown使计数减一

public static void main(String[] args) throws InterruptedException {
 CountDownLatch latch = new CountDownLatch(3);
 new Thread(() -> {
 log.debug("begin...");
 sleep(1);
 latch.countDown();
 log.debug("end...{}", latch.getCount());
    }).start();
 new Thread(() -> {
 log.debug("begin...");
 sleep(2);
 latch.countDown();
 log.debug("end...{}", latch.getCount());
    }).start();
 new Thread(() -> {
 log.debug("begin...");
 sleep(1.5);
 latch.countDown();
 log.debug("end...{}", latch.getCount());
    }).start();
 
 log.debug("waiting...");
 latch.await();
 log.debug("wait end...");
}

future

主线程获得线程池中线程的结果

RestTemplate restTemplate = new RestTemplate();
 log.debug("begin");
 ExecutorService service = Executors.newCachedThreadPool();
 CountDownLatch latch = new CountDownLatch(4);
 Future<Map<String,Object>> f1 = service.submit(() -> {
 Map<String, Object> r = 
restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1);
 return r;
 });
 Future<Map<String, Object>> f2 = service.submit(() -> {
 Map<String, Object> r = 
restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1);
 return r;
 });
 Future<Map<String, Object>> f3 = service.submit(() -> {
 Map<String, Object> r = 
restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2);
 return r;
 });Future<Map<String, Object>> f4 = service.submit(() -> {
 Map<String, Object> r = 
restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1);
 return r;
 });
 System.out.println(f1.get());
 System.out.println(f2.get());
 System.out.println(f3.get());
 System.out.println(f4.get());
 log.debug("执行完毕");
 service.shutdown();

6.9 cyclicbarrier

循环栅栏,用来进行线程协作,等待线程满足某个计数。否则使用await等待,满足计数个数时,继续执行。相比于countdownLatch,cyclicbarrier可以重用,满足计数个数时,就会重置。

CyclicBarrier cb = new CyclicBarrier(2); // 个数为2时才会继续执行
new Thread(()->{
 System.out.println("线程1开始.."+new Date());
 try {
 cb.await(); // 当个数不足时,等待
    } 
catch (InterruptedException | BrokenBarrierException e) {
 e.printStackTrace();
    }
 System.out.println("线程1继续向下运行..."+new Date());
 }).start();
 new Thread(()->{
 System.out.println("线程2开始.."+new Date());
 try { Thread.sleep(2000); } catch (InterruptedException e) { }
 try {
 cb.await(); // 2 秒后,线程个数够2,继续运行
    } 
catch (InterruptedException | BrokenBarrierException e) {
 e.printStackTrace();
    }
 System.out.println("线程2继续向下运行..."+new Date());
 }).start();

注意:线程池的线程数要和设置值一致

6.10 线程安全集合类

可分为三大类:

  • 遗留的线程安全集合如:Hashtable,Vector
  • 使用Collections装饰的线程安全集合
  • JUC安全集合:可分为三类关键词
    • Blocking:大部分基于锁实现,提供阻塞
    • CopyOnWrite:修改时拷贝,适合读多写少的场景
    • Concurrent:建议使用,fail-safe
      • 很多操作使用cas操作,提高吞吐量
      • 弱一致性
        • 遍历和修改时数据不一致
        • 求大小一致性,size操作未必100%准确
        • 读取弱一致性
      • 对于非安全容器,遍历时发生修改就立刻失败,抛出异常称为fail-fast

6.11 concurrentHashMap

问题:多线程计数,get、比较、put,这三个操作彼此是原子的,但是合在一起不是原子的。

使用ConcurrentHashMap中的computeIfAbsent方法

 demo(
    () -> new ConcurrentHashMap<String, LongAdder>(),
    (map, words) -> {
 for (String word : words) {
 // 注意不能使用 putIfAbsent,此方法返回的是上一次的 value,首次调用返回 null
 map.computeIfAbsent(word, (key) -> new LongAdder()).increment();
        }
    }
 );
 demo(
    () -> new ConcurrentHashMap<String, Integer>(),
    (map, words) -> {
 for (String word : words) {
 // 函数式编程,无需原子变量
map.merge(word, 1, Integer::sum);
        }
    }
 );

HashMap并发死链(JDK7)

  • 扩容原理:

jdk7中,后加入的元素会放到链表的头部,在数组元素超过数组长度的四分之三时,进行扩容

扩容时,会把当前头部加入新链表的头部,以此类推,最后再把改变下标的元素放到新的位置。

  • 死链原理:

    非线程安全,已经有一个线程做完了扩容操作时,其他线程还在扩容操作,e和next引用没变但内容变了,也就是节点没变,节点后面链着的东西变了,继续扩容时就会死链。

JDK 8 ConcurrentHashMap

属性和内部类

// 默认为 0
 // 当初始化时, 为 -1
 // 当扩容时, 为 -(1 + 扩容线程数)
 // 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;

 // 整个 ConcurrentHashMap 就是一个 Node[]
 static class Node<K,V> implements Map.Entry<K,V> {}

 // hash 表
transient volatile Node<K,V>[] table;

 // 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;

 // 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}

 // 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
 static final class ReservationNode<K,V> extends Node<K,V> {}

 // 作为 treebin 的头节点, 存储 root 和 first,红黑树
 static final class TreeBin<K,V> extends Node<K,V> {}

 // 作为 treebin 的节点, 存储 parent, left, right
 static final class TreeNode<K,V> extends Node<K,V> {}

方法

// 获取 Node[] 中第 i 个 Node
 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
     
 // cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
     
 // 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)

源码分析:数组为table,链表为bin

  • 构造器分析,懒惰初始化
    • 初始容量,不一定为最终初始化容量,最终容量为2的n次方
    • 扩容阈值,比如四分之三
    • 并发度
  • get流程,全称无锁
    • spread方法,确保普通哈希码都是正数
    • 先判断头结点是否是要查找的key
    • 如果头结点为负数,表示该链表在扩容中或者在treebin中,调用find查找
    • 如果都不是,则遍历链表。
  • put流程,不允许有空的键值
    • 先调用spread方法
    • 判断有没有table,没有就创建-initTable方法
    • 判断有没有头节点,如果头节点为空,cas操作直接put
    • 帮忙扩容
    • 如果都不满足,就锁住链表头节点,判断头节点是否被移动过,如果没有被移动,判断是不是链表,如果是就遍历链表,如果有相同的key就更新值,都没找到就追加新的key-value,如果不是链表,是红黑树,判断key在不在树中,在就返回对应的treeNode,释放链表头节点的锁
    • 再判断链表长度是否大于等于树化的阈值,将链表转为红黑树
    • put成功增加size计数,addCount
  • initTable流程
    • 首先判断有没有创建
    • 尝试将sizeCtl设置为-1(表示初始化table)
    • 成功以后就获得锁,创建table,其他线程yield
  • addCount,设置多个累加单元
    • 如果已经有累加数组,就直接累加,如果没有就向一个baseCount累加
    • 累加过程:判断有没有累加数组,没有就创建,然后判断有没有累加单元,没有就创建,然后cas累加
    • 然后判断需不需要扩容,如果已经在扩容则帮忙扩容,需要扩容则创建newTable
  • size方法,没有竞争就向baseCount累加计数,有竞争就新建counterCells,向其中一个cell累加计数,初始有两个,激烈就再创建
  • transfer,扩容
    • 如果nextTable为null,直接把原有的table乘2新建一个nexttable
    • 以链表为单位,进行搬迁

JDK 7 ConcurrentHashMap

维护一个segment数组,每个segment对应一把锁,与jdk8思想类似,缺点:数组大小固定,并且不是懒惰初始化

定位:通过位运算确定key在哪个segment上

  • put流程
    • 计算segment下标
    • 获得segment对象,判断是否为null,是则cas方式创建segment
    • 执行put方法
    • 尝试加锁,不成功则继续尝试,多次后阻塞,顺便检查是否需要创建新的node
    • 成功加锁,更新或者新增,检查是否需要扩容,扩容后才加新节点
  • rehash扩容
    • 容量乘2,创建新的Hash表
    • 旧hash表遍历,三种情况:
      • 没有下一个节点,直接搬迁
      • 扩容之后,记录没有改变下标的节点直接搬迁
      • 改变下标就新建一个节点
  • get流程未加锁
    • 用了UNSAFE方法保证可见性,扩容中get先发生就从旧表取内容,后发生就从新表取内容
  • size计算流程
    • 无限循环统计,如果超过循环重试次数上限就加锁
    • 没有溢出就返回累加值

6.12 linkedBlockingQueue原理

基本数据结构:

Node

next 有三种情况:后继结点,自己,null

入队出队操作:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

出队:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

加锁分析:

用了两把锁,同一时刻可以允许两个线程同时执行(一个生产者,一个消费者)但各自线程又是串行

  • 当节点总数大于2时,putLock保证队尾节点的线程安全,takeLock保证队头节点,两把锁保证入队出队没有竞争
  • 节点总数等于2(一个dummy,一个正常节点),仍然是两把锁锁两个对象,不会竞争
  • 节点总数等于1,只有dummy,take线程会被notEmpty阻塞,也不会产生两把锁锁一个对象

put流程

  • count维护元素计数
  • 如果到达容量上限就等待
  • 有空位就入队,计数加一
  • 如果除了自己还有空位,就去叫醒其他线程
  • 如果队列中只有一个元素,叫醒take线程,唤醒都用signal而不是signalAll

take流程

与put类似

性能比较

LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较

  • Linked 支持有界,Array 强制有界
  • Linked 实现是链表,Array 实现是数组
  • Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
  • Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
  • Linked 两把锁,Array 一把锁

6.13 ConcurrentLinkedQueue

也是两把锁,同一时刻可以允许两个线程同时执行

dummy节点的引入让两把锁锁住不同的对象,避免竞争

但是锁用cas实现

例如:tomcat的connector

6.14 CopyOnWriteArrayList

  • 采用了写入时拷贝的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,不影响其他线程并发读,读写分离。读操作不加锁

  • 适合读多写少的场景

  • 存在get弱一致性

  • 迭代器弱一致性

  • 并发度和一致性是矛盾的

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值