Java并发访问字段,不使用volatile的技巧

时间:2012-03-25 21:08:19

标签: java concurrency synchronized memory-model java-memory-model

前言:我知道在大多数情况下使用易失性字段不会产生任何可衡量的性能损失,但这个问题更具理论性,并且针对具有极高支持度的设计。

我有一个List<Something>的字段,在构造之后填充。为了节省一些性能,我想将List转换为只读地图。在任何时候这样做至少需要一个易变的Map字段,因此可以对所有线程进行更改。

我正在考虑做以下事情:

Map map;

public void get(Object key){
    if(map==null){
        Map temp = new Map();
        for(Object value : super.getList()){
            temp.put(value.getKey(),value);
        }
        map = temp;
    }
     return map.get(key);
}

这可能导致多个线程生成映射,即使它们以序列化方式进入get块。如果线程在地图的不同相同实例上工作,这将不是什么大问题。让我更担心的是:

是否有可能一个线程将新的临时映射分配给映射字段,然后第二个线程看到map!=null,因此访问映射字段而不生成新的映射字段,但令我惊讶的是发现map是空的,因为put操作还没有推到某个共享内存区域?

评论回答:

  • 线程仅在读取后才修改临时地图。
  • 我必须将List转换为Map,因为一些特殊的JAXB设置使得开始使用Map变得不可行。

4 个答案:

答案 0 :(得分:3)

  

是否有可能一个线程将新的临时映射分配给映射字段,然后第二个线程看到map!=null,因此访问映射字段而不生成新的映射字段,但令我惊讶的是发现map是空的,因为put操作还没有推到某个共享内存区域?

是的,这绝对是可能的;例如,优化编译器实际上可以完全摆脱本地temp变量,并且只要一次使用map字段,只要它恢复mapnull in例外情况。

类似地,一个线程也可以看到一个非空的,非空的map,但仍未完全填充。除非您的Map类经过精心设计以允许同时读取和写入(或使用synchronized来避免此问题),否则如果一个线程调用其get方法,您也可能会遇到奇怪的行为而另一个人正在调用put

答案 1 :(得分:1)

您可以在ctor中创建地图并将其声明为最终版吗?如果您没有泄漏地图以便其他人可以修改它,那么应该足以让您的get()安全地被多个线程共享。

答案 2 :(得分:0)

当你真的怀疑其他线程是否可以读取“半完成”地图时 (我不这么认为,但从不说永远;-),你可以试试这个。

地图为空或完成

static class MyMap extends HashMap {
   MyMap (List pList) {
    for(Object value : pList){
        put(value.getKey(), value);
    }
   }
}

MyMap map;

public Object get(Object key){
    if(map==null){
        map = new MyMap (super.getList());
    }
    return map.get(key);
 }

或者有人看到新引入的问题吗?

答案 3 :(得分:0)

除了前面提到的可见性问题之外,原始代码还存在另一个问题,即。它可以在这里抛出NullPointerException:

return this.map.get(key)

这是违反直觉的,但这是错误同步代码所能带来的。

防止这种情况的示例代码:

Map temp;
if ((temp = this.map) == null)
{

    temp = new ImmutableMap(getList());
    this.map = temp;
}
return temp.get(key);