在先前的文章中我们已经讲过了原子类(线程安全的基本类型,基于CAS实现),详见常见锁策略,synchronized内部原理以及CAS-CSDN博客 ,我们在来讲一下集合类,在原来的集合类,大多数是线程不安全的,虽然vector,Stack,HashTable 是线程安全的,但由于其在一些关键方法上都加了synchronized,导致同时读以及单线程中也要加锁,于是java官方已经将其标为不安全类,不建议使用了,那我们如何在多线程下保证安全的使用一些集合类。
多线程环境下使用ArrayList
1)自己使用同步机制(synchronized或者ReentrantLock)具体内容可以看这里Java线程安全问题以及解决方案-CSDN博客
2)使用Collections.synchronizedList()
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
用这种方式创建出的synchronizedList在关键方法上都具有synchronized
3)使用CopyOnWriteArrayList
CopyOnWriteArrayList
是 Java 中的一种并发集合类,它提供了一种在迭代时保证线程安全的机制。它的特点是在对集合进行修改(添加、删除元素)时,不直接在原始数据上进行操作,而是先将原始数据复制一份,然后在副本上进行修改,最后再将修改后的副本替换原始数据,且同时写时也会创建出两个拷贝。这种机制保证了在迭代过程中不会发生并发修改异常(ConcurrentModificationException
),因为每次迭代都是在集合的一个固定的副本上进行的。
import java.util.concurrent.CopyOnWriteArrayList;
public class ConcurrentListExample {
private static final int THREAD_COUNT = 3;
private static final int OPERATIONS_PER_THREAD = 10000;
private static CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
public static void main(String[] args) {
// 创建并启动多个线程进行写操作
for (int i = 0; i < THREAD_COUNT; i++) {
Thread writerThread = new Thread(() -> {
for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
list.add(j);
}
});
writerThread.start();
}
// 创建并启动多个线程进行读操作
for (int i = 0; i < THREAD_COUNT; i++) {
Thread readerThread = new Thread(() -> {
for (int j = 0; j < OPERATIONS_PER_THREAD; j++) {
int size = list.size(); // 读取列表大小
System.out.println("List size: " + size);
try {
Thread.sleep(10); // 模拟其他处理
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
readerThread.start();
}
}
}
列表大小的变化:读线程会定期读取列表的大小并打印出来。由于写线程在不断地往列表中添加元素,因此列表的大小会逐渐增加。读线程每次读取到的列表大小可能会不同,取决于它在列表大小被修改之前或之后读取到的。读线程的竞争:由于多个读线程同时进行,它们可能会竞争访问 CopyOnWriteArrayList
的大小。因此输出结果中可能会出现多个读线程同时读取列表大小并打印的情况。写线程的竞争:多个写线程同时向 CopyOnWriteArrayList
中添加元素,它们之间也可能存在竞争。因此,列表中的元素可能会以不确定的顺序被添加。
如何安全地使用 CopyOnWriteArrayList
:
-
并发读写安全:
CopyOnWriteArrayList
在迭代时提供了线程安全的保证,因此可以安全地在多个线程中进行读操作和写操作,而无需额外的同步控制。 -
适用场景:适用于读操作远远多于写操作的场景,因为每次写操作都会触发一次数组的拷贝,可能会带来一定的性能开销。
-
实时性:需要注意,由于写操作会对原始数据进行拷贝,因此在写操作完成之前,迭代器可能会遍历到老的数据。如果需要实时性较高的结果,可能需要其他方式进行处理。
-
性能考虑:虽然
CopyOnWriteArrayList
提供了线程安全的迭代机制,但在写操作频繁的情况下,可能会产生较高的开销,因为每次写操作都需要拷贝整个数组。因此,对于写操作频繁的场景,可能需要考虑其他更合适的并发集合类。
总的来说,CopyOnWriteArrayList
是一种适用于读多写少的场景下保证线程安全的并发集合类,能够有效地解决在多线程环境中的并发访问问题。
多线程环境使用哈希表
HashMap 本身不是线程安全的,我们可以使用Hashtable, ConcurrentHashMap
Hashtable
-
线程安全性:
Hashtable
是线程安全的,所有的公共方法都是同步的,因此可以在多线程环境中安全地使用。
-
性能:
Hashtable
的所有方法都是同步的,而且是对整个HashTable加锁,这意味着在高并发的情况下可能会出现性能瓶颈。因为每次操作都需要获得锁来保证线程安全性。Hashtable
使用一个称为扩容阈值(Expansion Threshold)的参数来控制何时需要进行扩容。当添加元素时,如果元素数量超过了当前容量乘以加载因子(Load Factor),就会触发扩容操作。默认情况下,加载因子是 0.75。扩容操作会创建一个新的数组,大小是原数组的两倍加一,然后将原有数据重新哈希到新数组中。因为Hashtable
的所有方法都是同步的,所以在进行扩容时会锁定整个哈希表,可能会引起性能不稳定。
ConcurrentHashMap
- 线程安全性:
ConcurrentHashMap
通过使用分段锁(Segment Locking)来实现线程安全性,它将整个存储空间分成多个段(Segment),每个段拥有自己的锁,这里的段在Java8以后可以简单理解成HashAMap的每一个链表或者树,锁对象则是链表头节点或者树的根节点,因此可以在大部分情况下实现更高的并发度。在Java 8及以后的版本中,ConcurrentHashMap
使用了 CAS (Compare and Swap) 操作来进一步提高性能,比如在size()方法上使用CAS实现了轻量级锁,避免了重量级锁的出现。
- 性能:
- 由于
ConcurrentHashMap
使用了分段锁和 CAS 操作,因此在高并发的情况下,通常比Hashtable
的性能要好。它提供了更好的并发性能,更适合大规模并发访问的场景。 - 在添加元素时,
ConcurrentHashMap
在初始化时会创建一定数量的段,每个段内部都是一个独立的哈希表。当添加元素时,只会锁定对应段的锁,其他段的数据仍然可以被并发访问。 - 扩容操作会在添加元素时自动触发,并且是分段进行的。每个段在达到一定的容量阈值时会触发扩容操作,而不是等待整个哈希表的容量达到阈值。
ConcurrentHashMap
的扩容是分段进行的,因此不会阻塞整个哈希表,可以在一定程度上提高并发性能。
- 由于