面试-线程安全集合

15 篇文章 0 订阅

一、线程不安全

ArrayList、LinkedList、HashSet、TreeSet、HashMap、TreeMap等

二、线程安全

如果我们要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现

三、以Concurrent开头的集合类

代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能

  1. ConcurrentHashMap
  2. ConcurrentLinkedQueue
  3. ConcurrentLinkedDeque
  4. ConcurrentSkipListMap
  5. ConcurrentSkipListSet

ConcurrentHashMap: 采用了分段锁技术,在默认理想状态下,ConcurrentHashMap可以支持16个线程执行并发写操作及任意数量线程的读操作,扩容机制和HashMap一样。ConcurrentHashMap读操作不需要加锁和分段锁机制。

ConcurrentLinkedQueue: 线程安全的队列。如果我们要实现一个线程安全的队列有两种实现方式一种是使用阻塞算法,另一种是使用非阻塞算法。使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现,而非阻塞的实现方式则可以使用循环CAS的方式来实现,ConcurrentLinkedQueue非阻塞的方式来实现线程安全队列

ConcurrentLinkedDeque: 双向链表结构的无界并发队列,使用CAS实现并发安全,与ConcurrentLinkedQueue的区别是该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除)。适合多生产-多消费的场景

ConcurrentSkipListMap:提供了一种线程安全的并发访问的排序映射表。内部是SkipList(跳表)结构实现,在理论上能够在O(log(n))时间内完成查找、插入、删除操作

ConcurrentSkipListSet:ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的

四、CopyOnWrite开头的集合类

  1. CopyOnWriteArrayList
  2. CopyOnWriteArraySet
CopyOnWriteArrayList

采用复制底层数组的方式来实现写操作,add的时候通过reentrantLock锁实现线程安全。

CopyOnWriteArraySet

底层封装了CopyOnWriteArrayList,因此它的实现机制完全类似于CopyOnWriteList集合

分析:线程对CopyOnWriteArrayList集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对CopyOnWriteArrayList集合执行写入操作时(包括调用add()、remove()、set()等方法),该集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对CopyOnWriteArrayList集合的写入操作都是对数组的副本执行操作,因此它是线程安全的

总结:CopyOnWriteArrayList执行写入操作时需要频繁地复制数组,性能比较差,但由于读操作与写操作不是操作同一个数组,并且读操作也不需要加锁,因此读操作就很快、很安全。由此可见,CopyOnWriteArrayList适合用在读取操作远远大于写入操作的场景中,比如缓存

五、集合源码分析

1、ConcurrentHashMap

if (tab == null){
	//初始化table的时候,同时会有一个cas的操作
	initTable();
}
void initTable(){
	//使用native方法进行初始化tab
	U.compareAndSwapInt(this, SIZECTL, sc, -1)
}
//判断对应的key在Map是否为空
if(tabAt(tab, i = (n - 1) & hash)==null){//tabAt 使用native方法获取对应的value
	//使用native方法进行获取对应的Tab[i]的值
	casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null));
	//使用native方法完成赋值
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

如果当前hash桶内元素不为空,就会去遍历桶内的链表,遍历开始直接用synchronized锁住


synchronized (f) {
	。。。。。。。
}
get{
	//当指定key在map中的时候
	tabAt(tab, (n - 1) & h)//使用native方法获取对应的value
	//也就是加锁的维度是针对于Map里面的所有的桶。就好比如每个key都加了单独的锁。
	key.next(链表)是没有加锁的
}

ConcurrentLinkedQueue

无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。只有add的时候会用到CAS

//使用for循环的方式cas设置tail
for (Node<E> t = tail, p = t;;) {
    Node<E> q = p.next;
    if (q == null) {
       
        if (p.casNext(null, newNode)) {
           
            if (p != t){
                casTail(t, newNode);
            }
            return true;
        }
       
    }else if (p == q){//判断是否队列只有一个元素
    	p = (t != (t = tail)) ? t : head;
    }else{
    	p = (p != t && t != (t = tail)) ? t : q;
    }    
}

ConcurrentLinkedDeque

双端队列,和ConcurrentLinkedQueue设置值的时候类似,通过for循环调用CAS

ConcurrentSkipListMap

concurrentHashMap与ConcurrentSkipListMap性能测试
在4线程1.6万数据的条件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。
但ConcurrentSkipListMap有几个ConcurrentHashMap 不能比拟的优点:
1、ConcurrentSkipListMap 的key是有序的。
2、ConcurrentSkipListMap 支持更高的并发。ConcurrentSkipListMap 的存取时间是log(N),和线程数几乎无关。也就是说在数据量一定的情况下,并发的线程越多,ConcurrentSkipListMap越能体现出他的优势。

  1. 高并发场景
  2. key是有序的
  3. 添加、删除、查找操作都是基于跳表结构(Skip List)实现的
  4. key和value都不能为null

在这里插入图片描述
ConcurrentSkipListMap里面每个元素是Node如下,包含key和value 以及当前元素的下一个Node

static final class Node<K,V> {
    final K key;     // 键
    V val;           // 值
    Node<K,V> next;  // 指向下一个节点的指针,不是冲突
}
static final class Index<K,V> {
    final Node<K,V> node;   // 当前节点
    final Index<K,V> down;  // 下一层
    Index<K,V> right;       // 当前层的下一个节点
}

疑问:这个没有Hash冲突吗

ConcurrentSkipListSet

concurrentSkipListSet底层用的是concurrentSkipListMap和ConcurrentHashMap

ConcurrentSkipListSet和TreeSet,它们虽然都是有序的集合。但是,第一,它们的线程安全机制不同,TreeSet是非线程安全的,而ConcurrentSkipListSet是线程安全的。第二,ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,而TreeSet是通过TreeMap实现的。

CopyOnWriteArrayList

add元素

final ReentrantLock lock = this.lock;
lock.lock();//先加锁
try {
    Object[] elements = getArray();//拿到原来的
    int len = elements.length;
    Object[] newElements = Arrays.copyOf(elements, len + 1); //复制
    newElements[len] = e;
    setArray(newElements);
    return true;
} finally {
    lock.unlock();//释放锁
}

remove元素

final ReentrantLock lock = this.lock;
lock.lock();
try {
    Object[] current = getArray();
    int len = current.length;
    
	for (int i = 0; i < prefix; i++) {
        if (current[i] != snapshot[i] && eq(o, current[i])) {
            index = i; 				//找到要截断的下标
            break findIndex;
        }
    }
    index = indexOf(o, current, index, len);
    //复制
    Object[] newElements = new Object[len - 1];
    System.arraycopy(current, 0, newElements, 0, index);
    System.arraycopy(current, index + 1,newElements, index,len - index - 1);
    setArray(newElements);
    return true;
} finally {
    lock.unlock();
}

本文转至:https://blog.csdn.net/gaolh89/article/details/104852565

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值