并发场景下Collections.shuffle(List<?> list)使元素重复的问题

本文探讨了在并发场景中使用Collections.shuffle(List)可能导致列表重复的问题,原因在于底层操作的不原子性。通过复制列表并进行操作,解决了线程可见性问题。解决方法包括同步机制和数据副本策略。
摘要由CSDN通过智能技术生成

并发场景下Collections.shuffle(List<?> list)需要注意的问题

场景:

有一个需求需要每个人每次看到的列表是乱序的,使用了Collections.shuffle(List<?> list)来完成这个乱序的功能,但是发现过一段时间就会出现原来没有重复元素的list变重复了。

复现:

public class CollectionShuffle extends Thread{
    private static List<Integer> STATIC_LIST = new ArrayList<>(Arrays.asList(1, 2, 3, 4));

    @Override
    public void run(){
        Collections.shuffle(STATIC_LIST);
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100; i ++) {
            CollectionShuffle thread = new CollectionShuffle();
            thread.start();
        }
        System.out.println(System.currentTimeMillis() - startTime + "ms");
        System.out.println(Arrays.toString(STATIC_LIST.toArray()));
    }
}
10ms
[2, 4, 4, 2]

原因解读:

查看shuffle方法的底层发现,这个方法底层的实现是一个void类型的,是将list转成一个新的Object arr[]对象,然后通过Random随机数来在原集合上修改,这就导致了其他线程可以读到正在进行排序还未完成的集合,此时集合中是有重复对象的。

public static void shuffle(List<?> list, Random rnd) {
        int size = list.size();
        if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
            for (int i=size; i>1; i--)
                swap(list, i-1, rnd.nextInt(i));
        } else {
            Object arr[] = list.toArray();
            // Shuffle array
            for (int i=size; i>1; i--)
                swap(arr, i-1, rnd.nextInt(i));
            ListIterator it = list.listIterator();
            for (int i=0; i<arr.length; i++) {
                it.next();
                it.set(arr[i]);
            }
        }
    }

解决方法1:

每个线程执行shuffle方法前,都从全局变量中拷贝一份即可解决该问题

	@Override
    public void run(){
        STATIC_LIST = new ArrayList<>(STATIC_LIST);
        Collections.shuffle(STATIC_LIST);
    }
11ms
[2, 1, 3, 4]
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会为您分析该源码。 首先,让我们来看看该方法的作用:将列表中的元素随机排列。它接受两个参数:一个是要随机排列的列表,另一个是用于随机化的随机数生成器。如果不提供随机数生成器,则使用默认的随机数生成器。 该方法的源码如下: ```java public static void shuffle(List<?> list, Random rnd) { int size = list.size(); if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) { for (int i=size; i>1; i--) swap(list, i-1, rnd.nextInt(i)); } else { Object arr[] = list.toArray(); // Shuffle array for (int i=size; i>1; i--) swap(arr, i-1, rnd.nextInt(i)); // Dump array back into list ListIterator it = list.listIterator(); for (int i=0; i<arr.length; i++) { it.next(); it.set(arr[i]); } } } private static void swap(Object[] arr, int i, int j) { Object tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } @SuppressWarnings({"rawtypes", "unchecked"}) private static void swap(List<?> list, int i, int j) { // instead of using a raw type here, it's possible to capture // the wildcard but it will require a call to a supplementary // private method final List l = list; l.set(i, l.set(j, l.get(i))); } ``` 该方法首先检查列表的大小是否小于一个阈值(默认为5)。如果是,则使用传统的交换算法来随机排列列表中的元素。如果列表实现了RandomAccess接口(例如ArrayList),则使用传统的交换算法来随机排列列表中的元素。 如果列表大小大于阈值,并且不是RandomAccess的实例(例如LinkedList),则首先将列表转换为数组。然后对数组中的元素进行随机排列,最后将数组中的元素转换回列表。 两个swap()方法用于交换数组或列表中的元素。 总的来说,该方法使用了经典的洗牌算法来随机排列列表中的元素
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值