JUC基础04——集合的线程安全安全问题

29 阅读5分钟

集合的线程安全安全问题

什么是集合的线程安全问题

集合的线程安全问题是指当多个线程同时访问或修改一个集合时,可能会导致数据的不一致或其他并发问题。这是因为在单线程环境下,程序按照顺序执行,每个操作都是连续的,而在多线程环境下,程序执行是交错的,多个线程可能同时对同一数据进行操作,这就可能导致数据的不一致

代码示例:创建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

image.png

为什么会出现并发修改异常?
先查看下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集合中的线程安全可以通过以下几种方式实现:

  1. 使用同步集合类:Java提供了一些同步集合类,如Vector和Hashtable等。这些集合类在每个方法上进行了同步处理,从而确保了线程安全。但是,使用这些类会导致性能下降,因为每次访问都需要进行同步。
  2. 使用Collections工具类:Java Collections工具类提供了一些静态方法来转换或操作集合类,这些方法可以确保线程安全。例如,Collections.synchronizedList()方法可以将一个普通列表转换为线程安全的列表
  3. 使用并发集合类: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工具类

image.png

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工具中返回线程安全集合的一种,除此之外还有其他几种能够返回线程安全集合的方法:

  1. Collections.synchronizedMap(Map<K,V> map):返回一个同步(线程安全的)映射。该映射使用指定映射进行初始化。
  2. Collections.synchronizedSet(Set<E> set):返回一个同步(线程安全的)集合。该集合使用指定集合进行初始化。
  3. Collections.synchronizedSortedMap(SortedMap<K,V> map):返回一个同步(线程安全的)排序映射。该映射使用指定排序映射进行初始化。
  4. Collections.synchronizedSortedSet(SortedSet<E> set):返回一个同步(线程安全的)排序集合。该集合使用指定排序集合进行初始化。

这些方法可以用于将现有的集合转换为线程安全的集合。在这些方法中,使用了java.util.Collections类的静态方法来返回一个经过同步处理的集合,以确保在多线程环境下对集合的操作是线程安全的

使用并发集合类

并发集合类:是指专为多线程并发操作而设计的集合类,能够提供更高的并发性能。这些并发集合类提供了线程安全的操作,能够在多线程环境下保证数据的一致性和并发访问的正确性。根据不同的应用场景和需求,可以选择适合的并发集合类来满足并发性能的需求

Java.util.concurrent包提供的并发集合类主要包括以下几类:

  1. 线程安全的List:包括Vector和CopyOnWriteArrayList。Vector是JDK1.0就存在的并发集合类,底层结构也是数组,通过对类中的所有操作进行加锁从而达到线程安全。CopyOnWriteArrayList是写时复制的并发集合,适合读多写少的场景。
  2. 线程安全的Set:包括ConcurrentHashMap和ConcurrentSkipListSet。ConcurrentHashMap是支持并发的哈希表实现,ConcurrentSkipListSet是基于跳表的并发集合,并发性能优于ConcurrentHashMap。
  3. 线程安全的Queue:包括ConcurrentLinkedQueue和BlockingQueue。ConcurrentLinkedQueue是基于链接节点的并发队列,适合多生产者多消费者场景。BlockingQueue是阻塞队列,包括ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等。
  4. 线程安全的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();
    }
}