并发容器

并发容器

  • ConcurrentHashMap:线程安全的HashMap
  • CopyOnWriteArrayList:线程安全的List
  • BlockingQueue:这是一个接口,表示阻塞队列,非常适合用于作为数据共享的通道
  • ConcurrentLinkedQueue:高效的非阻塞并发队列使用链表实现。可以看做一个线程安全的LinkedList
  • ConcurrentSkipListMap:是一个Map,使用跳表的数据结构进行快速查找

ArrayList和HashMap

虽然这两个类不是线程安全的,但是可以用

Collections.synchronizedList(new ArrayList<E>())和

Collections.synchronizedMap(new HashMap<K,V>0)使之变成线程安全的

ConcurrentHashMap和CopyOnWriteArrayList

  • 取代同步的HashMap和同步的ArrayList
  • 绝大多数并发情况下,ConcurrentHashMap和CopyOnWriteArrayList的性能都更好

Map

ConcurrentHashMap

  • Java7中的ConcurrentHashMap最外层是多个segment,每个segment的底层数据结构与HashMap类似,仍然是数组和链表组成的拉链法
  • 每个segment独立上ReentrantLock锁,每个segment之间互不影响,提高了并发效率
  • ConcurrentHashMap默认有16个Segments,所以最多可以同时支持16个线程并发写(操作分别分布在不同的Segment上)。这个默认值可以在初始化的时候设置为其他值,但是一旦初始化以后,是不可以扩容的

  • 每个Node结点都是独立的,并发度很高。
  • 使用链表方式解决hash冲突。
  • 当链表的长度超过阈值8时,为了提高查询速度,将链表转换成红黑树。

线程不安全

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);
}


@Override
public void run() {
for (int i = 0; i < 1000; i++) {
while (true) {
Integer score = scores.get("小明");
Integer newScore = score + 1;

// scores.put("小明",newScore); 线程不安全
boolean b = scores.replace("小明", score, newScore);
if (b) {
break;
}
}
}

}
}

CopyOnWriteArrayList

  • 代替Vector和SynchronizedList,就和ConcurrentHashMap代替SynchronizedMap的原因一样
  • Vector和SynchronizedList的锁的粒度太大,并发效率相对比较低,并且迭代时无法编辑
  • Copy-On-Write并发容器还包括CopyOnWriteArraySet,用来替代同步Set

适用场景

  • 读操作可以尽可能地快,而写即使慢一些也没有太大关系
  • 读多写少:黑名单,每日更新;监听器:迭代操作远多余修改操作

读写规则

  • 回顾读写锁:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
  • 读写锁规则的升级:读取是完全不用加锁的,并且更厉害的是写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待

缺点

  • 数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据,马上能读到,请不要使用CopyOnWrite容器。
  • 内存占用问题:因为CopyOnWrite的写是复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存。

并发队列(Queue)

阻塞队列(BlockingQueue)

  • 阻塞队列是具有阻塞功能的队列,所以它首先是一个队列其次是具有阻塞功能。
  • 通常,阻塞队列的一端是给生产者放数据用,另一端给消费者拿数据用。阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的

  • take()方法:获取并移除队列的头结点,一旦如果执行take的时候,队列里无数据,则阻塞,直到队列里有数据
  • put()方法:插入元素。但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间

take和put不会抛出异常,只会阻塞住

add,remove,element会抛出异常

offer,poll,peek

  • 是否有界(容量有多大):这是一个非常重要的属性无界队列意味着里面可以容纳非常多(Integer.MAXVALUE,约为2的31次,是非常大的一个数,可以近似认为是无限容量)
  • 阻塞队列和线程池的关系:阻塞队列是线程池的重要组成部分

ArrayBlockingQueue

  • 指定容量
  • 公平:指定是否需要保证公平,如果想保证公平的话那么等待了最长时间的线程会被优先处理,不过这会同时带来一定的性能损耗

LinkedBlockingQueue

  • 无界
  • 容量Integer.MAX_VALUE
  • 内部结构:Node、两把锁。

PriorityBlockingQueue

  • 支持优先级
  • 自然顺序(而不是先进先出)
  • 无界队列
  • PriorityQueue的线程安全版本

SynchronousQueue

  • 它的容量为0
  • 需要注意的是,SynchronousQueue的容量不是1而是0,因为SynchronousQueue不需要去持有元素,它所做的就是直接传递(direct handoff )
  • 效率很高
  • SynchronousQueue没有peek等函数,因为peek的含义是取出头结点,但是SynchronousQueue的容量是0,所以连头结点都没有,也就没有peek方法。同理,没有iterate相关方法
  • 是一个极好的用来直接传递的并发数据结构
  • SynchronousQueue是线程池Executors.newCachedThreadPool()使用的阻塞队列

DelayQueue

  • 延迟队列,根据延迟时间排序
  • 元素需要实现Delayed接口,规定排序规则

非阻塞并发队列

并发包中的非阻塞队列只有ConcurrentLinkedQueue这一种顾名思义ConcurrentLinkedQueue是使用链表作为其数据结构的,使用CAS非阻塞算法来实现线程安全(不具备阻塞功能),适合用在对性能要求较高的并发场景。用的相对比较少一些

posted @ 2024-02-29 15:01  wangzhilei  阅读(2)  评论(0编辑  收藏  举报