Java 中的线程安全容器

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 常用的一些容器例如 ArrayList、HashMap、都不是线程安全的,最简单的将这些容器变为线程安全的方式,是给这些容器所有的方法都加上 synchronized 关键字。

一、同步容器

常用的一些容器例如 ArrayList、HashMap、都不是线程安全的,最简单的将这些容器变为线程安全的方式,是给这些容器所有的方法都加上 synchronized 关键字。

Java 的 Collections 中实现了这些同步容器:

简单的使用如下:

List<String> list = Collections.synchronizedList(newArrayList<>());


Map<Integer, String> map = Collections.synchronizedMap(newHashMap<>());


Set<String> set = Collections.synchronizedSet(newHashSet<>());

同步容器虽然简单,但是相应的效率较低,因为锁的粒度较大。

循环遍历同步容器

如果在遍历同步容器的时候,组合了多个方法,这会可能会存在竞态条件,仍然不是线程安全的。解决的办法便是对容器加锁。例如下面这样:

public static void main(String[] args) {
    List<String> list = Collections.synchronizedList(new ArrayList<>());
    //省略添加数据的操作
    String[] str = new String[list.size()];
    int k = 0;
    synchronized (list){
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            str[k ++] = iterator.next();
        }
    }
}


二、并发容器


Java 中还提供了一系列并发容器,相比于同步容器,其性能更好。并发容器共分为了四类:List、Map、Set、Queue。

1. List

List 中一个最主要的实现类是 CopyOnWriteArrayList ,CopyOnWrite,即写时复制,这样的好处是读操作是无锁的。

其实现原理是内部维护了一个数组,内部变量 array 指向了这个数组。需要写时,并不是在原数组上操作,而是将数组复制一份,在拷贝的数组中进行写。完成后,将 array 指向新的数组。这样一来,读写之间不互斥,效率得到了很大的提升。

需要注意的是 CopyOnWriteArrayList 适用于读多写少的场景,并且需要接受读写的暂时不一致,因为在写的时候,并行的读操作可能并不能马上看到写的结果。

2. Map

Map 的两个主要实现类是 ConcurrentHashMapConcurrentSkipListMap,两者主要的区别是:前者是无序的,后者是有序的。

2.1 ConcurrentHashMap

在 Java 1.7 中,ConcurrentHashMap 的实现使用的是分段锁技术,其内部主要的数据结构是 Segment 和 HashEntry,ConcurrentHashMap 包含了一个 Segment 数组,每个 Segment 又包含一个 HashEntry 数组,每个 HashEntry 是一个存储数据的链表结构。

其中 Segment 继承了 ReentrantLock,每个 Segment 都有对应的锁,需要修改数据的时候,需要获取这把锁。修改不同的 Segment 数据,则完全可以并行,效率得到了提升。示意图如下:

Java 1.8 又对 ConcurrentHashMap 做了较大的改进,放弃了分段锁的技术。结构和 Java 1.8 中的 HashMap 类似,采用的是数组+链表/红黑树来实现。为了降低哈希冲突的成本,在链表长度超过 8 时,将链表转换为红黑树。使用 CAS 和 synchronized 解决并发问题,锁住链表或者红黑树的头节点,只要没有哈希冲突,则不会出现并发问题。示意图如下:

2.2 ConcurrentSkipListMap

ConcurrentSkipListMap 保证有序的主要原因是,底层使用的是跳表这种数据结构,关于跳表的介绍,你可以查看数据结构中的内容。


3. Set


Set 的两个实现是 CopyOnWriteArraySetConcurrentSkipListSet

和前面说到的 CopyOnWriteArrayList 、ConcurrentSkipListMap 实现的原理类似。


4. Queue


队列可以从两方面进行分类:

  • 单端和双端:单端队列指的是只能在队首出队,队尾出队,而双端队列指的是队首和队尾均可入队和出队。
  • 阻塞和非阻塞:阻塞队列指的是,当队列满的时候,入队列阻塞;当队列空的时候,出队列阻塞。

Java 中,单端队列使用 queue 标识,双端队列使用 deque 标识。

4.1 单端阻塞队列

常用的实现类有:ArrayBlockingQueueLinkedBlockingQueuePriorityQueue

4.2 单端非阻塞队列

其实现类是 ConcurrentLinkedQueue

4.3 双端阻塞队列

其实现类是 LinkedBlockingDeque

4.4 双端非阻塞队列

其实现类是 ConcurrentLinkedDeque

使用其实都非常地简单,就是入队出队之类的操作,这里不再赘述了。

相关文章
|
4天前
|
Java 程序员
从菜鸟到大神:JAVA多线程通信的wait()、notify()、notifyAll()之旅
【6月更文挑战第21天】Java多线程核心在于wait(), notify(), notifyAll(),它们用于线程间通信与同步,确保数据一致性。wait()让线程释放锁并等待,notify()唤醒一个等待线程,notifyAll()唤醒所有线程。这些方法在解决生产者-消费者问题等场景中扮演关键角色,是程序员从新手到专家进阶的必经之路。通过学习和实践,每个程序员都能在多线程编程的挑战中成长。
|
4天前
|
Java
并发编程的艺术:Java线程与锁机制探索
【6月更文挑战第21天】**并发编程的艺术:Java线程与锁机制探索** 在多核时代,掌握并发编程至关重要。本文探讨Java中线程创建(`Thread`或`Runnable`)、线程同步(`synchronized`关键字与`Lock`接口)及线程池(`ExecutorService`)的使用。同时,警惕并发问题,如死锁和饥饿,遵循最佳实践以确保应用的高效和健壮。
11 2
|
3天前
|
Java
Java Socket编程与多线程:提升客户端-服务器通信的并发性能
【6月更文挑战第21天】Java网络编程中,Socket结合多线程提升并发性能,服务器对每个客户端连接启动新线程处理,如示例所示,实现每个客户端的独立操作。多线程利用多核处理器能力,避免串行等待,提升响应速度。防止死锁需减少共享资源,统一锁定顺序,使用超时和重试策略。使用synchronized、ReentrantLock等维持数据一致性。多线程带来性能提升的同时,也伴随复杂性和挑战。
|
1天前
|
Java
Java中,有两种主要的方式来创建和管理线程:`Thread`类和`Runnable`接口。
【6月更文挑战第24天】Java创建线程有两种方式:`Thread`类和`Runnable`接口。`Thread`直接继承受限于单继承,适合简单情况;`Runnable`实现接口可多继承,利于资源共享和任务复用。推荐使用`Runnable`以提高灵活性。启动线程需调用`start()`,`Thread`直接启动,`Runnable`需通过`Thread`实例启动。根据项目需求选择适当方式。
9 2
|
1天前
|
Java
在Java中,死锁是指两个或多个线程互相等待对方释放资源,从而导致所有线程都无法继续执行的情况。
【6月更文挑战第24天】在Java并发中,死锁是多线程互相等待资源导致的僵局。避免死锁的关键策略包括:防止锁嵌套,设定固定的加锁顺序,使用`tryLock`带超时,避免无限等待,减少锁的持有时间,利用高级同步工具如`java.util.concurrent`,以及实施死锁检测和恢复机制。通过这些方法,可以提升程序的并发安全性。
6 1
|
3天前
|
安全 Java 程序员
Java多线程详解
Java多线程详解
|
4天前
|
缓存 安全 Java
Java线程面试题含答案
Java线程面试题含答案
|
4天前
|
算法 安全 网络协议
java高级面试题_java面试题大全带答案_线程面试题_java面试宝典2019
java高级面试题_java面试题大全带答案_线程面试题_java面试宝典2019
|
4天前
|
安全 算法 Java
java线程面试题_2019java面试题库
java线程面试题_2019java面试题库
|
3天前
|
Java API