【Beautiful JUC Part.8】ConcurrentHashMap等并发集合
并发容器概览、集合类的历史、ConcurrentHashMap(重点、面试常考)
CopyOnWriteArrayList、并发队列Queue(阻塞队列、非阻塞队列)
一、并发容器的概览
-
ConcurrentHashMap:线程安全的HashMap
-
CopyOnWriteArrayList:线程安全的List
-
BlockingQueue:这是一个接口,表示阻塞队列,非常适合用于作为数据共享的通道
-
ConcurrentLinkedQueue:高效的非阻塞并发队列,使用链表实现。可以看做一个线程安全的LinkedList
-
ConcurrentSkipListMap:是一个Map,使用跳表的数据结构进行快速查找
二、古老和过时的同步容器(历史)
1、Vector和Hashtable
JDK早起设计的并发安全的集合类,主要问题是性能不好。
vector的源码,大多数都是由synchronized保护的,性能不好。
hashtable也是一样的,大多数都是synchronized修饰的,高并发性能差
2、HashMap和ArrayList
这俩是线程不安全的,但是有升级版
下面是具体的实现代码
可以看出和声明vector和hashtable的大同小异。所以性能也一般。
3、ConcurrentHashMap和CopyOnWriteArrayList
取代同步的HashMap和同步的ArrayList
绝大多数并发情况下,ConcurrentHashMap和CopyOnWriteArrayList的性能都更好
三、ConcurrentHashMap
Map简介、为什么需要ConcurrentHashMap、HashMap的分析
JDK1.7中的ConcurrentHashMap的实现和分析
JDK1.8中的ConcurrentHashMap实现和源码分析
对比两个版本优缺点
组合操作:ConcurrentHashMap也不是线程安全的?
1、Map简介
HashMap、Hashtable、LinkedHashMap、TreeMap都是这个接口的实现
2、为什么需要ConcurrentHashMap?
HashMap是线程不安全的
同时put碰撞导致数据丢失
同时put扩容导致数据丢失
死循环造成的CPU100%(仅在JDK7及以前存在)
- 多个线程扩容单的时候,会造成链表的死循环(你指向我,我指向你)
- 其实这本来就不是个问题,因为HashMap本身就是不支持并发的,要并发就ConcurrentHashMap
3、HashMap并发的特点
非线程安全、迭代时候不允许修改内容、只读的并发是安全的,如果一定要把HashMap用在并发环境,用Collections.synchronizedMap(new HashMap())
4、JDK1.7的ConcurrentHashMap实现和分析
Java7中的ConcurrentHashMap最外层是多个segment,每个segment的底层数据结构与HashMap类似,仍然是数组和链表组成的拉链法、
每个segement独立上ReentrantLock锁,每个segment之间互不影响,提高了并发效率。
ConcurrentHashMap默认有16个Segments,所以最多可以同时支持16个线程并发写(操作分别分布在不同的Segment上)。这个恶魔人之可以再初始化的时候设置为其他值,但是一旦初始化以后,是不可以扩容的。
5、JDK1.8的ConcurrentHashMap实现和分析
红黑树把查询从O(n)变为了O(logn)
putVal的流程
get的流程
6、为什么要把1.7的结构改成1.8的结构
数据结构
Hash碰撞
保证并发安全
- 1.7是采用的分段锁,segment来保证并发安全,segment继承自ReentrantLock
- 1.8是通过CAS加上synchronized
查询复杂度提高了
为什么超过8要转为红黑树?
默认是链表,想要达到冲突为8,正常情况下是比较难的,概率只有千万分之一,如果真的发生这样的情况,可以确保在极端情况下,采用红黑树占用较大空间,提高查询效率。
7、COncurrentHashMap也不是线程安全的?
组合操作线程不安全
import java.util.concurrent.ConcurrentHashMap;
/**
* 描述: 组合操作并不保证线程安全
*/
public class OptionsNotSafe implements Runnable{
private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<String, Integer>();
public static void main(String[] args) throws InterruptedException {
scores.put("小明", 0);
Thread t1 = new Thread(new OptionsNotSafe());
Thread t2 = new Thread(new OptionsNotSafe());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(scores.get("小明"));
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
Integer score = scores.get("小明");
Integer newScore = score + 1;
scores.put("小明", newScore);
}
}
}
ConcurrentHashMap可以保证单个操作,比如说单个put或者get操作是线程安全的,但是组合操作,并不保证。
解决办法
方法一:上锁
这种方法不好,和普通的HashMap没啥区别了
方法二:使用replace方法
运行结果
方法二演进:
四、CopyOnWriteArrayList
1、诞生历史
2、使用场景
读操作可以尽可能的快,而写即是慢一些也没有太大关系
读多写少:黑名单,每日更新;监听器:跌倒操作远多于修改操作
3、读写规则
4、普通ArrayList缺陷
package collections.copyonwirte;
import java.util.ArrayList;
import java.util.Iterator;
/**
* 描述:演示CopyOnWriteArrayList可以再迭代的过程中修改数组内容
* 但是ArrayList不行
*/
public class CopyOnWriteArrayListDemo1 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println("list is" + list);
String next = iterator.next();
System.out.println(next);
if (next.equals("2")) {
list.remove("5");
}
if (next.equals("3")) {
list.add("3 found");
}
}
}
}
缺陷就是不能再迭代的时候进行修改
5、CopyOnWriteArrayList应用
将4中的代码改变
运行结果
为什么迭代器最后还是5呢?这就是它的特性,在迭代过程中,你改你的内容,我按照原来的去执行。
6、实现原理
CopyOnWrite的含义
意思就是在写的时候,将原来的数据copy一份到新的内存中去,然后再新的内存中做写操作,这样在修改完之后,再把指针指向新的内存区域,这样就实现了线程安全。并且CopyOnWrite的适用场景大多是读情况比较高,写操作较少。
总结就是创建新的副本、读写分离、旧的容器是不可变的。
迭代器对比
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 描述: 对比两个迭代器
*/
public class CopyOnWriteArrayListDemo2 {
public static void main(String[] args) throws InterruptedException {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(new Integer[]{1, 2, 3});
System.out.println(list);
Iterator<Integer> itr1 = list.iterator();
list.remove(2);
Thread.sleep(1000);
System.out.println(list);
Iterator<Integer> itr2 = list.iterator();
itr1.forEachRemaining(System.out::println);
itr2.forEachRemaining(System.out::println);
}
}
可以看出迭代器和生成这个迭代器时候的状态有关,并不是实时变化的。
源码分析
是一个array数组,上锁时候用的ReentrantLock()
上面的添加操作,是上锁的,我们看一下get()方法
整个get方法是没有上锁的,读操作不会出现阻塞。
7、缺点
五、并发队列
1、为什么使用队列
队列可以在线程间传递数据:生产者消费者模式、银行转账
考虑线程安全的重任转移到了“队列”上
2、阻塞队列BlockingQueue
什么是阻塞队列
阻塞功能:最具特色的两个带有阻塞功能的方法
-
take()方法:获取并移除队列的头结点,一旦如果执行take的时候,队列里无数据,则阻塞,直到队列里有数据
-
put()方法:插入元素。但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间
-
是否有界(容量有多大):这是一个非常重要的属性,无界队列意味着里面可以容纳非常多(Integer.MAX_VALUE,约为2的31次,是一个非常大的数,可以认为是无限容量)
-
阻塞队列和线程池的关系:阻塞队列是线程池的重要组成部分
BlcokingQueue的主要方法
3、重要实现ArrayBlockingQueue
使用案例
有10个面试者,一共只有1个面试官,大厅里有3个位子供面试者休息,每个人的面试时间是10秒,模拟所有人面试的场景
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* 描述: TODO
*/
public class ArrayBlockingQueueDemo {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<String>(3);
Interviewer r1 = new Interviewer(queue);
Consumer r2 = new Consumer(queue);
new Thread(r1).start();
new Thread(r2).start();
}
}
class Interviewer implements Runnable {
BlockingQueue<String> queue;
public Interviewer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
System.out.println("10个候选人都来啦");
for (int i = 0; i < 10; i++) {
String candidate = "Candidate" + i;
try {
queue.put(candidate);
System.out.println("安排好了" + candidate);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
queue.put("stop");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable {
BlockingQueue<String> queue;
public Consumer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String msg;
try {
while(!(msg = queue.take()).equals("stop")){
System.out.println(msg + "到了");
}
System.out.println("所有候选人都结束了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
源码分析
4、LinkedBlockingQueue
无界、容量为Integer.MAX_VALUE、内部结构:Node、两把锁。
构造方法
内部属性:有两把锁,take和put锁
put方法
如果已经满了,就休息,如果没有满,就把这个结点放进去队列。
5、其他阻塞队列
PriorityBlockingQueue
支持优先级
自然顺序(而不是先进先出)
无界队列
PriorityQueue的线程安全版本
SynchronusQueue
DelayQueue
6、非阻塞并发队列
7、如何选择适合自己的队列
考虑边界、空间、吞吐量