迁移到Linux后的Collections.synchronizedMap(Map) - 第二个Thread没有看到新条目

时间:2015-08-31 14:25:15

标签: java linux windows concurrency synchronization

尝试在Linux服务器上运行我的webapplication后出现以下问题。

在Windows上运行时,一切运行正常(简化版) - 调用send()方法,等待同步器对象的JMS响应,将响应发送给客户端)...

当在linux服务器上启动时(相同的JVM版本 - 1.7,字节码 - java 1.5版本),我只得到第一条消息的响应,并在日志中跟踪其余消息的错误:

synchronizer is null /*my_generated_message_id*/

看起来JMS消息监听器线程无法在同步器映射中看到新条目(在JMS发送方线程中创建),但我不明白为什么......

同步器地图定义:

public final Map<String, ReqRespSynchro<Map>> synchronizers 
    = Collections.synchronizedMap(new HashMap<String, ReqRespSynchro<Map>>());

发送带有活动响应的JMS请求,等待:

@Override
public Map send(Map<String,Object> params) {
    String msgIdent  = ""/*my_generated_message_id*/;
    Map response = null;

    ReqRespSynchro<Map> synchronizer = synchronizers.get(msgIdent);
    if (synchronizer == null) {
        synchronizer = new ReqRespSynchro<Map>();
        synchronizers.put(msgIdent , synchronizer);
    }

    synchronized(synchronizer) {
        try {
                sender.send(params);
        } catch (Exception ex) {
            log.error("send error", ex);
        }

        synchronizer.initSendSequence();
        int iter = 1;
        try {
            while (!synchronizer.isSet() && iter > 0) {
                synchronizer.wait(this.waitTimeout);
                iter--;
            }    
        } catch (Exception ex) {
            log.error("send error 2", ex);
            return null;
        } finally {
            response = (synchronizers.remove(msgIdent )).getRespObject();
        }           
    }
    return response;
}

JMS onMessage响应处理(单独的线程):

public void onMessage(Message msg) {
        Map<String,Object> response = (Map<String,Object>) om.getObject();
        String msgIdent = response.getMyMsgID(); ///*my_generated_message_id*/

        ReqRespSynchro<Map> synchronizer = synchronizers.get(msgIdent);
        if (synchronizer != null) {
            synchronized (synchronizer) {
                msgSynchronizer.setRespObject(response);
                synchronizer.notify();
            }
        } else {
            log.error("synchronizer is null " + msgIdent);
        }
}

Synchronizer class:

public class ReqRespSynchro<E> {
    private E obj = null;

    public synchronized void setRespObject(E obj) {
        this.obj = obj;
    }

    public synchronized void initSendSequence() {
        this.obj = null;
    }

    public synchronized boolean isSet() {
        return this.obj != null;
    }

    public synchronized E getRespObject() {
        E ret = null;
        ret = obj;              
        return ret;
    }
}

2 个答案:

答案 0 :(得分:2)

你的代码带有“check-then-act”反模式。

ReqRespSynchro<Map> synchronizer = synchronizers.get(msgIdent);
if (synchronizer == null) {
    synchronizer = new ReqRespSynchro<Map>();
    synchronizers.put(msgIdent , synchronizer);
}

在这里,您首先检查 synchronizers是否包含特定的映射,然后通过在映射不存在时添加新映射来 ,但是你行动的时候,没有保证你检查的条件仍然有效。

虽然Collections.synchronizedMap返回的地图保证了线程安全putget方法,但它没有(也不能)保证后续之间不会有更新调用getput

因此,如果两个线程执行上面的代码,则一个线程可能会放置一个新值而另一个线程已执行get操作但不执行put操作,因此将继续执行提出一个新的价值,覆盖现有的。因此,线程将使用不同的ReqRespSynchro实例,因此其他线程将从地图中获取其中任何一个。

正确的用法是同步整个复合操作:

synchronized(synchronizers) {
    ReqRespSynchro<Map> synchronizer = synchronizers.get(msgIdent);
    if (synchronizer == null) {
        synchronizer = new ReqRespSynchro<Map>();
        synchronizers.put(msgIdent , synchronizer);
    }
}

通常认为通过将地图或集合包装到同步中,每个线程安全问题都已解决,这是一个常见的错误。但是你仍然需要手动考虑访问模式和保护复合操作,所以有时你最好只使用手动锁定并抵制易于使用的同步包装器的诱惑。

但请注意,ConcurrentMap已添加到Java API中以解决此使用模式(以及其他模式)。

将地图声明更改为

public final ConcurrentHashMap<String, ReqRespSynchro<Map>> synchronizers
    = new ConcurrentHashMap<>(); 

此映射提供了线程安全的putget方法,但也提供了避免更新的“check-then-act”反模式的方法。

在Java 8下使用ConcurrentMap非常简单:

ReqRespSynchro<Map> synchronizer = synchronizers
    .computeIfAbsent(msgIdent, key -> new ReqRespSynchro<>());

computeIfAbsent的调用将获得ReqRespSynchro<Map>,如果有的话,否则将执行提供的函数来计算将被存储的值,所有这些都是 atomicity 保证。您只需get现有实例的地方无需更改。

Java 8之前的代码有点复杂:

ReqRespSynchro<Map> synchronizer = synchronizers.get(msgIdent);
if (synchronizer == null) {
    synchronizer = new ReqRespSynchro<>();
    ReqRespSynchro<Map> concurrent = synchronizers.putIfAbsent(msgIdent , synchronizer);
    if(concurrent!=null) synchronizer = concurrent;
}

在这里,我们不能以原子方式执行操作,但我们能够检测是否发生了并发更新。在这种情况下,putIfAbsent不会修改地图,但会返回地图中已包含的值。因此,如果我们遇到这种情况,我们所要做的就是使用现有的而不是我们试图放的那个。

答案 1 :(得分:0)

如果send()方法中的waitTimeout太短,可能会发生这种情况。您只为等待周期设置了一个迭代。因此,msgIdent条目可以在发送中的finally块中从地图中删除,然后才能在onMessage()中读取:等待超时到期,迭代计数器递减,线程退出循环并从映射中删除条目。

即使waitTimeout足够长,您也可能会遇到所谓的虚假唤醒:

  

线程也可以在没有被通知,中断或超时的情况下唤醒,即所谓的虚假唤醒。虽然这在实践中很少发生,但应用程序必须通过测试应该导致线程被唤醒的条件来防范它,并且如果条件不满足则继续等待。

顺便说一下,为什么不通过JMS发送回复而没有一些神秘的同步?以下是ActiveMQ消息代理的示例:How should I implement request response with JMS?