Java 自增线程安全性测试及解决方案

本文探讨了在多线程环境下i++操作的线程不安全性,并提供了三种使其线程安全的方法:使用synchronized关键字、Lock接口及AtomicInteger类。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

i++线程不安全原因

  • 多个线程同时访问共享变量i,而JVM允许每个线程存储变量的副本,i++的操作可以分为三步: 取值、自增、写回。存在一个线程在 自增 时,刚好有线程在 取值,因此最后会出现i增加的结果总比预计的结果线程小。
  • 测试例:
class TestIPlus {
    private int val = 0;
    public void run() {
        for(int i=0; i<10; i++) {
            this.val = 0;
            final CountDownLatch count = new CountDownLatch(10000);
            for(int j=0; j<100; j++) {
                new Thread(){
                    @Override
                    public void run() {
                        for(int i=0; i<100; i++) {
                            TestIPlus.this.val++;
                            count.countDown();
                        }
                    }
                }.start();
            }
            try {
                count.await();
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.val);
        }
    }
}

使i++变得线程安全有3种方式:

  • 使用synchronized关键字,将i++写成一个方法,并使用synchronized修饰
public synchronized void incI() {
    this.i++;
}
  • 使用Lock,在修改i的位置加锁
private Lock lock = new ReentrantLock();
public void incI() {
    lock.lock();
    try {
        i++;
    } finally {
        lock.unlock();
    }
}
  • 使用原子类AtomicInteger
class TestIPlus {
    private AtomicInteger val;
    public void run() {
        for(int i=0; i<10; i++) {
            this.val = new AtomicInteger(0);
            final CountDownLatch count = new CountDownLatch(10000);
            for(int j=0; j<100; j++) {
                new Thread() {
                    @Override
                    public void run() {
                        for(int i=0; i<100; i++) {
                            // 原子类自增
                            TestIPlus.this.val.getAndIncrement();
                            count.countDown();
                        }
                    }
                }.start();
            }
            try {
                count.await();
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.val);
        }
    }
}
<think>我们正在讨论Java中的线程安全和非线程安全的数据结构。根据提供的引用,我们可以总结如下: ### 线程安全的数据结构(内置同步机制): 1. **Vector**(相对于ArrayList是线程安全的)[^3] 2. **Hashtable**(相对于HashMap是线程安全的) 3. **Stack**(继承自Vector,线程安全) 4. **Concurrent包下的集合类**(如ConcurrentHashMap, CopyOnWriteArrayList等)[^2] 5. **Collections.synchronizedXxx()方法包装的集合**(如Collections.synchronizedList, Collections.synchronizedMap等)[^1] 6. **阻塞队列**(如ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue等)[^2] ### 非线程安全的数据结构(需要外部同步): 1. **ArrayList** [^3] 2. **LinkedList** [^3] 3. **HashMap** 4. **TreeMap** 5. **HashSet** 6. **TreeSet** 7. **PriorityQueue** ### 详细说明: #### 1. 线程安全集合类 - **Vector & Hashtable**: 这些是早期Java版本提供的线程安全集合,通过在每个方法上使用`synchronized`关键字实现同步。但这种方式在高并发场景下性能较差[^3]。 - **Collections.synchronizedXxx()**: 使用装饰器模式将非线程安全集合包装成线程安全版本。例如: ```java List<String> syncList = Collections.synchronizedList(new ArrayList<>()); Map<Integer, String> syncMap = Collections.synchronizedMap(new HashMap<>()); ``` 注意:使用这些集合时,**遍历操作仍需手动同步**: ```java synchronized(syncList) { Iterator<String> it = syncList.iterator(); while(it.hasNext()) { /* ... */ } } ``` - **Concurrent集合(推荐)**: Java 5+引入的`java.util.concurrent`包提供了更高效的线程安全集合: - `ConcurrentHashMap`:分段锁实现高并发访问 - `CopyOnWriteArrayList`:写时复制,适合读多写少场景 - `ArrayBlockingQueue`:基于数组的阻塞队列 - `ConcurrentLinkedQueue`:非阻塞队列 #### 2. 非线程安全集合类 - **ArrayList/LinkedList**: 多线程同时修改会导致`ConcurrentModificationException`或数据不一致[^3]。 - **HashMap**: 并发put可能导致死循环(JDK8已修复死循环问题,但仍可能数据丢失)。 ### 线程安全原理对比 | **实现方式** | **代表类** | **特点** | |--------------------|--------------------------|--------------------------------------------------------------------------| | 方法级`synchronized` | Vector, Hashtable | 简单但性能差,已不推荐使用 | | 装饰器模式 | Collections.synchronized | 需手动同步迭代操作 | | CAS+分段锁 | ConcurrentHashMap | 高并发场景性能优异 | | 写时复制 | CopyOnWriteArrayList | 每次修改创建新数组,读操作无锁 | | 阻塞队列 | ArrayBlockingQueue | 通过ReentrantLock实现阻塞操作 | ### 示例:非线程安全代码 ```java // 线程不安全的操作 List<String> unsafeList = new ArrayList<>(); Runnable task = () -> { for (int i=0; i<1000; i++) { unsafeList.add(Thread.currentThread().getName()); } }; new Thread(task).start(); new Thread(task).start(); // 可能导致元素丢失或ConcurrentModificationException ``` ### 最佳实践建议 1. **读多写少** → `CopyOnWriteArrayList` 2. **高并发Map** → `ConcurrentHashMap` 3. **生产者消费者** → `BlockingQueue`实现类 4. **遗留系统** → `Collections.synchronizedXxx()`+手动同步 --- ### 相关问题 1. 为什么`ConcurrentHashMap`比`Hashtable`性能更好? 2. 如何在遍历`Collections.synchronizedList`时保证线程安全? 3. `CopyOnWriteArrayList`适合哪些场景?有什么缺点? 4. Java中有哪些非阻塞队列实现? [^1]: 使用Collections.synchronizedXxx方法封装线程不安全集合 [^2]: 并发包集合类的适用场景 [^3]: ArrayList与Vector的线程安全性对比 [^4]: 线程安全示例代码分析</think>### Java 线程安全与非线程安全数据结构详解 #### 一、线程安全数据结构(内置同步机制) 1. **传统同步集合** - `Vector`:线程安全的动态数组(每个方法使用 `synchronized`)[^3] - `Hashtable`:线程安全的键值对集合(方法级同步) - `Stack`:继承自 `Vector` 的线程安全栈 2. **并发集合(Java 5+ 推荐)** - `ConcurrentHashMap`:分段锁实现的高并发 Map - `CopyOnWriteArrayList`:写时复制的线程安全列表 - `ArrayBlockingQueue`:基于数组的阻塞队列[^2] - `ConcurrentLinkedQueue`:非阻塞线程安全队列 3. **通过工具类包装** ```java // 将非线程安全集合转为线程安全 List<String> safeList = Collections.synchronizedList(new ArrayList<>()); Map<Integer, String> safeMap = Collections.synchronizedMap(new HashMap<>()); // [^1] ``` #### 二、非线程安全数据结构(需外部同步) 1. **基础集合** - `ArrayList`:动态数组(多线程修改会导致 `ConcurrentModificationException`)[^3] - `HashMap`:哈希表(并发 `put` 可能导致死循环或数据丢失) - `LinkedList`:双向链表(非原子操作导致数据不一致)[^3] 2. **其他常用结构** - `HashSet`:基于 `HashMap` 实现 - `TreeMap`:红黑树实现的排序 Map - `PriorityQueue`:优先级队列 - `StringBuilder`:非线程安全的字符串构建器 #### 三、线程安全原理对比 | **类型** | **实现方式** | **性能影响** | **适用场景** | |------------------|--------------------------|-------------------|--------------------------| | 传统同步集合 | 方法级 `synchronized` | 高锁竞争 | 低并发遗留系统 | | 并发集合 | CAS+分段锁/写时复制 | 中等 | 高并发场景 | | 包装集合 | 对象级监视锁 | 中等 | 简单同步需求 | | 非线程安全集合 | 无内置同步 | 无额外开销 | 单线程或需自定义同步逻辑 | #### 四、典型问题与解决方案 1. **非原子操作问题** ```java // 线程不安全的自操作 int count = 0; count++; // 实际包含"读-改-写"三步操作 ``` **解决方案**: - 使用 `AtomicInteger` - 或同步代码块: ```java synchronized(this) { count++; } // [^4] ``` 2. **复合操作问题** ```java // 线程不安全的"先检查后执行" if (!list.contains("key")) { list.add("key"); // 可能被其他线程打断 } ``` **解决方案**: ```java synchronized(list) { if (!list.contains("key")) { list.add("key"); } } ``` 3. **迭代器遍历问题** 即使使用 `Collections.synchronizedList`,遍历时仍需手动同步: ```java List<String> syncList = Collections.synchronizedList(new ArrayList<>()); // 遍历时必须加锁 synchronized(syncList) { for (String s : syncList) { /* ... */ } } // [^2] ``` #### 五、选型建议 1. **优先选择并发集合** `ConcurrentHashMap` 比 `Collections.synchronizedMap` 性能高 10 倍+(Java 8 基准测试) 2. **考虑使用模式** | **场景** | **推荐选择** | |-------------------------|--------------------------| | 读多写少 | `CopyOnWriteArrayList` | | 高频更新 | `ConcurrentHashMap` | | 生产者-消费者模式 | `BlockingQueue` 实现类 | | 简单同步需求 | `Collections.synchronizedXxx` | 3. **避免使用已弃用类** - 不要使用古老的 `Vector`/`Hashtable`(JDK 1.0) - 替换 `StringBuffer` 为 `StringBuilder`(单线程场景) --- ### 相关问题 1. 为什么 `ConcurrentHashMap` 比 `Collections.synchronizedMap` 性能更好? 2. 如何在 `CopyOnWriteArrayList` 中保证迭代过程中数据一致性? 3. `ConcurrentHashMap` 的锁分段机制是如何实现的? 4. 什么场景下应该选择 `BlockingQueue` 而不是普通队列? [^1]: 使用 `Collections.synchronizedXxx` 封装线程不安全集合 [^2]: 并发包集合类的适用场景与注意事项 [^3]: `ArrayList` 与 `Vector` 的线程安全性对比 [^4]: 同步代码块解决原子性问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值