集合的线程安全安全问题
什么是集合的线程安全问题
集合的线程安全问题是指当多个线程同时访问或修改一个集合时,可能会导致数据的不一致或其他并发问题。这是因为在单线程环境下,程序按照顺序执行,每个操作都是连续的,而在多线程环境下,程序执行是交错的,多个线程可能同时对同一数据进行操作,这就可能导致数据的不一致
代码示例:创建100个线程对List集合添加数据,看看在并发的情况下会出现什么问题
public static void main(String[] args) {
List list = new ArrayList<>();
for(int i=0;i<100;i++){
new Thread(()->{
list.add(UUID.randomUUID().toString());
System.out.println(list);
}).start();
}
}
执行结果:多执行几次,会发现某一次的执行结果就会出现 java.util.ConcurrentModificationException
为什么会出现并发修改异常?
先查看下ArrayList add()方法源码:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
可以发现add方法并不是线程同步(安全)的,在多线程环境下,当一个线程正在遍历一个集合时,另一个线程对集合进行了修改操作,从而引发了并发修改异常,这种异常通常发生在遍历集合的过程中,如果集合的结构在遍历过程中被修改,比如添加或删除元素,那么遍历操作可能会抛出ConcurrentModificationException异常
如何解决集合的线程安全问题
Java集合中的线程安全可以通过以下几种方式实现:
- 使用同步集合类:Java提供了一些同步集合类,如Vector和Hashtable等。这些集合类在每个方法上进行了同步处理,从而确保了线程安全。但是,使用这些类会导致性能下降,因为每次访问都需要进行同步。
- 使用Collections工具类:Java Collections工具类提供了一些静态方法来转换或操作集合类,这些方法可以确保线程安全。例如,Collections.synchronizedList()方法可以将一个普通列表转换为线程安全的列表
- 使用并发集合类:Java并发包java.util.concurrent 中提供了一些并发集合类,如ConcurrentHashMap和CopyOnWriteArrayList等。这些集合类使用了更高级的并发技术,可以在不阻塞其他线程的情况下进行修改,从而提高了并发性能。
使用同步的集合类进行操作
针对上面的案例,只需要把 ArrayList 替换成 Vector即可;
public static void main(String[] args) {
List list = new Vector();
for(int i=0;i<100;i++){
new Thread(()->{
list.add(UUID.randomUUID().toString());
System.out.println(list);
}).start();
}
}
查看Vector类的add方法:可以看到 add方法是加了 synchronized 关键字来保证了线程同步
/**
* Appends the specified element to the end of this Vector.
*
* @param e element to be appended to this Vector
* @return {@code true} (as specified by {@link Collection#add})
* @since 1.2
*/
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
使用Collections工具类
public static void main(String[] args) {
List list = Collections.synchronizedList(new ArrayList<>());
for(int i=0;i<1000;i++){
new Thread(()->{
list.add(UUID.randomUUID().toString());
System.out.println(list);
}).start();
}
}
Collections.synchronizedList(List<E> list)
:返回一个同步(线程安全的)列表。该列表使用指定列表进行初始化
查看Collections.SynchronizedList 下的 add 方法:代码块中也是加了synchronized关键字保证多线程操作时的同步
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
Collections.SynchronizedList 只是Collections工具中返回线程安全集合的一种,除此之外还有其他几种能够返回线程安全集合的方法:
Collections.synchronizedMap(Map<K,V> map)
:返回一个同步(线程安全的)映射。该映射使用指定映射进行初始化。Collections.synchronizedSet(Set<E> set)
:返回一个同步(线程安全的)集合。该集合使用指定集合进行初始化。Collections.synchronizedSortedMap(SortedMap<K,V> map)
:返回一个同步(线程安全的)排序映射。该映射使用指定排序映射进行初始化。Collections.synchronizedSortedSet(SortedSet<E> set)
:返回一个同步(线程安全的)排序集合。该集合使用指定排序集合进行初始化。
这些方法可以用于将现有的集合转换为线程安全的集合。在这些方法中,使用了java.util.Collections
类的静态方法来返回一个经过同步处理的集合,以确保在多线程环境下对集合的操作是线程安全的
使用并发集合类
并发集合类:是指专为多线程并发操作而设计的集合类,能够提供更高的并发性能。这些并发集合类提供了线程安全的操作,能够在多线程环境下保证数据的一致性和并发访问的正确性。根据不同的应用场景和需求,可以选择适合的并发集合类来满足并发性能的需求
Java.util.concurrent包提供的并发集合类主要包括以下几类:
- 线程安全的List:包括Vector和CopyOnWriteArrayList。Vector是JDK1.0就存在的并发集合类,底层结构也是数组,通过对类中的所有操作进行加锁从而达到线程安全。CopyOnWriteArrayList是写时复制的并发集合,适合读多写少的场景。
- 线程安全的Set:包括ConcurrentHashMap和ConcurrentSkipListSet。ConcurrentHashMap是支持并发的哈希表实现,ConcurrentSkipListSet是基于跳表的并发集合,并发性能优于ConcurrentHashMap。
- 线程安全的Queue:包括ConcurrentLinkedQueue和BlockingQueue。ConcurrentLinkedQueue是基于链接节点的并发队列,适合多生产者多消费者场景。BlockingQueue是阻塞队列,包括ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等。
- 线程安全的Map:包括ConcurrentHashMap和ConcurrentSkipListMap。ConcurrentHashMap是支持并发的哈希表实现,ConcurrentSkipListMap是基于跳表的并发集合,并发性能优于ConcurrentHashMap。
使用CopyOnWirteArrayList 解决上面并发导致的修改异常问题,代码如下:
public static void main(String[] args) {
List list = new CopyOnWriteArrayList();
for(int i=0;i<1000;i++){
new Thread(()->{
list.add(UUID.randomUUID().toString());
System.out.println(list);
}).start();
}
}