Java同步容器与并发容器的区别(新手必看)
在 Java 集合框架中,主要有 List、Set、Map 和 Queue 几大类并发容器,它们的大多数实现默认都是非线程安全的,比如 ArrayList、LinkedList、HashMap 等容器都是非线程安全的。
在多线程环境中,使用这些非线程安全的容器就会出现问题,需要我们在编写程序时手动处理,非常不方便,因此 Java 提供了一些同步容器供开发者使用。
同步容器是设计用来在并发环境下安全使用的数据结构,内置了多线程操作所需的同步措施。这使得开发者能够在多线程程序中直接使用这些容器,而无须担心线程安全问题。
同步容器通常分为两种类型,分别是普通同步类和通过 Collections 类包装的内部同步类。
比如 Vector 容器中的 get() 和 set() 方法的源码定义如下:
SynchronizedCollection 部分源码定义如下:
通过对同步容器的介绍,我们发现同步容器在性能上存在不足,由于对所有操作都添加了同步锁,对于需要多步骤的复合操作,同步容器有时候并不能保证线程安全。
为了解决同步容器的这些问题,Java 提供了多种并发容器,例如 ConcurrentHashMap、CopyOnWriteArrayList 等。这些容器利用分段锁、无锁 CAS 算法等技术,将对共享资源的串行操作转换为并行操作,从而显著提升性能。
以 ConcurrentHashMap 和 HashTable 为例,虽然它们都是线程安全的 Map 集合,但是 ConcurrentHashMap 的性能比 HashTable 的性能要高,原因如下:
通过对比可知,同步容器在多线程环境中提供了基本的线程安全,但在高并发情况下可能会成为性能瓶颈。相反,并发容器通过锁的精细化和其他高级并发技术,提供了更好的性能和可伸缩性。
在实际应用中,应根据实际场景选择合适的容器类型。
在多线程环境中,使用这些非线程安全的容器就会出现问题,需要我们在编写程序时手动处理,非常不方便,因此 Java 提供了一些同步容器供开发者使用。
同步容器是设计用来在并发环境下安全使用的数据结构,内置了多线程操作所需的同步措施。这使得开发者能够在多线程程序中直接使用这些容器,而无须担心线程安全问题。
同步容器通常分为两种类型,分别是普通同步类和通过 Collections 类包装的内部同步类。
Java同步容器
1) 普通同步类
普通同步类有 Vector、Stack 和 HashTable,它们依靠给每个关键方法加上 synchronized 关键字来实现线程安全。比如 Vector 容器中的 get() 和 set() 方法的源码定义如下:
public synchronized E get(int index) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); return elementData(index); } public synchronized E set(int index, E element) { if (index >= elementCount) throw new ArrayIndexOutOfBoundsException(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }使用 synchronized 关键字可以使每次只有一个线程能够执行这些同步的方法,从而保证操作的原子性。
2) 内部同步类
内部同步类同步容器是通过 Collections 类创建的,通过 SynchronizedList() 和 SynchronizedSet() 方法,将原始容器包装成内部同步类 SynchronizedCollection 对象,并在关键代码块上利用 synchronized 锁定实现同步,从而创建出线程安全的容器。SynchronizedCollection 部分源码定义如下:
static class SynchronizedCollection<E> implements Collection<E>, Serializable { // 内部实现细节 public boolean add(E e) { synchronized (mutex) { return c.add(e); } } // 其他方法 }从源码中可以发现,在 SynchronizedCollection 内部同步类中,通过为操作指定一个锁对象 mutex 来保证方法的线程同步。
通过对同步容器的介绍,我们发现同步容器在性能上存在不足,由于对所有操作都添加了同步锁,对于需要多步骤的复合操作,同步容器有时候并不能保证线程安全。
为了解决同步容器的这些问题,Java 提供了多种并发容器,例如 ConcurrentHashMap、CopyOnWriteArrayList 等。这些容器利用分段锁、无锁 CAS 算法等技术,将对共享资源的串行操作转换为并行操作,从而显著提升性能。
以 ConcurrentHashMap 和 HashTable 为例,虽然它们都是线程安全的 Map 集合,但是 ConcurrentHashMap 的性能比 HashTable 的性能要高,原因如下:
- HashTable 是同步容器,采用 synchronized 关键字锁定实现同步,其本质是对整张 HashTable 进行锁定,每次锁定整张表让线程独占,因此 HashTable 保证线程安全其实是以性能损耗为代价的。
- ConcurrentHashMap 是 JUC 提供的并发容器,它使用一种称为“分区锁定”或“段锁定”的技术。该技术将内部数据结构分为一定数量的段(Segment),每个段本质上是一个小的 HashMap,并且拥有自己的锁。默认情况下,这些段的数量是 16,也就是说,整个 ConcurrentHashMap 可以有 16 把锁,从而减小锁的粒度,提高并发访问的效率。
- 在较新的 Java 版本中,设计者对 ConcurrentHashMap 进行了重新设计和优化,去除了原有的段结构,改为在节点级别上使用锁,锁的粒度更小了,性能也得到了进一步提升。
Java同步容器和并发容器的区别
同步容器和并发容器都是线程安全的,但是它们在设计和性能特征上有显著差异。以下是两者的主要区别。1) 锁的粒度
- 同步容器:通常在方法级别进行同步,这种方法简单但会导致较大的性能开销,特别是在高并发场景中。
- 并发容器:使用更精细的锁定策略(如分段锁定或无锁技术)来减少线程间的竞争。
2) 并发性能
- 同步容器:由于使用了重量级的锁定机制,当多个线程频繁访问容器时,同步容器的性能可能会显著下降。
- 并发容器:通常能提供更高的并发级别和更好的性能,特别是当读取操作远多于写入操作时。
3) 设计目的
- 同步容器:设计于早期的 Java 版本中,主要解决单线程容器在多线程环境中的线程安全问题。
- 并发容器:专为满足现代多核、并发编程的需求而设计,提供更强大的线程安全性能。
4) 内存一致性影响
- 同步容器:保证内存一致性是通过在每个方法上使用 synchronized 键字来实现的。
- 并发容器:使用了更复杂的机制来保证操作的原子性和内存可见性,例如使用 volatile 变量、CAS 操作和内部锁机制。
通过对比可知,同步容器在多线程环境中提供了基本的线程安全,但在高并发情况下可能会成为性能瓶颈。相反,并发容器通过锁的精细化和其他高级并发技术,提供了更好的性能和可伸缩性。
在实际应用中,应根据实际场景选择合适的容器类型。