更新/写入静态变量的最佳实践是什么?

5
我有一个项目,用于显示部门文档。我将所有文档(从数据库中获取)存储在静态arrayList中。每隔X个小时,我会根据来自数据库的新文档(如果有)重新构建该arrayList。还有一个静态变量来控制是否重建该数组,在执行重建任务的方法中设置和取消设置。每个Web浏览器访问服务器都会创建此类的实例,但文档arrayList和该控制变量在所有类实例之间共享。
Find-Bugs工具抱怨“从实例方法someClassMethod写入静态字段someArrayName和someVariableName”。似乎这不是好的做法(让类实例方法写入静态字段)。是否有好的建议如何解决这个问题?谢谢。

当然可以。由于每个请求(如果是JSF2.0,则为新会话或新视图)都会生成一个类实例,因此我不想创建许多包含相同文档的重复ArrayList。谢谢。 - Charles
Charles:你需要一个单例“管理器”对象,实例可以引用它。使用静态实例来实现单例会产生一些棘手的副作用。更好的方法是创建一个单一的管理器对象,并将每个实例指向该对象。如果您正在使用框架创建实例(而不是自己运行Java),请查找如何创建单例对象。例如,Tomcat有某种上下文可以读取/写入,作为处理持久状态的一种方式--但我对此并不熟悉。 - Jason S
感谢您的帮助。我添加了一个单例类,在其中每隔X小时重建文档静态树或静态ArrayList。由于我正在使用JSF,JSF PhaseListeners被所有JSF Web请求共享,因此我将单例类的实例化放在PhaseListener中,效果很好。虽然在重建树时存在线程访问风险,但我可以将树重建时间设置为午夜,并且该树仅供只读使用。我们的流量较低,所以可能没问题。 - Charles
5个回答

7
根据FindBugs bug descriptions

ST:从实例方法写入静态字段(ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD)

这个实例方法写入了一个静态字段。如果正在操作多个实例,这很难得到正确的结果,并且通常是不好的做法。

除了并发问题之外,这意味着JVM中的所有实例都在访问相同的数据,并且不允许两个独立的实例组。最好使用单例"manager"对象,并将其作为构造函数参数或至少作为setManager()方法参数传递给每个实例。

关于并发问题:如果必须使用静态字段,则应将静态字段设置为final;显式同步是困难的。(还有一些棘手的方面,如果您初始化非final静态字段,在Java Puzzlers书中可以看到,而这超出了我的Java知识范围。)有至少三种处理这种情况的方法(警告,未经测试的代码如下,请先检查再使用):

  1. Use a thread-safe collection, e.g. Collections.synchronizedList wrapped around a list that is not accessed in any other way.

    static final List<Item> items = createThreadSafeCollection();
    
    
    static List<Item> createThreadSafeCollection()
    {
       return Collections.synchronizedList(new ArrayList());
    }
    

    and then later when you are replacing this collection, from an instance:

    List<Item> newItems = getNewListFromSomewhere();
    items.clear();
    items.add(newItems);
    

    The problem with this is that if two instances are doing this sequence at the same time, you could get:

    Instance1: items.clear(); Instance2: items.clear(); Instance1: items.addAll(newItems); Instance2: items.addAll(newItems);

    and get a list that doesn't meet the desired class invariant, namely that you have two groups of newItems in the static list. So this method doesn't work if you are clearing the entire list as one step, and adding items as a second step. (If your instances just need to add an item, though, items.add(newItem) would be safe to use from each instance.)

  2. Synchronize access to the collection.

    You'll need an explicit mechanism for synchronizing here. Synchronized methods won't work because they synchronize on "this", which is not common between the instances. You could use:

    static final private Object lock = new Object();
    static volatile private List<Item> list;
    // technically "list" doesn't need to be final if you
    // make sure you synchronize properly around unit operations.
    
    
    static void setList(List<Item> newList)
    {
      synchronized(lock)
      {
          list = newList;
      }
    }
    
  3. use AtomicReference

    static final private AtomicReference<List<Item>> list;
    
    
    static void setList(List<Item> newList)
    {
      list.set(newList);
    }
    

1

如果我正确理解了您从Find Bugs发布的消息,那么这只是一个警告。

如果您想隐藏警告,请从静态方法进行修改。Find Bugs正在警告您,因为这通常是一个错误。程序员认为他们正在更改某些实例状态,但实际上他们正在更改影响每个实例的某些状态。


它被报告为一个错误(左侧附有一个错误图像)。 - Charles

0

使用Singleton设计模式是一种方法。您可以只有一个包含所需值的对象实例,并通过全局属性访问该实例。优点是,如果以后想要更多实例,则需要修改现有代码的量较少(因为您不会将静态字段更改为实例字段)。


1
单例模式在这里没有帮助——问题在于列表没有同步。单例模式无法保证同步访问。 - hvgotcodes
+1 -- 这是正确的答案。一般来说,静态方法应该是无状态的,而静态字段已经被弃用,因为它们使得在测试中替换对象更加困难。 - Jason S
问题真的是同步吗? - user541686

0

您不需要每次删除列表。根据上面的内容,您将不得不处理多个线程,但是您可以创建ArrayList一次,然后使用clear()和addAll()方法来清除和重新填充。FindBugs应该会很满意,因为您没有设置静态变量。

如果这种技术有任何问题,请随时提出建议 :-)

第二个想法是通过Hibernate从数据库驱动事物。因此,不要维护列表,Hibernate具有内置缓存,因此速度几乎相同。如果您在数据库级别更新数据(这意味着Hibernate不知道),则可以告诉Hibernate清除其缓存并在下次查询时从数据库刷新。


你是正确的!一旦我使用了arrayList.clear(),然后给它分配了一个新的arrayList(由于它是另一个DB模型调用,所以它返回一个新的arrayList()),那么FindBugs就感到高兴了。但问题仍然存在于静态布尔控制变量(控制是否根据计时器重建arrayList)。 - Charles

-1

你不想这样做。每个请求都在自己的线程中运行。如果在浏览器操作上执行的代码修改了列表,那么两个请求可能会同时修改列表,并破坏数据。这就是为什么从非静态上下文访问静态资源不是一个好主意,也可能是你的工具警告你的原因。

看看这个

http://download.oracle.com/javase/6/docs/api/index.html?java/util/concurrent/package-summary.html

特别是关于ArrayList未同步的部分。还要注意,我提到的段落有一个解决方案,具体来说。
List list = Collections.synchronizedList(new ArrayList(...));

这是一种解决方法。但这仍然不是一个好主意,因为它可能会很慢。如果这不是商业级应用程序,并且您没有处理大量数据,那么您可以忽略警告,如果两个请求相互干扰,则可能会发生一些问题。

更好的解决方案:由于您有数据库,因此我建议您根据需要从数据库中获取信息,即随着请求的到来而进行。您可以使用一些缓存技术来提高性能。

我不喜欢单例模式的原因是,即使它消除了警告,它本身并没有解决基本的同步问题。然而,在这种情况下,有一些线程安全的http://en.wikipedia.org/wiki/Singleton_pattern#Traditional_simple_way_using_synchronization,这可能有效。


@Jason,这是真的,但考虑到问题的性质,我认为OP没有正确处理并发。 - hvgotcodes
特别是,静态变量应该在新列表完全(重新)构建之后才设置,而不是在构建过程中设置。这并不能消除同步问题,但它大大缩小了争用窗口。 - David R Tribble

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接