Java 多线程系列Ⅶ(线程安全集合类)

简介: Java 多线程系列Ⅶ(线程安全集合类)

前言

在数据结构中,我们学习过 Java 的内置集合,但是我们知道,我们学过的大多数集合类都是线程不安全的,少数如 VectorStackHashTable 是线程安全的,但这些都是一些比较“粗糙”的类(在所有方法上加了 synchronized 锁),一般不建议使用。

那么当我们想要在多线程下使用集合类该怎么处理呢?

一、多线程使用线性表

方式1:手动给会出现线程安全问题的逻辑加锁。

例如多个线程修改 ArrayList,此时可能出现问题,就可以给修改操作进行加锁。

方式2:使用 Collections.synchronizedList(); 封装

将想要使用的线性表用上述方法封装起来,相当于给集合里的关键方法加上了锁。

方式3:使用使用 CopyOnWriteArrayList

CopyOnWriteArrayList 原理

CopyOnWriteArrayList 又称写时复制的容器。当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是我们可以对 CopyOnWriteArrayList 容器进行并发的读,而不需要加锁(只有在写时加锁),因为当前容器不会添加任何元素。

所以说 CopyOnWriteArrayList 容器采用的是一种读写分离的思想,读和写不同的容器。

优点:

适用于读多写少的场景下。在读多写少的场景下,性能很高,不需要加锁竞争.

缺点:

在修改时需要重新拷贝容器,占用内存较多。

新写的数据不能被第一时间读取到,即可能出现“脏读”问题。

二、多线程使用栈和队列

多线程使用栈和队列,我们可以直接使用 Java 标准库提供的阻塞队列,因为带有阻塞功能,这些集合在多线程下是线程安全的:

  1. ArrayBlockingQueue 基于数组实现的阻塞队列
  2. LinkedBlockingQueue 基于链表实现的阻塞队列
  3. PriorityBlockingQueue 基于堆实现的带优先级的阻塞队列
  4. TransferQueue 最多只包含一个元素的阻塞队列

三、多线程下使用哈希表

哈希表也是我们经常会使用到的集合类,而标准库提供了 3 种哈希表,3 种哈希表之间的区别也是一个非常重要的知识点,下面就花点时间,对比一下 HashMapHashTableConcurrentHashMap

(1)HashMap

HashMap 不必多说,这就是一个在单线程下使用的哈希表,本身是不安全的。

(2)HashTable

HashTable 是对其中的公共方法加上了 synchronized 锁,其实就想当于给整个哈希表上了锁。如果多个线程访问同一个 HashTable 就和产生锁竞争,而且一旦触发扩容就由该线程完成整个扩容过程,效率会非常低。我们可以简单画个图理解一下:

(3)ConcurrentHashMap

ConcurrentHashMap 在 HashTable 的基础上做了一些优化:

Java1.7 中主要的优化手段是:

使用的是分段锁技术, 简单的说就是把若干个哈希桶分成一个"段" (Segment),针对每个段分别加锁。目的也是为了降低锁竞争的概率。当两个线程访问的数据恰好在同一个段上的时候,才触发锁竞争。

Java1.8 中的优化

优化1:读写操作(最关键的优化)

在Java1.8中取消了分段锁,直接给每个哈希桶/每个链表,分配了一个锁,就是以每个链表的头结点对象作为锁对象。在读操作上取消了加锁,使用了 volatile 保证从内存读取结果。同样我们画个图理解:

举个例子:

假如现在有两个线程,插入两个元素。线程 1 插入元素对应下标为 1 的链表上;线程 2 插入的元素,对应在下标为 2

的链表上。此时就相当于是两个线程修改不同的变量,显然是没有线程安全问题的。ConcurrentHashMap,每次插入操作只是针对对应的链表加锁,操作不同的链表就是针对不同的锁加锁,不会产生锁竞争。因此这就导致大部分加锁实际上是没有所冲突的,而这里的加锁操作开销也就微乎其微了。如果此时使用的是 HashTable,由于是对整个哈希表加锁,这两个插入依然会对同一个 this 产生锁竞争,产生阻塞等待。

优化2:重复利用CAS特性

例如更新、获取元素个数直接使用 CAS 完成,不必加锁。

优化3:优化扩容机制(化整为零)

我们知道哈希表中有一个参数叫做“负载因子”。如果元素过多,导致负载因子过大就要考虑扩容。

扩容就需要重新申请内存空间,把元素从旧的哈希表上删掉,插入到新的哈希表上。但是如果哈希表元素非常多,搬运一次就会导致这一次put操作非常卡顿。


而对于 ConcurrentHashMap 的扩容策略——化整为零。发现需要扩容的线程,只需要创建一个新的数组(更大的内存空间),同时只搬几个元素过去。扩容期间,新老数组同时存在。


后续每个来操作 ConcurrentHashMap 的线程,都会参与搬家的过程。每个操作负责搬运一小部分元素,搬完最后一个元素再把老数组删掉。在此期间,插入只往新数组加,查找需要同时查新数组和老数组。

优化4:底层实现

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

使用 HashMap、HashTable、ConcurrentHashMap 冷知识:

  • HashMap:key 允许为 null
  • HashTable:key 不允许为 null
  • ConcurrentHashMap:key 不允许为 null


相关文章
|
1月前
|
Python
python3多线程中使用线程睡眠
本文详细介绍了Python3多线程编程中使用线程睡眠的基本方法和应用场景。通过 `time.sleep()`函数,可以使线程暂停执行一段指定的时间,从而控制线程的执行节奏。通过实际示例演示了如何在多线程中使用线程睡眠来实现计数器和下载器功能。希望本文能帮助您更好地理解和应用Python多线程编程,提高程序的并发能力和执行效率。
64 20
|
1月前
|
安全 Java C#
Unity多线程使用(线程池)
在C#中使用线程池需引用`System.Threading`。创建单个线程时,务必在Unity程序停止前关闭线程(如使用`Thread.Abort()`),否则可能导致崩溃。示例代码展示了如何创建和管理线程,确保在线程中执行任务并在主线程中处理结果。完整代码包括线程池队列、主线程检查及线程安全的操作队列管理,确保多线程操作的稳定性和安全性。
|
3月前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
103 3
|
3月前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
91 1
|
4月前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
115 2
java 中 i++ 到底是否线程安全?
|
4月前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
5月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
52 2
|
5月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
91 2
|
4月前
|
数据采集 Java Python
爬取小说资源的Python实践:从单线程到多线程的效率飞跃
本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
139 0
|
1月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
51 17