java - 迁移到 Linux 后的 Collections.synchronizedMap(Map) - 第二个线程看不到新条目

标签 java linux windows concurrency synchronization

尝试在 Linux 服务器上运行我的 Web 应用程序后,我遇到了以下问题。

在 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 请求并等待 Activity 响应:

@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);
        }
}

同步器类:

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;
    }
}

最佳答案

您的代码带有“先检查后执行”反模式。

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

在这里,您首先检查是否synchronizers包含一个特定的映射然后你行动当映射不存在时放置一个新的映射,但是当你行动时,不能保证你检查的条件仍然成立。

虽然 Collections.synchronizedMap 返回 map 保证线程安全 putget方法,它不(也不能)保证在后续调用 get 之间不会有更新。和 put .

因此,如果两个线程执行上面的代码,则有可能一个线程输入新值而另一个线程已经执行了 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 以解决此使用模式(以及其他)。

将 map 声明更改为

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

本图提供线程安全putget方法,还有允许避免更新的“先检查后执行”反模式的方法。

使用 ConcurrentMap在 Java 8 下特别容易:

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

调用 computeIfAbsent 将得到 ReqRespSynchro<Map> ,如果有的话,否则将执行提供的函数来计算一个将被存储的值,所有这些都具有原子性保证。你简单的地方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 不会修改 map ,但会返回 map 中已包含的值。因此,如果我们遇到这种情况,我们所要做的就是使用现有的而不是我们试图放置的那个。

关于java - 迁移到 Linux 后的 Collections.synchronizedMap(Map) - 第二个线程看不到新条目,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32313568/

相关文章:

windows - 查找在日期之间创建/访问/修改的文件,批处理脚本

mysql - Git Bash 2.5 无法连接到 mysql

linux - 为什么 SSH 会反复显示此消息?

linux - linux中 "ps -ef "和 "ps -ef | more"命令有什么区别

windows - 当我在同一系统中使用 Ubuntu/Windows 时,如何配置 Skype 以保持相同的历史记录位置?

java - 使用 java 进行 Flyway 迁移

java - 在 Hibernate Component 标签中延迟加载不起作用

java - 如何让我的复选框动画在启动其 Onclick 方法之前完成

java - Java 中的 hasNext 代码

php - 确定哪些用户上传了图像?