java--线程安全集合 CopyOnWriteArrayList、ConcurrentHashMap、ConcurrentSkipListMap、BlockingQueue

概要

 线程安全集合可以分为三大类:

  • 从以前 jdk 传承下来的 Hashtable、Vector;
  • 使用Collections装饰的线程安全集合,如:Collections.synchronizedCollection、Collections.synchronizedList、Collections.synchronizedMap、Collections.synchronizedSet 等;
  •  java.util.concurrent.* ;

 java.util.concurrent.* 

   jdk 5 引入并发包 java.util.concurrent.* (简称:juc),juc 下的线程安全集合类,可发现它们是有规律的,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent,如下:

  •    CopyOnWrite     开头的集合,采用了“写入时拷贝”的思想来提高并发度
  •    Concurrent         开头的集合,支持并发(线程安全)
  •    Blocking             开头的集合,支持阻塞操作

基于 lock-free,在常见的多线程访问场景,一般可以提供较高吞吐量;

往往提供 了较低的遍历一致性(弱一致性),如:当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以搜索进行遍历;

弱一致性的另外一个体是,size等操作准确性是有限的,未必是100%准确;

与此同时,读取的性能具有一定的不确定性;

与弱一致性对应的,就是同步容器常见的行为 “fail-fast”,也就是检测到容器在遍历过程中发生了修改,则抛出ConcurrentModificationException,不再继续遍历

CopyOnWriteArrayList

CopyOnWriteArrayList 底层实现采用拷贝写入的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其他线程的并发读,读写分离。

 

过程介绍:

  • CopyOnWriteArrayList 支持多线程并发读取 (如:get, 遍历) ,只支持单线程写入 (如:add, remove)
  • 当有修改操作发生时,会对原有的数组进行拷贝,拷贝出一个新的数组,修改操作在新的数组上发生,修改时加锁。
  • 原有数组的查询操作不需要加锁保护,当修改线程执行完毕,再用新的数组替换原有数组。
  • 把读写操作分离开,读不加锁,写加锁,用空间换取读不加锁
  • 适合读多写少的场景

 ConcurrentHashMap

Hashtable 是锁住了整个map集合,而 ConcurrentHashMap,它只会锁住map集合中的一个桶,根据桶的多少,可以进一步提高并发度,只要读写操作落在不同的桶里,操作就可以并行执行

1.7 以前 

采用的是分段锁,过程如下:

  •  初始化时(不是懒惰初始化),会创建所有 segment 数组的元素,而且 segmen t数组不能扩容(默认大小为16(并行度)),占用内存多
  •  put 操作会锁住 key 对应的 segment,元素放入 HashEntry 的数组+链表结构中,元素放入链表头
  •  get 操作无锁,仅需保证可见性,扩容过程中,get 先发生从旧表中读取,get 发生后就从新表中读取
  •  扩容会加锁,它发生在 put 方法内,因此要提前加锁
  •  计算个数(size)前,先不加锁多次计算,前后两次结果一样,认为个数正确,计算超过三次将所有 segment 锁住,重新计算个数返回
  •  数据结构 数组 segment + 数组 HashEntry + 链表 HashEntry,从图中首先是一个 segment 数组,每个 segment 的元素又是一个HashEntry 数组,Node 数组中又存放了链表

1.8 又做了修改

  •  初始化数组时,使用 cas 来保证并发安全性,懒惰初始化
  •  当容量小于64首先尝试扩容,当超过这个容量并且链表大于或等于8,会将链表树化,树化过程中会锁住链表头
  •  put 操作会锁住链表头,新加的元素放入链表尾部
  •  get 操作不需要加锁,仅需要用cas保证元素的可见性
  •  扩容 以链表为单位扩容,当扩容时有多个线程来同时访问,这些线程会协助扩容
  •  数据结构 数组 Node+链表 Node /红黑树 TreeNode

 

ConcurrentSkipListMap

ConcurrentSkipListMap 类似于之前 LinkedHashMap ,都可以保持元素遍历的顺序和放入的顺序是一致的。

它们的不同之处:

  • LinkedHashMap 非线程安全
  • ConcurrentSkipListMap 线程安全
  • LinkedHashMap 数据结构 数组+链表
  • ConcurrentSkipListMap 数据结构 跳跃表

跳跃表结构如下,白色链表部分起到了快速定位底层链表的效果:

  查询操作

  比如要查询 7,查找流程如下:

第 4 层 1 --> NIL (有大于7的吗?no --> 向下)
第 3 层 1 --> 4  --> 6 --> NIL(有大于7的吗?no --> 向下)
第 2 层                     6 --> 9 (有大于7的吗?yes --> 向下)
第 1 层                     6 --> 7 (找到)

   插入操作

   插入元素动态图如下:

BlockingQueue  阻塞队列

队列,它是先进先出的,经常用来实现生产消费模式,来解耦生产者、消费者线程。

其类如下:

阻塞队列演示

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class TestBlockingQueue {

    public static void main(String[] args) throws InterruptedException {
        //创建队列,指定队列容量为5
        BlockingQueue<String> queue = new LinkedBlockingDeque<>(5);

        // 存放元素, 如果集合满了,put 方法会阻塞,也就是数据个数超过容量,队列就不能在装下数据,除非把队列里的元素出列
        queue.put("a");
        queue.put("b");
        queue.put("c");
        queue.put("d");
        queue.put("e");

        // 获取元素, 如果集合空了, take 方法会阻塞,没有数据可以拿,也就阻塞了
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
        System.out.println(queue.take());
    }
}

结果

结果中队列一直在等待取到第六个数据,队列中没有第六个数据,所以控制台一直是开启状态,没有结束编译。

线程安全控制的三个级别

  • JVM 级别:通常以 cas 指令形式,是一种级别低的、细粒度的技术
  • 低级使用程序类:锁定和原子类,使用 cas 作为并发原语,ReentrantLock 类提供与 synchronized 原语相同的锁定和内存语义
  • 高级使用程序类:信号、互斥、屏障、交换程序等

cas 

 cas 它是乐观锁(无锁并发), 不断重试直到成功,体现的是乐观的精神,而 synchronized 它是悲观锁 , 同一时刻,只能有一个线程访问 synchronized 代码块的内容。

乐观锁应用场景:

  •  原子操作类 例如 AtomicInteger,AtomicBoolean... 它们内部用的都是 cas 的无锁并发
  •  适用于并发量较小,多cpu情况

 volatile 关键字

配合 cas 编程时,共享变量的声明上必须用 volatile ,因为线程的工作区是独立的,当线程读取到主存的数据时,会缓存一份在线程里,对数据的修改在工作区内,修改完数据后新值还没有来得及更改到主存中,这时如果有人访问数据,访问的还是原来的值,因此要想读取主存中最新的结果,就必须加上 volatile 。(详情请见:https://blog.csdn.net/grey_mouse/article/details/84193844

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值