我可以初始化一次静态成员并在线程之间共享吗?

时间:2013-07-02 09:23:02

标签: java multithreading concurrency static

这是我第一次尝试编写一个使用多线程的程序,所以我在程序中使用并发有几个问题。

我的程序从Web UI获取用户输入,然后使用该用户输入启动进程。我知道我必须使用并发性,因为这个过程需要一个多小时,我不可能让用户在开始下一个过程之前等待一个过程完成。

以下简化代码处理用户输入,然后启动该过程。

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String myInput = request.getParameter("input");
    Thread t = new Thread(new MyRunnable(myInput));
    t.start();

    response.setContentType("text/html");
    PrintWriter out = response.getWriter();
    out.println("Process started!");
    out.close();
}

以下代码简化了我的实际流程。

public class MyRunnable implements Runnable {

    private static HashMap<String,String> mapOfConstants = null;

    private String member;

    public MyRunnable(String member) {
        this.member = member;
    }

    @Override
    public void run() {
        if (mapOfConstants == null) init();
        // and so on...
    }

    private void init() {
        mapOfConstants = new HashMap<String,String>();
        mapOfConstants.put("LOCATION", "http://localhost/folder");
        // and so on...
    }

}

在上面的代码中,我打算将一系列占位符定义为常量,它将存储在HashMap mapOfConstants中。

编辑: 最后,我可能想要使这个地图的初始化从其他地方获取值,比如文本文件。

我的代码是否达到了在MyRunnable的所有实例之间共享此占位符映射的目的,只执行此初始化过程一次?

5 个答案:

答案 0 :(得分:1)

如果你想在所有用户之间分享常数,那么你就走在正确的路径,那么你必须synchronize你的代码。

synchronize代码的最简单方法是编写

public synchronized void run() {
}

请阅读一些关于Java同步的教程,因为这是Java中的一个雷区,即使是经验丰富的开发人员也会遇到问题。

关于第二个问题:请写一个新问题。

答案 1 :(得分:1)

我正在回答您的问题1,您应该发布另一个问题2。

  

我的代码是否达到了共享此占位符映射的目的   跨越MyRunnable的所有实例,执行此初始化过程   只有一次?

是的,但它不是线程安全的。所以你有两个选择:

  

我的回答是假设您不想更改地图内容   运行时,因为你已经告诉它它是一个常量图。

  • 选项1:制作地图final并使用Collections.unmodifiableMap并在static块中对其进行初始化,这样您的代码线程也会安全。

  • 选项2 :(同步)如果你想使用延迟初始化,这里显然不需要,那么你必须使你的代码线程安全。 您的代码不是线程安全的。

原因:多个正在运行的线程可以将地图视为null并调用init,这将多次初始化地图。使用synchronized阻止。

//keeping map `volatile`
private static volatile HashMap<String,String> mapOfConstants = null;

...
if(map == null)
  synchronized(SomeClass.class){
     if(map == null){
        init();
     }
  }

答案 2 :(得分:1)

我知道这不会得到很好的接收,但你的代码几乎没问题。尽管不是严格的线程安全(仅在加载属性方面),它仍然是正确的(就不创建损坏的数据而言)。

最大的变化是:

private void init() {
    HashMap<String,String> tempMap = new HashMap<String,String>(); // <--- new object assigned to a placeholder variable
    tempMap.put("LOCATION", "http://localhost/folder");
    // and so on...
    mapOfConstants = Collections.unmodifiableMap(tempMap); // <--- atomic assignment here
}

假设mapOfConstants实际上是一些标准的属性集,它们将从文件中加载并且永远不会被更改,那么最大的“风险”是前几个任务都会认为地图为空并且每个都加载它。在编写代码时,进一步的风险是多个线程会同时修改它。使用上面修改过的代码可能会有多个版本的地图,但所有版本都是正确的而且没有损坏,因为地图是以原子方式分配的。最终,JVM将整理当前与该静态成员关联的映射,并将任何其他副本收集为垃圾。

答案 3 :(得分:0)

首先,不要在run()方法中初始化地图,不能保证初始化只发生一次。当然,在这种情况下,创建该映射的次数并不重要,最终将设置为静态引用,其他设置为GCed。它只是不漂亮。我建议静态初始化块:

private final static Map<String,String> mapOfConstants;
static {
    Map<String, String> map = new HashMap<String, String>();
    // initialize map.
    map.put("", "");
    ...

    // convert the map into unmodifiable
    mapOfConstants = Collections.unmodifiableMap(map);
}

但是,还有另一种方法可以在多个线程之间共享常量映射。由于您正在考虑从文本文件加载常量,您是否考虑过从Runnable中提取静态引用,在其他地方初始化地图,然后传入引用?构造函数采用额外的地图引用。

public class MyRunnable implements Runnable {

    private final Map<String,String> mapOfConstants = null;

    private String member;

    public MyRunnable(String member, Map<String,String> mapOfConstants) {
        this.member = member;
        this.mapOfConstants = mapOfConstants;
    }
    ....
}

然后在您的servlet中,从MapOfConstantsFactory

获取地图参考
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws     ServletException, IOException {
    Map<String, String> sharedMapOfConstants = MapOfConstantsFactory.getMapOfConstants();
    String myInput = request.getParameter("input");
    Thread t = new Thread(new MyRunnable(myInput, sharedMapOfConstants));
    t.start();

    response.setContentType("text/html");
    PrintWriter out = response.getWriter();
    out.println("Process started!");
    out.close();
}

这样,您可以针对不同的常量配置编写测试,而无需修改Runnable类。

答案 4 :(得分:0)

是的,你可以初始化一次静态成员并在线程之间共享它,但前提是你要小心谨慎。例如,所有 init() 呼叫网站必须同步;也就是说, init() 只能从构造函数或同步方法中调用。或者,您可以在静态初始化程序块中初始化 mapOfConstants 变量。无论您使用哪种方法,您可能还需要将java.util.concurrent.ConcurrentHashMap实现视为 mapOfConstants 变量的具体类型,因为这样可以避免以后的麻烦。

未能适当注意可能会导致您遇到“双重检查锁定”竞争状况。此外,初始化静态引用变量通常被认为是反模式,因为在这种情况下,映射中的条目可以在程序的一次激活内无限制地增长。通常情况下,这种参考的唯一可接受的用法是,如果内容是,嗯,不变,或者内容增长非常缓慢 - 随着时间的推移,以对数方式思考。使用java.util.WeakHashMap可能会有助于对数增长,或者在weak references的明智使用下,可能会有助于对数增长。

到目前为止,重点似乎是初始化静态 mapOfConstants 变量。但是这忘记了地图的整个目的是(通常)存储一些东西,以便在最好的情况下在O(1)时间内进行后续检索。请记住,当存储和检索操作(在 mapOfConstants 变量上)跨越线程边界时,那些操作也必须同步。缺少同步,一个线程的添加或编辑可能会被其他线程遗漏,这可能会破坏您的程序的数据完整性。