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弱一致性
-
迭代器弱一致性
-
并发度和一致性是矛盾的