线程安全的集合类(多线程环境下使用ArrayList、队列及哈希表)

简介: 线程安全的集合类(多线程环境下使用ArrayList、队列及哈希表)

多线程环境下使用ArrayList

在多线程环境下使用ArrayList可以有以下三种方式:

1.使用同步机制 (synchronized 或者 ReentrantLock)

2.Collections.synchronizedList(new ArrayList)synchronizedList 是标准库提供的一个基于synchronized 进行线程同步的 List,synchronizedList 的关键操作上都带有 synchronized,使用这个方法把集合类套一层。

3.使用 CopyOnWriteArrayList

CopyOnWrite容器即写时复制的容器,简称“COW”,也叫做“写时拷贝”。如果针对这个ArrayList进行读操作,不做任何额外的工作。如果进行写操作,则拷贝一份新的ArrayList,针对新的进行修改,修改过程中如果有读操作,就继续读旧的这份数据。当修改完毕了,使用新的替换旧的(本质上就是一个引用之间的赋值,是原子的)。

很明显,这种方案,优点是不需要加锁,不需要锁竞争,在读多写少的场景下的性能很高;缺点则是要求这个ArrayList不能太大,否则占用内存太大,而且新写的数据不能第一时间被读到。


多线程环境下使用队列

1) ArrayBlockingQueue 基于数组实现的阻塞队列

2) LinkedBlockingQueue 基于链表实现的阻塞队列

3) PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列

4) TransferQueue 最多只包含一个元素的阻塞队列

TransferQueue 的应用场景是,当不想生产者过度生产消息时,TransferQueue可能非常有用,在这样的设计中,消费者的消费能力将决定生产者产生消息的速度。


多线程环境下使用哈希表

HashMap 本身不是线程安全的,在多线程环境下使用哈希表可以使用:Hashtable和ConcurrentHashMap

Hashtable是线程安全的,它是在关键方法上加了synchronized。更推荐使用的是ConcurrentHashMap,它是更优化的线程安全哈希表。

ConcurrentHashMap进行了哪些优化?比HashTable好在哪里?和HashTable之间的区别是啥?

1.最大的优化之处:ConcurrentHashMap相比于HashTable 大大缩小了锁冲突的概率,把一把大锁,转换成多把小锁了。


HashTable的做法是直接在方法上加synchronized,等于是给this加锁,只要操作哈希表上的任意元素,都会产生加锁,也就都可能会发生锁冲突。但是实际上,基于哈希表的结构特点,有些元素在进行并发操作的时候,是不会产生线程安全问题的,也就不需要使用锁控制。

1dd8eed7f9bf4039bbcb1156c8d86887.png

此时元素1和元素2在同一链表上,如果线程A修改(增删)元素1,线程B修改元素2,那么此时是有线程安全问题的(相邻两元素并发的插入或者删除的时候,相邻两节点的next指向可能会发生改变)。如果线程A修改(增或者删)元素3,线程B修改元素4,,这个情况相当于多个线程修改不同的变量,那么此时是没有线程安全问题的。


使用HashTable,锁冲突概率就太大了,任何两个元素的操作都会有锁冲突,即使是处在不同的链表上,这就是不用HashTable的主要原因。


ConcurrentHashMap做法是,每个链表有各自的锁(而不是大家共用同一个锁了),具体来说,就是使用每个链表的头结点,作为锁对象(两个线程针对同一个锁对象加锁才有锁竞争,才有阻塞等待,针对不同对象,没有锁竞争)。

此时,把锁的粒度变小了,针对12这个情况,是针对同一把锁进行加锁,会有锁竞争,会保证线程安全。针对34这个情况,是针对不同的锁进行加锁,不会有锁竞争了,没有阻塞等待,程序就会更快。


2.针对读操作,不加锁,只针对写操作加锁

读和读之间没有冲突;写和写之间有冲突,可以加锁;读和写之间没有冲突,但是很多场景下,读写之间不加锁控制,如果写操作不是原子的,那么会产生脏读,所以使用了 volatile 保证了原子性。


3.ConcurrentHashMap内部充分的使用了CAS,通过这个也来进一步的削减加锁操作的数目


4.针对扩容,采取了"化整为零"的方式

HashMap/HashTable扩容:

创建一个更大的数组空间,把旧的数组上的链表上的每个元素搬运到新的数组上(删除+插入),这个扩容操作会在某次put 的时候进行触发。如果元素个数特别多,就会导致这样的搬运操作,比较耗时。


ConcurrentHashMap 中,扩容采取的是每次搬运一小部分元素的方式。创建新的数组,旧的数组也保留。每次 put 操作,都往新数组上添加,同时进行一部分搬运(把一小部分旧的元素搬运到新数组上)。每次get的时候,则旧数组和新数组都查询。每次remove 的时候,只是把元素删了就行了。经过一定时间之后,所有的元素都搬运好了,最终再释放旧数组。

小结:

Hashtable和HashMap、ConcurrentHashMap 之间的区别?

HashMap: 线程不安全. key 允许为 null

Hashtable: 线程安全,使用 synchronized 锁 Hashtable 对象, 效率较低,key 不允许为 null

ConcurrentHashMap: 线程安全,使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用

CAS 机制,优化了扩容方式,key 不允许为 null

ConcurrentHashMap在jdk1.8做了哪些优化?

取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象)。将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式,当链表较长的时候(大于等于

8 个元素)就转换成红黑树。

PS:分段锁是 Java1.7 中采取的技术,Java1.8 中已经不再使用了,简单的说就是把若干个哈希桶分成一个

"段", 针对每个段分别加锁,目的也是为了降低锁竞争的概率。当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争。

ConcurrentHashMap的读是否要加锁?

读操作没有加锁,目的是为了进一步降低锁冲突的概率,为了解决脏读,保证读到刚修改的数据, 搭配了

volatile 关键字。


相关文章
|
8天前
|
安全 Java
并发编程之常见线程安全类以及一些示例的详细解析
并发编程之常见线程安全类以及一些示例的详细解析
13 0
|
3天前
|
安全 Java 开发者
【JAVA】哪些集合类是线程安全的
【JAVA】哪些集合类是线程安全的
|
1天前
|
监控 安全 Java
【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题
【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题
|
1天前
|
缓存 安全 Java
多线程--深入探究多线程的重点,难点以及常考点线程安全问题
多线程--深入探究多线程的重点,难点以及常考点线程安全问题
|
1天前
|
数据采集 安全 Java
Python的多线程,守护线程,线程安全
Python的多线程,守护线程,线程安全
|
5天前
|
监控 Java 调度
Java多线程实战-从零手搓一个简易线程池(四)线程池生命周期状态流转实现
Java多线程实战-从零手搓一个简易线程池(四)线程池生命周期状态流转实现
|
5天前
|
设计模式 Java
Java多线程实战-从零手搓一个简易线程池(三)线程工厂,核心线程与非核心线程逻辑实现
Java多线程实战-从零手搓一个简易线程池(三)线程工厂,核心线程与非核心线程逻辑实现
|
5天前
|
Java 测试技术
Java多线程实战-从零手搓一个简易线程池(二)线程池实现与拒绝策略接口定义
Java多线程实战-从零手搓一个简易线程池(二)线程池实现与拒绝策略接口定义
|
5天前
|
存储 安全 Java
Java多线程实战-从零手搓一个简易线程池(一)定义任务等待队列
Java多线程实战-从零手搓一个简易线程池(一)定义任务等待队列
|
5天前
|
存储 消息中间件 Java
Java多线程实战-异步操作日志记录解决方案(AOP+注解+多线程)
Java多线程实战-异步操作日志记录解决方案(AOP+注解+多线程)