You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程读取时覆盖List是否线程安全?

问题

我在类里定义了一个静态List<Book>,内容通过API接口加载文件数据得到,同时存了这个List最后刷新/检查更新的时间戳。所有外部调用的类方法都只对这个List做读取操作,List里的Book对象也只会被读取,不会修改。

每次调用访问List的方法时,我会检查时间戳,如果超出设定时长就去检查API对应的文件有没有变更,要是有变更就启动异步线程重新加载文件,加载完成后直接覆盖原List。

我本来觉得只有读取操作不会有线程安全问题,目前也没发现异常,但大部分访问方法都会遍历List,所以我有个疑问:当线程正在遍历List的时候,另一个线程完成重载并覆盖了List会发生什么?遍历是复制了整个List再遍历,还是逐个索引加载元素?

我知道指定时长后首次调用访问方法的线程会拿到旧List,遍历完才会完成重载,这是可以接受的。

附上代码:

private static List<Book> bookList = new ArrayList<>();
private static long timestamp = 0; 

static
{
    try
    {
        bookList = loadBookList();
        timestamp = System.currentTimeMillis();
    } catch (Exception e)
    {
        log.error("Failed initial load of bookList : " + e.getMessage());
    }
}


public static String getShortFromName(String name) throws UnknownBookException
{
    asyncListCheck();

    for (Book book: bookList)
    {
        if (book.getName().equalsIgnoreCase(name))
        {
            return book.getShort();
        }
    }

    throw new UnknownObjException(MSG_ERR_NOBOOK);
}

[...] More methods that access the list

private static void asyncListCheck() 
{
    new Thread(() ->
    {
        try
        {
            if (isOldList() && fileWasModified())
            {
                bookList = loadBookList();
                timestamp = System.currentTimeMillis();
            }
        } catch (Exception e)
        {
            log.error("Failed reload of bookList : " + e.getMessage());
        }
    }).start();
}

public static List<Book> loadBookList() throws Exception
{
    // Make API call

    List<Object> objList = // Results end up in here
    
    List<Book> list = new ArrayList<>();

    for (Object obj: objList)
    {
        Map<String, Object> bookMap = (Map<String, Object>) obj;

        String name = String.valueOf(bookMap.get("name"));
        [...] more of this

        Book book = new Book();
        [...] set all the fields

        list.add(book);
    }

    return list;
}

另外我觉得用Collections.synchronizedList(list)没意义,因为我是直接覆盖整个List。我也可以先清空List再重新填充,但这好像没必要?


回答

核心问题:遍历中的List被覆盖会发生什么?

完全不会影响当前正在进行的遍历。原因很直白:

  • 遍历开始时,线程已经拿到了bookList当前指向的旧ArrayList实例的引用,后续所有遍历操作都是针对这个旧实例的。
  • 异步线程覆盖的只是静态变量bookList的引用,让它指向新的ArrayList,但旧实例本身还在内存里,直到遍历它的线程用完之前,都不会被GC回收。
  • 你用的增强for循环,本质是通过迭代器遍历,迭代器绑定的是旧List实例,所以全程只会遍历旧数据,不会突然切换到新List。

简单说:遍历线程用旧List,覆盖线程换新引用,两者互不干扰,不会抛出并发修改异常,也不会出现遍历到一半数据突变的情况。

关于线程安全的补充

你的实现本身是线程安全的,两点依据:

  1. 引用赋值是原子操作:Java中对引用类型的赋值不会出现半完成状态,其他线程要么看到旧引用,要么看到新引用,不会看到中间的无效状态。
  2. 元素不可变:Book对象只会被读取不会修改,新加载的List里的Book都是新创建的实例,旧List的Book也不会被改动,不存在元素内部的并发问题。

关于synchronizedList和清空填充的疑问

  • 确实没必要用Collections.synchronizedList:这个工具类是用来保护同一个List实例的读写/写写并发的,而你每次都是直接替换整个引用,根本不会在同一个实例上做修改,所以完全没用。
  • 也没必要清空原List再填充:直接替换引用更高效,还能避免在同一个List实例上修改可能带来的潜在问题(哪怕你现在都是只读,这个习惯也更安全)。

可优化的小细节

当前实现有个小瑕疵:bookList和timestamp的赋值顺序。现在是先替换List再更新时间戳,如果有线程在这两个操作之间访问,会拿到新List但旧时间戳,可能导致不必要的重复检查。可以把顺序反过来:

timestamp = System.currentTimeMillis();
bookList = loadBookList();

这样能保证新List和新时间戳的一致性,避免中间状态的问题。


内容的提问来源于stack exchange,提问作者Patrick

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 14:15:55