多线程笔记(三)

多线程笔记(三)

1. 同步容器与并发容器

同步容器

通过synchronized关键字实现线程安全的容器;或通过Collections这个工具类的synchronizedXXX方法创建的容器,都称为同步容器

例如Vector, Stack, Hashtable

Vector是list接口的线程安全实现

Stack是Vector的子类,是一个先进后出的栈,入栈和出栈都是同步的

Hashtable是Map接口的线程安全实现

并发容器

同步容器一次只能允许一个线程去使用,因此性能较差。

允许多线程同时使用容器,并能保证线程安全的容器都是并发容器。

并发容器有两个接口,分别为ConcurrentMapBlockingQueue

主要的实现

  • CopyOnWrite容器
    • CopyOnWriteArrayList
    • CopyOnWriteArraySet
  • ConcurrentMap的实现类
    • ConcurrentHashMap
    • ConcurrentSkipListMap(支持排序)
  • 阻塞队列的实现
    • ArrayBlockingQueue:使用数组实现的有界阻塞队列
    • LinkedBlockingQueue:使用链表实现的有界阻塞队列
    • PriorityBlockingQueue:支持优先级的无界阻塞队列
    • DelayQueue:支持延时获取元素的无界阻塞队列
    • SyncronousQueue:不存储元素的阻塞队列
    • LinkedTransferQueue:使用链表实现的无界阻塞队列
    • LinkedBlockingDeque:使用链表实现的双向阻塞队列
  • 非阻塞队列的实现:
    • ConcurrentLinkedQueue:使用链表实现的无界非阻塞队列
    • ConcurrentLinkedDeque:使用链表实现的双向非阻塞队列

2. CopyOnWriteArrayList

CopyOnWriteArrayList是一个允许多线程使用,能够保证线程安全,底层使用数组实现的并发容器。

基本设计思想

​ 内部还是使用数组来存放数据,CopyOnWrite指的是写时复制,一个数组在读的时候使用原数组,写的时候,假如添加一个元素进来,先copy原来的数组,添加一个元素的位置,然后把新的元素放进来。这时内存里面同时存在两个数组,原数组支持读请求,新数组支持读请求。同时将指向原数组的变量改为指向新数组。

缺点:

​ 每次写的时候,都去cpoy一份数据出来,如果数据比较大的话,比较耗费内存

​ 只能保证数据最终一致,不能保证实时一致,当数据在修改的时候,读取到的数据是”旧“的值。

适用场景:读多写少,对实时性要求不是特别高。

3. ConcurrentHashMap

概述

ConcurrentHashMap是一个实现Map功能的并发容器,也可以认为是一个线程安全的HashMap

不同JDK版本里面的实现机制不一样的。JDK1.8之前是数组加链表,JDK1.8及其之后是数组加链表/红黑树。

ConcurrentHashMap继承了AbstractMap,实现了ConcurrentMap接口

AbstractMap实现了Map接口,提供了Map接口的骨干实现,如果我们自己想要实现一个Map,可以继承AbstractMap,这样可以最大限度的减少自己实现Map这类数据结构所需要的工作量。

ConcurrentMap主要提供了一些针对Map的原子操作

内部结构

使用Node<K, V>[](Node类型的数组)来存放数据

Node节点类型

Node:

用来存放k-v数据的node,如果发生了哈希冲突,那么就使用链表法解决

TreeBin

它是一个指向红黑树的代理节点,用来存放数据,树上的节点是TreeNode,TreeNode继承了Node节点。TreeBin的作用是方便对红黑树的操作(左旋,右旋,删除,平衡等等),TreeBin还包含了加锁解锁等操作。

ForwardingNode

是一种临时节点,扩容的时候才会使用。不存储数据。

ReservationNode

保留节点,给ConcurrentHashMap中的一些特殊方法使用,不存储数据。只在computeIfAbsent和compute这两个方法里面使用。

扩容和数据迁移的思路

扩容:

  1. 数组扩容:创建一个新数组,通常长度为原来的两倍

  2. 数据迁移:把旧的数组里的数据拷贝到新的数组里面

扩容部分大家可以看看这一篇 https://blog.csdn.net/zzu_seu/article/details/106698150

4. BlockingQueue

阻塞队列(BlockingQueue):在并发环境下,调用队列的过程中,会根据情况去阻塞调用线程,实现这样带阻塞功能的队列,就是阻塞队列。

阻塞队列是通过“锁”?来实现的,主要用在生产者-消费者模式,用于线程间的数据交换和系统解耦。

阻塞队列的作用:

  • 如果线程向队列插入元素,而这个时候队列满了,就会阻塞这个线程,直到队列有空闲。
  • 如果线程从队列中获取元素,而这个时候,队列为空,就会阻塞这个线程,直到队列里面有数据

BlockingQueue接口中的一些方法

操作成功返回true, 如果操作失败抛异常:add(E e), remove(Object o)

操作成功返回true,操作失败返回false:offer(E e)

队列满了阻塞调用线程:put(E e), take()

阻塞+超时:offer(E e, long timeout, TimeUnit unit), poll(long timeout, TimeUnit unit)

阻塞队列的特点

  • 不能包含null元素
  • 实现这个接口的类都必须是线程安全的
  • 可以限定容量大小

5. ArrayBlockingQueue

ArrayBlockingQueue是BlockingQueue接口的典型实现。

ArrayBlockingQueue是基于数组来实现的,有界的阻塞队列

ArrayBlockingQueue特点

  • 队列容量在创建的时候指定,之后不可更改
  • 插入元素在队尾,删除元素在队首
  • 队列满了,对阻塞插入元素的线程,队列为空,会阻塞删除元素的线程
  • 支持公平/非公平的册罗,默认是非公平的
  • 加的锁是全局锁,如果在处理出队的时候,是处理不了入队的,反之同理。在超高并发环境下,可能会有性能问题

6. LinkedBlockingQueue

LinkedBlockingQueue是BlockingQueue接口的典型实现。

LinkedBlockingQueue是基于链表实现的,一种近似有界阻塞队列。

LinkedBlockingQueue特点

  • 与ArrayBlockingQueue的全局锁不同的是,LinkedBlockingQueue有两把锁,一把是控制入队的putLock,一把是控制出队的takeLock。
  • 与ArrayBlockingQueue初始必须指定队列大小不同的是,其可以在初始化时指定队列的容量,如果不指定,容量大小默认为Integer的最大值。
  • 与ArrayBlockingQueue可以指定公平/非公平策略不同的是,LinkedBlockingQueue不可以指定公平/非公平策略。

7. ConcurrentLinkedQueue

ConcurrentLinkedQueue是Queue接口的实现

ConcurrentLinkedQueue是基于链表实现的,无界的非阻塞队列

与阻塞队列最大的不同是,该队列不再基于“锁”来保证队列的并发安全性,而是通过自旋+CAS的方式来保证

0 个评论

要回复文章请先登录注册