文章目录
1、概览
- ConcurrentHashMap:线程安全的HashMap
- CopyOnWriteArrayList:线程安全的List
- BlockingQueue:接口,表示阻塞队列,非常适合用于作为数据共享的通道。
- ConcurrentLinkedQueue :高效的非阻塞并发队列,使用链表实现。可以看作是一个线程安全的LinkedList。
- ConcurrentSkipListMap:是一个Map,使用跳表的数据结果进行快速查找。
2、ConcurrentHashMap
- 先介绍一下hashMap
3、JDK7中的 concurrenthashmap
-
Java7中的ConcurrenHashMap最外层是多个segment,每个segment的底层数据结构于HashMap类似,仍然是数组和链表组成的拉链法。
-
每个segment独立上ReentranLock锁,每个segment之间互不影响,提高了并发效率。
-
ConcurrentHashMap默认有16个segments,所以最多可以同时支持16个线程并发写(操作在不同的segment上)。这个默认值可以在初始化的时候设置为其它的值,但是一旦初始化以后,是不可以进行扩容的。
4、JDK8中的 concurrenthashmap
-
几乎是把jdk7中的ConcurrentHashMap重写了,代码量从一千多行变为现在的六千多行。
在实现和结构上有很大的区别
-
不在采用segment,而是采用node。
-
数据结构、Hash碰撞、保证并发安全、查询复杂度等方面不同。
-
保证线程安全的方式变为了synchronized和CAS
-
结构 (借鉴hashmap1.8的设计结构,链表加红黑树)
-
保证所有的put和get操作都是线程安全的。但是不能保证组合操作是线程安全的。
-
3、CopyOnWriteArrayList(适合读多写少的场景)
3.1诞生的原因
- 代替Vecotr和SynchronizedList,就和ConcurrentHashMap代替SynchronizedMap的原因是一样的。Vecotr和SynchronizedList的锁的粒度太大,效率低。
3.2 适用场景
- 读操作尽可能的快,写操作慢一点也没有问题。例如:黑名单,每日更新……
- 读写锁规则的升级:读取是不用加锁的,并且更厉害的是**,写入也不会阻塞读取操作**,只有写入和写入之间需要进行同步操作。
3.3代码演示
- 演示CopyOnWriteArrayList可以在迭代的过程中修改数组内容,但是ArrayList不行,对比
public class CopyOnWriteArrayListDemo1 {
public static void main(String[] args) {
//如果是ArrayList,到这里的时候5被删除了,就会报错
// ArrayList<String> list = new ArrayList<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
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");
}
}
}
}
尽管已经修改list的内容,但是迭代器中的内容任然没有发生改变。–》修改的时候将内存中的值复制一份,对复制的数据进行操作。
3.4 实现原理(copyOnWrite)
- CopyOnWrite的含义
- 修改的时候将内存中的值复制一份,对复制的数据进行操作。修改完成后将内存指向这个地址。
- 这样就可以实现在读的同时实现修改的操作。提高读效率,由于写的次数很少,就不用担心读取不到最新的数据。
- **创建新副本,读写分离。**最后再替换回去。
- “**不可变”**原理,旧的容器不会发生改变。
3.5 缺点
- 数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据能马上得到,就不要使用CopyOnWrite.
- 内存占用问题:因为CopyOnWrite的写是复制机制,所以再进行写操作的时候,内存里会同时驻扎两个对象。
3.6源码分析
- get方法没有加锁,可以随意读取。但是写操作是加锁同步的
4、并发队列Queue(阻塞队列–BlockingQueue)
4.1为什么要采用队列
- 用队列可以在线程中传递数据:生产者消费者模式,银行转账。
- 考虑线程安全的重任转移到了**“队列”**上。
4.2常见队列
- Queue
- BlockingQueue:阻塞队列。如果队列为空,则读取的时候一直等待。如果队列满了,插入会一直等待。
4.3什么是阻塞队列
- 阻塞队列的一端给生产者放数据用,另一端给消费者拿数据。阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的。
- 两个具有特色的阻塞功能:
- take()方法:湖区并移除队列的头结点,一旦队列中无数据,则阻塞,直到队列中有数据。
- put()方法:插入元素。但是如果队列已满,则阻塞,直到队列里有了空闲空间。
- **是否有界:**这时一个非常重要的属性,无界意味着里面可以容纳非常多的数据。
- 阻塞队列和线程池的关系:阻塞队列是线程池的重要组成部分。
4.4BlockingQueue主要方法。
- put,take //这些方法失败的时候会阻塞
- add,remove,element //这些方法不满足的时候抛出异常
- offer:添加一个元素,返回布尔值。 poll:取出一个值,空的话就返回空值,取出并删除。peek:取出一个值,但是不会进行删除。
4.5ArrayBlockingQueue
-
有界
-
指定容量
-
公平:还可以指定是否需要保证公平(等待了最长时间的线程被优先处理),但是会造成性能损耗。
-
代码演示:
/** * 描述: 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.6 LinkedBlockingQueue
-
无界
-
容量值为Integer.MAX_VALUE
-
内部结构:Node,两个锁。分析put方法。
-
源码分析:
-
结点结构
-
put方法分析
-
4.7PriorityBlockingQueue.
- 支持优先级
- 自然顺序(而不是先进先出)
- 无界队列(可以进行扩容)
4.8SynchonousQueue
- 容量为0
- 不需要持有元素,直接进行接收传递
- 效率高,是一个极好的用来直接传递的并发数据结构。
- 是线程池Executors.newCachedThreadPool()的使用的阻塞队列。
5、非阻塞并发队列
- 并法宝中的非阻塞队列中只有ConcurrentLinkedQueued这一种,使用链表作为数据结构,使用CAS非阻塞算法实现线程安全,适合用在对性能要求较高的并发场景,用的情况较少。