线程安全的集合类

简介: 线程安全的集合类

原来的集合类,大部分都是线程不安全的.

Vector,Stack,HashTable,是线程安全的(不建议用),其它的集合类不是线程安全的.

多线程使用ArrayList

1.自己使用同步机制(Synchronized或者ReentrantLock),前面已经做过许多讨论了,这里不再展开.

2.Collections.synchronizedList(new ArrayList);

相当于给ArrayList套壳->得到了新的对象,新对象里面的方法都是加锁的.

synchronizedList是标准库提供的一个基于synchronized进行线程同步的List.synchronizedList的关键操作上都带有synchronized.

3.使用CopyOnWriteArrayList.

CopyOnWrite容器即写时复制的容器,这个主要用于解决多个线程修改一个数据的问题.

当我们往一个容器添加元素的时候,不直接往当前容器里添加,而是先将当前容器进行Copy,复制出一个新的容器,然后向新的容器里添加元素,

添加完元素之后,再将原容器的引用指向新容器.

一旦有线程修改值时,把顺序表复制一份,修改这个顺序表的内容,并修改引用的指向(这个操作是原子的)

这样写的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素.

所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器.

比如服务器在加载配置文件时,就需要把配置文件解析出来放到内存数据结构.

优点:在读多写少的场景下,性能很高,不需要加锁竞争.

缺点:1.占用内存较多(因此修改不能太频繁). 2.新写的数据不能被第一时间读取到.

多线程环境使用队列

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

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

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

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

多线程环境使用哈希表

HashMap本身是不安全的.

在多线程环境下可以使用: HashTable, ConcurrentHashMap.

HashTable

只是简单的把关键方法加上了synchronized关键字.

public synchronized V put(K key, V value) {

public synchronized V get(Object key) {

这相当于直接针对HashTable对象加锁.

如果多线程访问同一个HashTable就会造成锁冲突.

size属性也通过synchronized来控制同步,也是比较慢的.

一旦触发扩容,就由该线程完成整个扩容过程,这个过程就会涉及到大量的元素拷贝,效率非常低.

一个HashTable只有一把锁.两个线程访问HashTable中的任意数据都会出现锁竞争.

我们认为这样的类仍然不好用.我们希望:如果修改两个不同链表上的元素,不涉及线程安全的问题(因为修改的是不同变量).但如果在同一个链表上修改,涉及线程安全问题.操作引用时,就可能涉及到操作同一引用.因此我们在这里引入ConcurrentHashMap.

ConcurrentHashMap

相比于HashTable做出了一系列的改进和优化.它就解决了HashTable出现的部分问题:即针对同一个链表操作再加锁,针对不同链表操作,不必加锁(不要产生锁冲突)->缩小了锁的粒度

下面来看一下详细介绍:

读操作没有加锁(但是使用volatile保证从内存中读取结果),只对写操作进行加锁.加锁的方式仍然是使用synchronized,但是不是锁整个对象,而是"锁桶"(用每个链表的头结点作为锁对象),大大降低了锁冲突的概率.

充分利用了CAS的特性.比如size属性通过CAS来更新.避免出现重量级锁的情况.

优化了扩容方式:化整为零

   (1)发现需要扩容的线程,只需要创建一个新的数组,同时搬几个元素过去.

   (2)扩容期间,新老数组同时存在

   (3)后续每个来操作ConcurrentHashMap的线程,都会参与搬家的过程.每个操作负责搬运一小部分元素

   (4)搬完最后一个元素再把老数组删掉

   (5)这个期间,插入只往新数组中加.

   (6)这个期间,查找需要同时查新数组和老数组.

这种设定,不会产生更多的空间代价.因为java中任何一个对象都可以直接作为锁对象.本身,在哈希表中,就得有数组,数组元素都是存在的(每个链表的头节点),用其做对象加锁即可.

ConcurrentHashMap每个哈希桶上都有一把锁.只有两个线程访问的恰好是同一个哈希桶上的数据才出现锁冲突.

相关面试题

1.ConcurrentHashMap的读是否要加锁,为什么?

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

2.介绍下ConcurrentHashMap的锁分段技术?

这个是Java1.7所采取的技术.Java1.8中已经不再使用了.简单的说就是把若干个哈希桶分成一个"段"(Segment),针对每个段分别加锁.

目的也是为了降低锁冲突的概率.当两个线程访问的数据恰好在同一个段上时,才会触发锁竞争

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

取消了分段锁,直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头节点对象作为锁对象).

将原来的数组 + 链表的实现方式改进成 数组 + 链表 /红黑树的方式.当链表较长的时候(大于等于8个元素)就转换成红黑树.

4.HashMap和HashTable,ConcurrentHashMap之间的区别?

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

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

ConcurrentHashMap: 线程安全.使用synchronized锁每个链表的头节点,锁冲突概率较低,充分利用CAS机制,优化了扩容方式.key不允许为null.

相关文章
|
2月前
|
存储 安全 Java
【Java集合类面试二十五】、有哪些线程安全的List?
线程安全的List包括Vector、Collections.SynchronizedList和CopyOnWriteArrayList,其中CopyOnWriteArrayList通过复制底层数组实现写操作,提供了最优的线程安全性能。
|
2月前
|
安全 Java 调度
|
2月前
|
安全 Java 程序员
线程安全与 Vector 类的分析
【8月更文挑战第22天】
24 4
|
2月前
|
安全 Java
【Java集合类面试十三】、HashMap如何实现线程安全?
实现HashMap线程安全的方法包括使用Hashtable类、ConcurrentHashMap,或通过Collections工具类将HashMap包装成线程安全的Map。
|
2月前
|
Java
【Java集合类面试十二】、HashMap为什么线程不安全?
HashMap在并发环境下执行put操作可能导致循环链表的形成,进而引起死循环,因而它是线程不安全的。
|
2月前
|
安全 Java API
Java多线程编程:使用Atomic类实现原子操作
在Java多线程环境中,共享资源的并发访问可能导致数据不一致。传统的同步机制如`synchronized`关键字或显式锁虽能保障数据一致性,但在高并发场景下可能导致线程阻塞和性能下降。为此,Java提供了`java.util.concurrent.atomic`包下的原子类,利用底层硬件的原子操作确保变量更新的原子性,实现无锁线程安全。
15 0
【多线程面试题 二】、 说说Thread类的常用方法
Thread类的常用方法包括构造方法(如Thread()、Thread(Runnable target)等)、静态方法(如currentThread()、sleep(long millis)、yield()等)和实例方法(如getId()、getName()、interrupt()、join()等),用于线程的创建、控制和管理。
|
2月前
|
安全 Java
【Java集合类面试五】、 如何得到一个线程安全的Map?
如何得到一个线程安全的Map的方法包括:使用Collections工具类将Map包装为线程安全,使用java.util.concurrent包下的ConcurrentHashMap,以及不推荐使用性能较差的Hashtable。
|
2月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
64 1
|
4天前
|
数据采集 负载均衡 安全
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口
本文提供了多个多线程编程问题的解决方案,包括设计有限阻塞队列、多线程网页爬虫、红绿灯路口等,每个问题都给出了至少一种实现方法,涵盖了互斥锁、条件变量、信号量等线程同步机制的使用。
LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口