JUC多线程:集合线程安全全解析

文章提示

  • 🔍 重点掌握:线程不安全现象的表现形式及底层原理

  • ⚠️ 避坑指南:高并发场景下直接使用非线程安全集合的风险

  • 🛠️ 实战技巧:通过代码案例快速验证不同解决方案的效果

  • 💡 拓展思维:结合JUC包理解Java并发设计的哲学


目录

文章提示

前言

一、ArrayList的线程安全问题与解决方案

1.1 问题现象演示

1.2 解决方案对比

方案1:Vector(同步方法)

方案2:Collections.synchronizedList

方案3:CopyOnWriteArrayList(写时复制)

二、HashSet线程安全问题与解决方案

2.1 问题复现

2.2 解决方案

三、HashMap线程安全问题与解决方案

3.1 并发问题表现

3.2 解决方案

四、解决方案对比总结

文章总结与升华

📚 核心要点回顾

🚀 技术延伸

💡 学习建议


前言

        在多线程编程中,集合的线程安全问题是引发生产事故的"隐形炸弹"。本文将以ArrayListHashSetHashMap三大常用集合为切入点,通过代码实例演示线程不安全现象,深度解析VectorCollections.synchronizedListCopyOnWriteArrayList等解决方案的实现原理。无论你是刚接触并发编程的新手,还是需要巩固知识的老手,本文都将为你提供清晰易懂的实践指南。

一、ArrayList的线程安全问题与解决方案

1.1 问题现象演示

package JUC.Usafe;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class ArrayListDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        // 创建10个线程并发添加元素
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                //向集合还行添加内容
                list.add(UUID.randomUUID().toString().substring(0, 8));
                //打印集合内容
                System.out.println(list);
            },String.valueOf(i)).start();
        }

    }
}

运行结果分析

问题根源

  1. 数据覆盖:多个线程同时执行add()导致elementData数组越界

  2. 扩容竞争:当需要扩容时,size的可见性问题导致数组越界

java.util.ConcurrentModificationException 是由于在多线程环境下,一个线程正在对集合进行迭代(如 System.out.println(list) 的内部实现会调用 toString() 方法),而另一个线程同时修改了集合的内容(如 list.add() 操作)。这种情况下,ArrayList 的迭代器检测到集合被修改后抛出了此异常。
ArrayList 并不是线程安全的集合类。在多线程环境中,如果需要对集合进行并发操作,必须采取适当的同步机制或使用线程安全的集合类。

1.2 解决方案对比

方案1:Vector(同步方法)
List<String> list = new Vector<>();

 实现原理

  • 所有方法都添加synchronized关键字

  • 默认扩容2倍(ArrayList是1.5倍)

优缺点

  • ✅ 保证强一致性

  • ❌ 锁粒度大,性能差(吞吐量约下降60%)


方案2:Collections.synchronizedList
List<String> list = Collections.synchronizedList(new ArrayList<>());
import java.util.*;
import java.util.concurrent.*;

public class ArrayListDemo {
    public static void main(String[] args) {
        // 使用 synchronizedList 包装 ArrayList
        List<String> list = Collections.synchronizedList(new ArrayList<>());

        // 创建10个线程并发添加元素
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                // 向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0, 8));

                // 打印集合内容时需要显式加锁
                synchronized (list) {
                    System.out.println(list);
                }
            }, String.valueOf(i)).start();
        }
    }
}

实现特点

  • 使用同步代码块包裹所有操作

  • 通过mutex对象作为锁

源码:

public boolean add(E e) {
    synchronized (mutex) {
        return c.add(e);
    }
}

适用场景

  • 需要兼容老代码的改造

  • 对性能要求不高的场景


方案3:CopyOnWriteArrayList(写时复制)

写时复制是指:在并发访问的情景下,当需要修改JAVA中Containers的元素时,不直接修改该容器,而是先复制一份副本,在副本上进行修改。修改完成之后,将指向原来容器的引用指向新的容器(副本容器)。

List<String> list = new CopyOnWriteArrayList<>();
import java.util.*;
import java.util.concurrent.*;

public class ArrayListDemo {
    public static void main(String[] args) {
        // 使用 CopyOnWriteArrayList 替代 ArrayList
        List<String> list = new CopyOnWriteArrayList<>();

        // 创建10个线程并发添加元素
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                // 向集合中添加内容
                list.add(UUID.randomUUID().toString().substring(0, 8));

                // 打印集合内容
                System.out.println(list);
            }, String.valueOf(i)).start();
        }
    }
}

 核心原理

  1. 写操作时复制新数组(ReentrantLock保证单线程写

①由于不会修改原始容器,只修改副本容器(JMM原理)。因此,可以对原始容器进行并发地读。其次,实现了读操作与写操作的分离,读操作发生在原始容器上,写操作发生在副本容器上。
②数据一致性问题:读操作的线程可能不会立即读取到新修改的数据,因为修改操作发生在副本上。但最终修改操作会完成并更新容器,因此这是最终一致性。

        2.修改完成后将引用指向新数组

源码:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

性能优势

  • 读操作完全无锁(适合读多写少场景)

  • 写操作通过复制避免数据竞争

应用场景:

CopyOnWrite容器适用于读多写少的场景。因为写操作时,需要复制一个容器,造成内存开销很大,也需要根据实际应用把握初始容器的大小。
不适合于数据的强一致性场合。若要求数据修改之后立即能被读到,则不能用写时复制技术。因为它是最终一致性。

4,为什么会出现COW?

集合类(ArrayList、HashMap)上的常用操作是:向集合中添加元素、删除元素、遍历集合中的元素然后进行某种操作。当多个线程并发地对一个集合对象执行这些操作时就会引发ConcurrentModificationException,比如线程A在for-each中遍历ArrayList,而线程B同时又在删除ArrayList中的元素,就可能会抛出ConcurrentModificationException,可以在线程A遍历ArrayList时加锁,但由于遍历操作是一种常见的操作,加锁之后会影响程序的性能,因此for-each遍历选择了不对ArrayList加锁而是当有多个线程修改ArrayList时抛出ConcurrentModificationException,因此,这是一种设计上的权衡。
为了应对多线程并发修改这种情况,一种策略就是本文的主题“写时复制”机制;另一种策略是:线程安全的容器类:
ArrayList—>CopyOnWriteArrayList
HashSet—>CopyOnWriteHashSet
HashMap—>ConcurrentHashMap
而ConcurrentHashMap并不是从“复制”这个角度来应对多线程并发修改,而是引入了分段锁(JDK7);CAS解决多线程并发修改的问题。

package JUC.Usafe;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteList {
    public static void main(String[] args) {
    // 并发下 ArrayList 不安全的吗,Synchronized;
    /**
     * 解决方案;
     * 1、List<String> list = new Vector<>();
     * 2、List<String> list = Collections.synchronizedList(new ArrayList<>
     ());
     * 3、List<String> list = new CopyOnWriteArrayList<>();
     */
    // CopyOnWrite 写入时复制 COW 计算机程序设计领域的一种优化策略;
    // 多个线程调用的时候,list,读取的时候,固定的,写入(覆盖)
    // 在写入的时候避免覆盖,造成数据问题!
    // 读写分离
    // CopyOnWriteArrayList 比 Vector Nb 在哪里?
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(list);
            },String.valueOf(i)).start();
        }
    }
}

二、HashSet线程安全问题与解决方案

2.1 问题复现

Set<String> set = new HashSet<>();
// 多线程并发add操作(代码类似ArrayList示例)

底层原理

  • HashSet实际使用HashMap存储元素

public boolean add(E e) {
    return map.put(e, PRESENT)==null; // PRESENT是固定Object对象
}

2.2 解决方案

Set<String> set = new CopyOnWriteArraySet<>();

实现特点

  • 基于CopyOnWriteArrayList实现

  • 通过addIfAbsent()保证元素唯一性

package JUC.Usafe;

import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
/**
 * 同理可证 : ConcurrentModificationException
 * //1、Set<String> set = Collections.synchronizedSet(new HashSet<>());
 * //2、CopyOnWriteArraySet
 */
public class CopyOnWriteSet {
    public static void main(String[] args) {
// Set<String> set = new HashSet<>();
// Set<String> set = Collections.synchronizedSet(new HashSet<>());
        Set<String> set = new CopyOnWriteArraySet<>();
        for (int i = 1; i <=30 ; i++) {
            new Thread(()->{
                set.add(UUID.randomUUID().toString().substring(0,5));
                System.out.println(set);
            },String.valueOf(i)).start();
        }
    }
}

三、HashMap线程安全问题与解决方案

3.1 并发问题表现

  • JDK1.7及之前:环形链表导致CPU 100%

  • JDK1.8:数据覆盖或size计算错误

3.2 解决方案

Map:

Map<String, Object> map = new ConcurrentHashMap<>();
package JUC.Usafe;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class MapTest {
    public static void main(String[] args) {
    // map 是这样用的吗? 不是,工作中不用 HashMap
    // 默认等价于什么? new HashMap<>(16,0.75);
    // Map<String, String> map = new HashMap<>();
    // 唯一的一个家庭作业:研究ConcurrentHashMap的原理
        Map<String, String> map = new ConcurrentHashMap<>();
        for (int i = 1; i <=30; i++) {
            new Thread(()->{
                map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(
                        0,5));
                System.out.println(map);
            },String.valueOf(i)).start();
        }
    }
}

JDK1.7实现

  • 分段锁(Segment继承ReentrantLock)

  • 默认16个段,支持16个并发写

JDK1.8优化

  • 改用Node + synchronized + CAS

  • 链表转红黑树优化查询效率

四、解决方案对比总结

方案锁粒度适用场景吞吐量数据一致性
Vector方法级同步兼容老系统强一致
synchronizedList代码块同步简单并发场景强一致
CopyOnWriteArrayList写时复制读多写少(配置信息等)最终一致
ConcurrentHashMap分段锁/CAS高频并发写入(缓存系统)极高弱一致

文章总结与升华

📚 核心要点回顾

  1. 问题本质:集合线程不安全的核心在于共享数据的可见性与原子性

    • 示例:两个线程同时执行list.add()导致size计数错误

  2. 解决方案选择标准

    • 根据读写比例选择(如CopyOnWrite适合读多写少)

    • 根据数据一致性要求选择(强一致 vs 最终一致)

  3. JUC设计哲学

    • 通过空间换时间(CopyOnWrite)

    • 减小锁粒度提升并发度(ConcurrentHashMap分段锁)

🚀 技术延伸

  1. 写时复制(CopyOnWrite)的衍生应用

    • Docker镜像分层存储

    • Redis持久化中的COW机制

    // 自定义简易COW实现
    class SimpleCOWList<T> {
        private volatile Object[] array = new Object[0];
        
        public synchronized void add(T item) {
            Object[] newArray = Arrays.copyOf(array, array.length + 1);
            newArray[array.length] = item;
            array = newArray;
        }
        
        public T get(int index) {
            return (T) array[index]; // 无锁读
        }
    }
  2. ConcurrentHashMap的进阶用法

    • 使用computeIfAbsent实现缓存

    ConcurrentHashMap<String, Connection> connectionPool = new ConcurrentHashMap<>();
    
    public Connection getConnection(String key) {
        return connectionPool.computeIfAbsent(key, k -> createConnection(k));
    }
  3. 锁的演进趋势

    • synchronizedReentrantLock

    • 从分段锁到CAS+synchronized优化

    • 未来趋势:无锁编程(如LongAdder)

💡 学习建议

  1. 实践验证:使用javap -c反编译查看synchronized的实现

  2. 性能测试:使用JMH对比不同方案的吞吐量差异

  3. 源码学习:重点研究CopyOnWriteArrayList的迭代器实现

读者不仅能够理解集合线程安全的核心原理,还能根据实际业务场景选择合适的并发容器,为构建高并发系统打下坚实基础

参考:

写时复制技术的实现与介绍(Copy-On-Write)_copy on write 最终一致性-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值