线程安全的集合类

简介: 线程安全的集合类

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

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.

相关文章
|
8天前
|
安全 Java
并发编程之常见线程安全类以及一些示例的详细解析
并发编程之常见线程安全类以及一些示例的详细解析
13 0
|
3天前
|
安全 Java 开发者
【JAVA】哪些集合类是线程安全的
【JAVA】哪些集合类是线程安全的
|
2月前
|
存储 缓存 安全
【C/C++ 关键字 存储类说明符 】 线程局部变量的魔法:C++ 中 thread_local的用法
【C/C++ 关键字 存储类说明符 】 线程局部变量的魔法:C++ 中 thread_local的用法
33 0
|
18天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
42 0
|
18天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(上)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)
35 0
|
21天前
|
存储 安全 Java
java多线程之原子操作类
java多线程之原子操作类
|
22天前
|
Java
Java中的多线程实现:使用Thread类与Runnable接口
【4月更文挑战第8天】本文将详细介绍Java中实现多线程的两种方法:使用Thread类和实现Runnable接口。我们将通过实例代码展示如何创建和管理线程,以及如何处理线程同步问题。最后,我们将比较这两种方法的优缺点,以帮助读者在实际开发中选择合适的多线程实现方式。
24 4
|
24天前
|
Java Spring
springboot单类集中定义线程池
该内容是关于Spring中异步任务的配置和使用步骤。首先,在启动类添加`@EnableAsync`注解开启异步支持。然后,自定义线程池类`EventThreadPool`,设置核心和最大线程数、存活时间等参数。接着,将线程池bean注入到Spring中,如`@Bean("RewardThreadPool")`。最后,在需要异步执行的方法上使用`@Async`注解,例如在一个定时任务类中,使用`@Scheduled(cron = "...")`和`@Async`结合实现异步定时任务。
17 2
|
2月前
|
Linux API C++
【C++ 线程包裹类设计】跨平台C++线程包装类:属性设置与平台差异的全面探讨
【C++ 线程包裹类设计】跨平台C++线程包装类:属性设置与平台差异的全面探讨
56 2
|
1天前
|
监控 安全 Java
【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题
【多线程学习】深入探究阻塞队列与生产者消费者模型和线程池常见面试题