如何保证集合是线程安全的?

本文介绍了Java中线程安全容器的实现方式,包括传统的同步容器和并发包中的高性能并发容器,如ConcurrentHashMap。分析了ConcurrentHashMap如何通过分离锁和优化设计提高并发性能。

典型回答

Java提供了不同层面的线程安全支持。在传统集合框架内部,除了Hashtable等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),可以调用Collections工具类提供的包装方法,来获取一个同步的包装容器,例如Collections.synchronizedMap()。但是它们都是利用非常粗粒度的同步方式,在高并发情况下的性能比较低下。

另外,更加普遍的选择是利用并发包(java.util.concurrent)提供的线程安全容器类:

  • 各种并发容器,比如ConcurrentHashMap、CopyOnWriteArrayList。
  • 各种线程安全队列(Queue/Deque),比如ArrayBlockingQueue、SynchronousQueue。
  • 各种有序容器的线程安全版本等。

具体保证线程安全的方式,包括有从简单的synchronized方式,到基于更加精细化的,比如基于分离锁实现的ConcurrentHashMap等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远远优于早期的简单同步实现。

知识扩展

1、为什么需要ConcurrentHashMap?

首先,Hashtable本身比较低效,因为它的实现基本就是将put、get、size等方法简单粗暴地加上“synchronized”。这就导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其它线程只能等待,大大降低了并发操作的性能。

上一讲已经提到HashMap不是线程安全的,并发情况或导致类似CPU占用100%等一些问题。

那么能不能利用Collections提供的同步包装器来解决问题呢?以下代码片段摘自Collections:

private static class SynchronizedMap<K,V>
  implements Map<K,V>, Serializable {
  private final Map<K,V> m; // Backing Map
  final Object mutex;  // Object on which to synchronize
  // ...
  public int size() {
    synchronized (mutex) {return m.size();}
  }
}

我们发现同步包装器指示利用输入Map构造了另一个同步版本,所有操作虽然不再声明成为synchronized方法,但是还是利用了“this”作为互斥的mutex,没有真正意义上的改进!

所以,Hashtable或者同步包装版本都只适合在非高度并发的场景下。

2、ConcurrentHashMap分析

再来看看ConcurrentHashMap是如何设计实现的,为什么它能大大提供并发效率。

首先需要强调,ConcurrentHashMap的设计实现其实一直在演化,比如在Java 8中就发生了非常大的变化。

早期的ConcurrentHashMap其实现是基于:

  • 分离锁,也就是将内部进行分段(Segment),里面则是HashEntry的数组。和HashMap类似,哈希相同的条目也是以链表形式存放。
  • HashEntry内部使用volatile的value字段来保证可见性,也利用了不可变对象的机制以改进利用sun.misc.Unsafe提供的底层能力,比如volatile access,去直接完成部分操作,以最优化性能。毕竟Unsafe中的很多操作都是JVM intrinsic优化过的。

参考下面这个早期ConcurrentHashMap内部结构示意图,其核心是利用分段设立,在进行并发操作的时候,只需要锁定相应段,这样就有效避免了类似Hashtable整体同步的问题,大大提高了性能。

在构造的时候,Segment的数量由所谓的concurrentcyLevel决定,默认是16,也可以在相应构造函数直接指定。注意,Java需要它是2的整数次幂,例如输入15,将会被自动调整到16。

分离锁看似完美,但是在size方法实现时会有副作用。试想,如果不进行同步,简单的计算所有Segment的总值,可能会因为并发put,导致结果不准确;但是直接锁定所有Segment进行计算,就会变得非常昂贵。其实,分离锁也给包括Map初始化等操作带来类似的副作用。

所以,ConcurrentHashMap的实现是通过重试机制(RETRIES_BEFORE_LOCK,指定重复次数2),来试图获得可靠值。如果没有监控到发生变化,就直接返回,否则将获取锁进行操作。

3、Java 8对ConcurrentHashMap的改进

  • 总体结构上,它的内部存储变得和上一讲中介绍的HashMap结构非常相似,同样是大的桶数组,然后内部也是一个个链表结果,同步的粒度要更细致一些。
  • 其内部仍然有Segment定义,但仅仅是为了保证序列化时的兼容性而已,不再有任何结构上的用处。
  • 因为不再使用Segment,初始化操作大大简化,修改为lazy-load形式。这样可以有效避免初始开销,解决了老版本很多人抱怨的这一点。
  • 数据存储利用volatile来保证可见性。
  • 使用CAS(Compare And Swap)等操作,在特定场景进行无锁并发操作。
  • 使用Unsafe、LongAdder之类底层手段,进行极端情况的优化。

【完】

### 什么是线程安全线程安全是指在多线程环境中,一个类、方法或数据结构能够在多个线程同时访问的情况下,仍然保持其行为的正确性和一致性,不会因为线程的并发执行而导致数据不一致、状态错误或程序崩溃等问题。线程安全通常要求对象在并发访问时能够正确地处理共享状态,确保数据的完整性与一致性[^5]。 根据线程安全的不同层次,可以将其分为以下几种类型: - **绝对线程安全**:无论运行时环境如何,调用者都不需要任何额外的同步措施,对象的行为始终是正确的。 - **相对线程安全**:这是通常意义上的线程安全,要求对象的操作在并发环境中是安全的,例如 `Vector`、`Hashtable` 和 `Collections.synchronizedList()` 等。 - **线程兼容**:对象本身不是线程安全的,但可以通过外部同步手段(如 `synchronized` 或 `ReentrantLock`)实现线程安全,例如 `ArrayList` 和 `HashMap`。 - **线程对立**:无论调用端是否采用同步措施,都无法在并发环境中安全使用的代码[^5]。 ### 实现线程安全的方法 实现线程安全的核心方法包括以下几种: #### 1. 避免共享状态 通过避免多个线程共享同一份数据,可以从根本上消除线程安全问题。例如,使用局部变量或线程私有变量(如 `ThreadLocal`)可以确保数据只被当前线程访问,从而避免并发问题。如果多个线程操作的是独立的局部变量,则无需额外的同步机制,局部变量是线程安全的[^3]。 #### 2. 使用不可变对象 不可变对象一旦创建后其状态就不能被修改,因此天然具备线程安全性。例如,Java 中的 `String` 和 `BigInteger` 类型是不可变的,可以在多线程环境下安全使用。此外,使用 `final` 修饰基本类型或不可变对象也能保证线程安全[^5]。 #### 3. 使用同步机制 当多个线程需要访问共享资源时,必须通过同步机制来保证访问的原子性、可见性和有序性。常见的同步方式包括: - **synchronized 关键字**:用于方法或代码块,确保同一时间只有一个线程可以执行该段代码。 - **显式锁(ReentrantLock)**:提供比 `synchronized` 更灵活的锁机制,支持尝试锁、超时等功能。 - **原子变量(如 AtomicInteger、AtomicLong)**:使用 CAS(Compare and Swap)操作保证变量操作的原子性。 以下是一个使用 `synchronized` 实现线程安全的例子: ```java public class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } } ``` 在这个例子中,`increment` 和 `getCount` 方法被 `synchronized` 修饰,确保了多个线程访问 `count` 变量时的线程安全性[^1]。 #### 4. 使用并发集合类 Java 提供了多种线程安全集合类,如 `ConcurrentHashMap`、`CopyOnWriteArrayList` 等,它们在设计时就考虑了并发访问的性能与安全性,适用于高并发场景。这些集合类相比传统的同步包装类(如 `Collections.synchronizedList()`)在性能上有显著提升[^2]。 #### 5. 使用线程安全的函数式设计 在函数式编程中,尽量避免可变状态,使用纯函数和不可变数据结构可以简化并发控制。例如,在 Java 中可以使用 `Stream` API 和 `Optional` 类来减少共享状态的使用,从而提高线程安全性。 ### 总结 线程安全的核心在于如何处理共享状态和并发访问。通过避免共享、使用不可变性、引入同步机制以及使用并发集合类等方法,可以有效确保多线程环境下的程序稳定性与正确性。不同的线程安全级别适用于不同的场景,开发者应根据实际需求选择合适的实现方式。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值